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
240/// Context of the **current** subroutine call (`wantarray`).
241#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
242pub(crate) enum WantarrayCtx {
243    #[default]
244    Scalar,
245    List,
246    Void,
247}
248
249impl WantarrayCtx {
250    #[inline]
251    pub(crate) fn from_byte(b: u8) -> Self {
252        match b {
253            1 => Self::List,
254            2 => Self::Void,
255            _ => Self::Scalar,
256        }
257    }
258
259    #[inline]
260    pub(crate) fn as_byte(self) -> u8 {
261        match self {
262            Self::Scalar => 0,
263            Self::List => 1,
264            Self::Void => 2,
265        }
266    }
267}
268
269/// Minimum log level filter for `log_*` / `log_json` (trace = most verbose).
270#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
271pub(crate) enum LogLevelFilter {
272    Trace,
273    Debug,
274    Info,
275    Warn,
276    Error,
277}
278
279impl LogLevelFilter {
280    pub(crate) fn parse(s: &str) -> Option<Self> {
281        match s.trim().to_ascii_lowercase().as_str() {
282            "trace" => Some(Self::Trace),
283            "debug" => Some(Self::Debug),
284            "info" => Some(Self::Info),
285            "warn" | "warning" => Some(Self::Warn),
286            "error" => Some(Self::Error),
287            _ => None,
288        }
289    }
290
291    pub(crate) fn as_str(self) -> &'static str {
292        match self {
293            Self::Trace => "trace",
294            Self::Debug => "debug",
295            Self::Info => "info",
296            Self::Warn => "warn",
297            Self::Error => "error",
298        }
299    }
300}
301
302/// True when `@$aref->[IX]` / `IX` needs **list** context on the RHS of `=` (multi-slot slice).
303fn arrow_deref_array_assign_rhs_list_ctx(index: &Expr) -> bool {
304    match &index.kind {
305        ExprKind::Range { .. } => true,
306        ExprKind::QW(ws) => ws.len() > 1,
307        ExprKind::List(el) => {
308            if el.len() > 1 {
309                true
310            } else if el.len() == 1 {
311                arrow_deref_array_assign_rhs_list_ctx(&el[0])
312            } else {
313                false
314            }
315        }
316        _ => false,
317    }
318}
319
320/// Wantarray for the RHS of a plain `=` assignment — must match [`crate::compiler::Compiler`] lowering
321/// so `<>` / `readline` list-slurp matches Perl for `@a = <>` (not only `my`/`our`/`local` initializers).
322pub(crate) fn assign_rhs_wantarray(target: &Expr) -> WantarrayCtx {
323    match &target.kind {
324        ExprKind::ArrayVar(_) | ExprKind::HashVar(_) => WantarrayCtx::List,
325        ExprKind::ScalarVar(_) | ExprKind::ArrayElement { .. } | ExprKind::HashElement { .. } => {
326            WantarrayCtx::Scalar
327        }
328        ExprKind::Deref { kind, .. } => match kind {
329            Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
330            Sigil::Array | Sigil::Hash => WantarrayCtx::List,
331        },
332        ExprKind::ArrowDeref {
333            index,
334            kind: DerefKind::Array,
335            ..
336        } => {
337            if arrow_deref_array_assign_rhs_list_ctx(index) {
338                WantarrayCtx::List
339            } else {
340                WantarrayCtx::Scalar
341            }
342        }
343        ExprKind::ArrowDeref {
344            kind: DerefKind::Hash,
345            ..
346        }
347        | ExprKind::ArrowDeref {
348            kind: DerefKind::Call,
349            ..
350        } => WantarrayCtx::Scalar,
351        ExprKind::HashSliceDeref { .. } | ExprKind::HashSlice { .. } => WantarrayCtx::List,
352        ExprKind::ArraySlice { indices, .. } => {
353            if indices.len() > 1 {
354                WantarrayCtx::List
355            } else if indices.len() == 1 {
356                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
357                    WantarrayCtx::List
358                } else {
359                    WantarrayCtx::Scalar
360                }
361            } else {
362                WantarrayCtx::Scalar
363            }
364        }
365        ExprKind::AnonymousListSlice { indices, .. } => {
366            if indices.len() > 1 {
367                WantarrayCtx::List
368            } else if indices.len() == 1 {
369                if arrow_deref_array_assign_rhs_list_ctx(&indices[0]) {
370                    WantarrayCtx::List
371                } else {
372                    WantarrayCtx::Scalar
373                }
374            } else {
375                WantarrayCtx::Scalar
376            }
377        }
378        ExprKind::Typeglob(_) | ExprKind::TypeglobExpr(_) => WantarrayCtx::Scalar,
379        _ => WantarrayCtx::Scalar,
380    }
381}
382
383/// Memoized inputs + result for a non-`g` `regex_match_execute` call. Populated on every
384/// successful match and consulted at the top of the next call; on exact-match (same pattern,
385/// flags, multiline, and haystack content) we skip regex execution + capture-var scope population
386/// entirely, replaying the stored `PerlValue` result. See [`Interpreter::regex_match_memo`].
387#[derive(Clone)]
388pub(crate) struct RegexMatchMemo {
389    pub pattern: String,
390    pub flags: String,
391    pub multiline: bool,
392    pub haystack: String,
393    pub result: PerlValue,
394}
395
396/// Tree-walker state for scalar `..` / `...` (key: `Expr` address).
397#[derive(Clone, Copy, Default)]
398struct FlipFlopTreeState {
399    active: bool,
400    /// Exclusive `...`: `$.` line where the left bound matched — right is only tested when `$.` is
401    /// strictly greater (Perl: do not test the right operand until the next evaluation; for numeric
402    /// `$.` that defers past the left-match line, including multiple evals on that line).
403    exclusive_left_line: Option<i64>,
404}
405
406/// `BufReader` / `print` / `sysread` / `tell` on the same handle share this [`File`] cursor.
407#[derive(Clone)]
408pub(crate) struct IoSharedFile(pub Arc<Mutex<File>>);
409
410impl Read for IoSharedFile {
411    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
412        self.0.lock().read(buf)
413    }
414}
415
416pub(crate) struct IoSharedFileWrite(pub Arc<Mutex<File>>);
417
418impl IoWrite for IoSharedFileWrite {
419    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
420        self.0.lock().write(buf)
421    }
422
423    fn flush(&mut self) -> io::Result<()> {
424        self.0.lock().flush()
425    }
426}
427
428pub struct Interpreter {
429    pub scope: Scope,
430    pub(crate) subs: HashMap<String, Arc<PerlSub>>,
431    pub(crate) file: String,
432    /// File handles: name → writer
433    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
434    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
435    /// Output separator ($,)
436    pub ofs: String,
437    /// Output record separator ($\)
438    pub ors: String,
439    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
440    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
441    pub irs: Option<String>,
442    /// $! — last OS error
443    pub errno: String,
444    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
445    pub errno_code: i32,
446    /// $@ — last eval error (string)
447    pub eval_error: String,
448    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
449    pub eval_error_code: i32,
450    /// When `die` is called with a ref argument, the ref value is preserved here.
451    pub eval_error_value: Option<PerlValue>,
452    /// @ARGV
453    pub argv: Vec<String>,
454    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
455    pub env: IndexMap<String, PerlValue>,
456    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
457    pub env_materialized: bool,
458    /// $0
459    pub program_name: String,
460    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle)
461    pub line_number: i64,
462    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
463    pub last_readline_handle: String,
464    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
465    pub(crate) last_stdin_die_bracket: String,
466    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
467    pub handle_line_numbers: HashMap<String, i64>,
468    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
469    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
470    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
471    pub(crate) flip_flop_active: Vec<bool>,
472    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
473    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
474    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
475    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
476    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
477    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
478    pub(crate) flip_flop_sequence: Vec<i64>,
479    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
480    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
481    /// range on one line return the same sequence number).
482    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
483    /// Scalar `..` / `...` flip-flop for tree-walker (key: `Expr` address).
484    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
485    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
486    pub sigint_pending_caret: Cell<bool>,
487    /// Auto-split mode (-a)
488    pub auto_split: bool,
489    /// Field separator for -F
490    pub field_separator: Option<String>,
491    /// BEGIN blocks
492    begin_blocks: Vec<Block>,
493    /// `UNITCHECK` blocks (LIFO at run)
494    unit_check_blocks: Vec<Block>,
495    /// `CHECK` blocks (LIFO at run)
496    check_blocks: Vec<Block>,
497    /// `INIT` blocks (FIFO at run)
498    init_blocks: Vec<Block>,
499    /// END blocks
500    end_blocks: Vec<Block>,
501    /// -w warnings / `use warnings` / `$^W`
502    pub warnings: bool,
503    /// Output autoflush (`$|`).
504    pub output_autoflush: bool,
505    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
506    pub default_print_handle: String,
507    /// Suppress stdout output (fan workers with progress bars).
508    pub suppress_stdout: bool,
509    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
510    pub child_exit_status: i64,
511    /// Last successful match (`$&`, `${^MATCH}`).
512    pub last_match: String,
513    /// Before match (`` $` ``, `${^PREMATCH}`).
514    pub prematch: String,
515    /// After match (`$'`, `${^POSTMATCH}`).
516    pub postmatch: String,
517    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
518    pub last_paren_match: String,
519    /// List separator for array stringification in concatenation / interpolation (`$"`).
520    pub list_separator: String,
521    /// Script start time (`$^T`) — seconds since Unix epoch.
522    pub script_start_time: i64,
523    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
524    pub compile_hints: i64,
525    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
526    pub warning_bits: i64,
527    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
528    pub global_phase: String,
529    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
530    pub subscript_sep: String,
531    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
532    /// The `stryke` driver sets this from `-i` / `-i.ext`.
533    pub inplace_edit: String,
534    /// `$^D` — debugging flags (integer; mostly ignored).
535    pub debug_flags: i64,
536    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
537    pub perl_debug_flags: i64,
538    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
539    pub eval_nesting: u32,
540    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
541    pub argv_current_file: String,
542    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
543    pub(crate) diamond_next_idx: usize,
544    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
545    pub(crate) diamond_reader: Option<BufReader<File>>,
546    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
547    pub strict_refs: bool,
548    pub strict_subs: bool,
549    pub strict_vars: bool,
550    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
551    pub utf8_pragma: bool,
552    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
553    pub open_pragma_utf8: bool,
554    /// `use feature` — bit flags (`FEAT_*`).
555    pub feature_bits: u64,
556    /// Number of parallel threads
557    pub num_threads: usize,
558    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
559    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
560    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
561    /// Third flag: `$*` multiline (prepends `(?s)` when true).
562    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
563    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
564    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
565    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
566    /// scope population entirely on cache hit.
567    ///
568    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
569    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
570    /// capture-var side-effect replay is forced on the next hit.
571    regex_match_memo: Option<RegexMatchMemo>,
572    /// False when the user (or some non-regex code path) has written to one of the capture
573    /// variables since the last `apply_regex_captures` call. The memoized match result is still
574    /// valid, but the scope side effects need to be reapplied on the next hit.
575    regex_capture_scope_fresh: bool,
576    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
577    pub(crate) regex_pos: HashMap<String, Option<usize>>,
578    /// Persistent storage for `state` variables, keyed by "line:name".
579    pub(crate) state_vars: HashMap<String, PerlValue>,
580    /// Per-frame tracking of state variable bindings: (var_name, state_key).
581    state_bindings_stack: Vec<Vec<(String, String)>>,
582    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
583    pub(crate) rand_rng: StdRng,
584    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
585    pub(crate) dir_handles: HashMap<String, DirHandleState>,
586    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
587    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
588    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
589    pub(crate) pipe_children: HashMap<String, Child>,
590    /// Sockets from `socket` / `accept` / `connect`.
591    pub(crate) socket_handles: HashMap<String, PerlSocket>,
592    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
593    pub(crate) wantarray_kind: WantarrayCtx,
594    /// `struct Name { ... }` definitions (merged from VM chunks and tree-walker).
595    pub struct_defs: HashMap<String, Arc<StructDef>>,
596    /// `enum Name { ... }` definitions (merged from VM chunks and tree-walker).
597    pub enum_defs: HashMap<String, Arc<EnumDef>>,
598    /// `class Name extends ... impl ... { ... }` definitions.
599    pub class_defs: HashMap<String, Arc<ClassDef>>,
600    /// `trait Name { ... }` definitions.
601    pub trait_defs: HashMap<String, Arc<TraitDef>>,
602    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
603    /// call/return (JIT disabled); tree-walker fallback uses per-statement lines and subs.
604    pub profiler: Option<Profiler>,
605    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
606    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
607    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
608    pub(crate) tied_hashes: HashMap<String, PerlValue>,
609    /// `tie $name` — TIESCALAR object for FETCH/STORE.
610    pub(crate) tied_scalars: HashMap<String, PerlValue>,
611    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
612    pub(crate) tied_arrays: HashMap<String, PerlValue>,
613    /// `use overload` — class → Perl overload key → short method name in that package.
614    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
615    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
616    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
617    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
618    pub(crate) special_caret_scalars: HashMap<String, PerlValue>,
619    /// `$%` — format output page number.
620    pub format_page_number: i64,
621    /// `$=` — format lines per page.
622    pub format_lines_per_page: i64,
623    /// `$-` — lines remaining on format page.
624    pub format_lines_left: i64,
625    /// `$:` — characters to break format lines (Perl default `\n`).
626    pub format_line_break_chars: String,
627    /// `$^` — top-of-form format name.
628    pub format_top_name: String,
629    /// `$^A` — format write accumulator.
630    pub accumulator_format: String,
631    /// `$^F` — max system file descriptor (Perl default 2).
632    pub max_system_fd: i64,
633    /// `$^M` — emergency memory buffer (no-op pool in stryke).
634    pub emergency_memory: String,
635    /// `$^N` — last opened named regexp capture name.
636    pub last_subpattern_name: String,
637    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
638    pub inc_hook_index: i64,
639    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
640    pub multiline_match: bool,
641    /// `$^X` — path to this executable (cached).
642    pub executable_path: String,
643    /// `$^L` — formfeed string for formats (Perl default `\f`).
644    pub formfeed_string: String,
645    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
646    pub(crate) glob_handle_alias: HashMap<String, String>,
647    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
648    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
649    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
650    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
651    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
652    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
653    pub(crate) special_var_restore_frames: Vec<Vec<(String, PerlValue)>>,
654    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
655    pub(crate) english_enabled: bool,
656    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
657    pub(crate) english_no_match_vars: bool,
658    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
659    /// available for the rest of the program — Perl exports them into the caller's namespace
660    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
661    pub(crate) english_match_vars_ever_enabled: bool,
662    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
663    english_lexical_scalars: Vec<HashSet<String>>,
664    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
665    our_lexical_scalars: Vec<HashSet<String>>,
666    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
667    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
668    pub vm_jit_enabled: bool,
669    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
670    pub disasm_bytecode: bool,
671    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from a `.pec` cache hit. When
672    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
673    /// (`.take()`) on first read so re-entry compiles normally.
674    pub pec_precompiled_chunk: Option<crate::bytecode::Chunk>,
675    /// Sideband: fingerprint to save the compiled chunk under after a cache miss (pairs with
676    /// [`crate::pec::try_save`]). `None` when the cache is disabled or the caller does not want
677    /// the compiled chunk persisted.
678    pub pec_cache_fingerprint: Option<[u8; 32]>,
679    /// Set while stepping a `gen { }` body (`yield`).
680    pub(crate) in_generator: bool,
681    /// `-n`/`-p` driver: prelude only in [`Self::execute_tree`]; body runs in [`Self::process_line`].
682    pub line_mode_skip_main: bool,
683    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
684    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
685    /// matches Perl (true on the last line of that source).
686    pub(crate) line_mode_eof_pending: bool,
687    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
688    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
689    pub line_mode_stdin_pending: VecDeque<String>,
690    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
691    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
692    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
693    pub(crate) log_level_override: Option<LogLevelFilter>,
694    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
695    /// Pushed on `call_sub` entry, popped on exit.
696    pub(crate) current_sub_stack: Vec<Arc<PerlSub>>,
697    /// Interactive debugger state (`-d` flag).
698    pub debugger: Option<crate::debugger::Debugger>,
699    /// Call stack for debugger: (sub_name, call_line).
700    pub(crate) debug_call_stack: Vec<(String, usize)>,
701}
702
703/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
704#[derive(Debug, Clone, Default)]
705pub struct ReplCompletionSnapshot {
706    pub subs: Vec<String>,
707    pub blessed_scalars: HashMap<String, String>,
708    pub isa_for_class: HashMap<String, Vec<String>>,
709}
710
711impl ReplCompletionSnapshot {
712    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
713    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
714        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
715        let mro = linearize_c3(class, &parents, 0);
716        let mut names = HashSet::new();
717        for pkg in &mro {
718            if pkg == "UNIVERSAL" {
719                continue;
720            }
721            let prefix = format!("{}::", pkg);
722            for k in &self.subs {
723                if k.starts_with(&prefix) {
724                    let rest = &k[prefix.len()..];
725                    if !rest.contains("::") {
726                        names.insert(rest.to_string());
727                    }
728                }
729            }
730        }
731        for k in &self.subs {
732            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
733                if !rest.contains("::") {
734                    names.insert(rest.to_string());
735                }
736            }
737        }
738        let mut v: Vec<String> = names.into_iter().collect();
739        v.sort();
740        v
741    }
742}
743
744fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
745    let left = left.trim_end();
746    if left.is_empty() {
747        return None;
748    }
749    if let Some(i) = left.rfind('$') {
750        let name = left[i + 1..].trim();
751        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
752            return state.blessed_scalars.get(name).cloned();
753        }
754    }
755    let tok = left.split_whitespace().last()?;
756    if tok.contains("::") {
757        return Some(tok.to_string());
758    }
759    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
760        return Some(tok.to_string());
761    }
762    None
763}
764
765/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
766pub fn repl_arrow_method_completions(
767    state: &ReplCompletionSnapshot,
768    line: &str,
769    pos: usize,
770) -> Option<(usize, Vec<String>)> {
771    let pos = pos.min(line.len());
772    let before = &line[..pos];
773    let arrow_idx = before.rfind("->")?;
774    let after_arrow = &before[arrow_idx + 2..];
775    let rest = after_arrow.trim_start();
776    let ws_len = after_arrow.len() - rest.len();
777    let method_start = arrow_idx + 2 + ws_len;
778    let method_prefix = &line[method_start..pos];
779    if !method_prefix
780        .chars()
781        .all(|c| c.is_alphanumeric() || c == '_')
782    {
783        return None;
784    }
785    let left = line[..arrow_idx].trim_end();
786    let class = repl_resolve_class_for_arrow(state, left)?;
787    let mut methods = state.methods_for_class(&class);
788    methods.retain(|m| m.starts_with(method_prefix));
789    Some((method_start, methods))
790}
791
792/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
793#[derive(Debug, Clone, Default)]
794pub(crate) struct ModuleExportLists {
795    /// Default imports for `use Module` with no list.
796    pub export: Vec<String>,
797    /// Extra symbols allowed in `use Module qw(name)`.
798    pub export_ok: Vec<String>,
799}
800
801/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
802fn piped_shell_command(cmd: &str) -> Command {
803    if cfg!(windows) {
804        let mut c = Command::new("cmd");
805        c.arg("/C").arg(cmd);
806        c
807    } else {
808        let mut c = Command::new("sh");
809        c.arg("-c").arg(cmd);
810        c
811    }
812}
813
814/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
815/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
816/// so the Rust `regex` crate can match them.
817/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
818/// so the Rust regex crate can match NUL and other octal-specified bytes.
819/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
820fn expand_perl_regex_octal_escapes(pat: &str) -> String {
821    let mut out = String::with_capacity(pat.len());
822    let mut it = pat.chars().peekable();
823    while let Some(c) = it.next() {
824        if c == '\\' {
825            if let Some(&'0') = it.peek() {
826                // Collect up to 3 octal digits starting with '0'
827                let mut oct = String::new();
828                while oct.len() < 3 {
829                    if let Some(&d) = it.peek() {
830                        if ('0'..='7').contains(&d) {
831                            oct.push(d);
832                            it.next();
833                        } else {
834                            break;
835                        }
836                    } else {
837                        break;
838                    }
839                }
840                if let Ok(val) = u8::from_str_radix(&oct, 8) {
841                    out.push_str(&format!("\\x{:02x}", val));
842                } else {
843                    out.push('\\');
844                    out.push_str(&oct);
845                }
846                continue;
847            }
848        }
849        out.push(c);
850    }
851    out
852}
853
854fn expand_perl_regex_quotemeta(pat: &str) -> String {
855    let mut out = String::with_capacity(pat.len().saturating_mul(2));
856    let mut it = pat.chars().peekable();
857    let mut in_q = false;
858    while let Some(c) = it.next() {
859        if in_q {
860            if c == '\\' && it.peek() == Some(&'E') {
861                it.next();
862                in_q = false;
863                continue;
864            }
865            out.push_str(&perl_quotemeta(&c.to_string()));
866            continue;
867        }
868        if c == '\\' && it.peek() == Some(&'Q') {
869            it.next();
870            in_q = true;
871            continue;
872        }
873        out.push(c);
874    }
875    out
876}
877
878/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
879///
880/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
881/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
882///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
883pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
884    let mut out = String::with_capacity(replacement.len() + 8);
885    let mut it = replacement.chars().peekable();
886    while let Some(c) = it.next() {
887        if c == '\\' {
888            match it.peek() {
889                Some(&d) if d.is_ascii_digit() => {
890                    it.next();
891                    out.push_str("${");
892                    out.push(d);
893                    while let Some(&d2) = it.peek() {
894                        if !d2.is_ascii_digit() {
895                            break;
896                        }
897                        it.next();
898                        out.push(d2);
899                    }
900                    out.push('}');
901                }
902                Some(&'\\') => {
903                    it.next();
904                    out.push('\\');
905                }
906                _ => out.push('\\'),
907            }
908        } else if c == '$' {
909            match it.peek() {
910                Some(&d) if d.is_ascii_digit() => {
911                    it.next();
912                    out.push_str("${");
913                    out.push(d);
914                    while let Some(&d2) = it.peek() {
915                        if !d2.is_ascii_digit() {
916                            break;
917                        }
918                        it.next();
919                        out.push(d2);
920                    }
921                    out.push('}');
922                }
923                Some(&'{') => {
924                    // already braced — pass through as-is
925                    out.push('$');
926                }
927                _ => out.push('$'),
928            }
929        } else {
930            out.push(c);
931        }
932    }
933    out
934}
935
936/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
937/// past the closing `]`.
938fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
939    debug_assert_eq!(chars.get(i), Some(&'['));
940    out.push('[');
941    i += 1;
942    if i < chars.len() && chars[i] == '^' {
943        out.push('^');
944        i += 1;
945    }
946    if i >= chars.len() {
947        return i;
948    }
949    // `]` as the first class character is literal iff another unescaped `]` closes the class
950    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
951    // this `]`.
952    if chars[i] == ']' {
953        if i + 1 < chars.len() && chars[i + 1] == ']' {
954            // `[]]` / `[^]]`: literal `]` then the closing `]`.
955            out.push(']');
956            i += 1;
957        } else {
958            let mut scan = i + 1;
959            let mut found_closing = false;
960            while scan < chars.len() {
961                if chars[scan] == '\\' && scan + 1 < chars.len() {
962                    scan += 2;
963                    continue;
964                }
965                if chars[scan] == ']' {
966                    found_closing = true;
967                    break;
968                }
969                scan += 1;
970            }
971            if found_closing {
972                out.push(']');
973                i += 1;
974            } else {
975                out.push(']');
976                return i + 1;
977            }
978        }
979    }
980    while i < chars.len() && chars[i] != ']' {
981        if chars[i] == '\\' && i + 1 < chars.len() {
982            out.push(chars[i]);
983            out.push(chars[i + 1]);
984            i += 2;
985            continue;
986        }
987        out.push(chars[i]);
988        i += 1;
989    }
990    if i < chars.len() {
991        out.push(']');
992        i += 1;
993    }
994    i
995}
996
997/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
998/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
999/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1000/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1001fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1002    if multiline_flag {
1003        return pat.to_string();
1004    }
1005    let chars: Vec<char> = pat.chars().collect();
1006    let mut out = String::with_capacity(pat.len().saturating_add(16));
1007    let mut i = 0usize;
1008    while i < chars.len() {
1009        let c = chars[i];
1010        if c == '\\' && i + 1 < chars.len() {
1011            out.push(c);
1012            out.push(chars[i + 1]);
1013            i += 2;
1014            continue;
1015        }
1016        if c == '[' {
1017            i = copy_regex_char_class(&chars, i, &mut out);
1018            continue;
1019        }
1020        if c == '$' {
1021            if let Some(&next) = chars.get(i + 1) {
1022                if next.is_ascii_digit() {
1023                    out.push(c);
1024                    i += 1;
1025                    continue;
1026                }
1027                if next == '{' {
1028                    out.push(c);
1029                    i += 1;
1030                    continue;
1031                }
1032                if next.is_ascii_alphanumeric() || next == '_' {
1033                    out.push(c);
1034                    i += 1;
1035                    continue;
1036                }
1037            }
1038            out.push_str("(?=\\n?\\z)");
1039            i += 1;
1040            continue;
1041        }
1042        out.push(c);
1043        i += 1;
1044    }
1045    out
1046}
1047
1048/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1049#[derive(Debug, Clone)]
1050pub(crate) struct DirHandleState {
1051    pub entries: Vec<String>,
1052    pub pos: usize,
1053}
1054
1055/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1056pub(crate) fn perl_osname() -> String {
1057    match std::env::consts::OS {
1058        "linux" => "linux".to_string(),
1059        "macos" => "darwin".to_string(),
1060        "windows" => "MSWin32".to_string(),
1061        other => other.to_string(),
1062    }
1063}
1064
1065fn perl_version_v_string() -> String {
1066    format!("v{}", env!("CARGO_PKG_VERSION"))
1067}
1068
1069fn extended_os_error_string() -> String {
1070    std::io::Error::last_os_error().to_string()
1071}
1072
1073#[cfg(unix)]
1074fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1075    unsafe {
1076        (
1077            libc::getuid() as i64,
1078            libc::geteuid() as i64,
1079            libc::getgid() as i64,
1080            libc::getegid() as i64,
1081        )
1082    }
1083}
1084
1085#[cfg(not(unix))]
1086fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1087    (0, 0, 0, 0)
1088}
1089
1090fn unix_id_for_special(name: &str) -> i64 {
1091    let (r, e, _, _) = unix_real_effective_ids();
1092    match name {
1093        "<" => r,
1094        ">" => e,
1095        _ => 0,
1096    }
1097}
1098
1099#[cfg(unix)]
1100fn unix_group_list_string(primary: libc::gid_t) -> String {
1101    let mut buf = vec![0 as libc::gid_t; 256];
1102    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1103    if n <= 0 {
1104        return format!("{}", primary);
1105    }
1106    let mut parts = vec![format!("{}", primary)];
1107    for g in buf.iter().take(n as usize) {
1108        parts.push(format!("{}", g));
1109    }
1110    parts.join(" ")
1111}
1112
1113/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1114#[cfg(unix)]
1115fn unix_group_list_for_special(name: &str) -> String {
1116    let (_, _, gid, egid) = unix_real_effective_ids();
1117    match name {
1118        "(" => unix_group_list_string(gid as libc::gid_t),
1119        ")" => unix_group_list_string(egid as libc::gid_t),
1120        _ => String::new(),
1121    }
1122}
1123
1124#[cfg(not(unix))]
1125fn unix_group_list_for_special(_name: &str) -> String {
1126    String::new()
1127}
1128
1129/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1130/// `~/.ssh/config` and keys).
1131#[cfg(unix)]
1132fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1133    use libc::{getpwuid_r, getuid};
1134    use std::ffi::CStr;
1135    use std::os::unix::ffi::OsStringExt;
1136    let uid = unsafe { getuid() };
1137    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1138    let mut result: *mut libc::passwd = std::ptr::null_mut();
1139    let mut buf = vec![0u8; 16_384];
1140    let rc = unsafe {
1141        getpwuid_r(
1142            uid,
1143            &mut pw,
1144            buf.as_mut_ptr().cast::<libc::c_char>(),
1145            buf.len(),
1146            &mut result,
1147        )
1148    };
1149    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1150        return None;
1151    }
1152    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1153    if bytes.is_empty() {
1154        return None;
1155    }
1156    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1157}
1158
1159/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1160#[cfg(unix)]
1161fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1162    use libc::getpwnam_r;
1163    use std::ffi::{CStr, CString};
1164    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1165    let bytes = login.as_bytes();
1166    if bytes.is_empty() || bytes.contains(&0) {
1167        return None;
1168    }
1169    let cname = CString::new(bytes).ok()?;
1170    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1171    let mut result: *mut libc::passwd = std::ptr::null_mut();
1172    let mut buf = vec![0u8; 16_384];
1173    let rc = unsafe {
1174        getpwnam_r(
1175            cname.as_ptr(),
1176            &mut pw,
1177            buf.as_mut_ptr().cast::<libc::c_char>(),
1178            buf.len(),
1179            &mut result,
1180        )
1181    };
1182    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1183        return None;
1184    }
1185    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1186    if dir_bytes.is_empty() {
1187        return None;
1188    }
1189    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1190}
1191
1192impl Default for Interpreter {
1193    fn default() -> Self {
1194        Self::new()
1195    }
1196}
1197
1198/// How [`Interpreter::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1199#[derive(Clone, Copy)]
1200pub(crate) enum CaptureAllMode {
1201    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1202    Empty,
1203    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1204    Append,
1205    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1206    Skip,
1207}
1208
1209impl Interpreter {
1210    pub fn new() -> Self {
1211        let mut scope = Scope::new();
1212        scope.declare_array("INC", vec![PerlValue::string(".".to_string())]);
1213        scope.declare_hash("INC", IndexMap::new());
1214        scope.declare_array("ARGV", vec![]);
1215        scope.declare_array("_", vec![]);
1216        scope.declare_hash("ENV", IndexMap::new());
1217        scope.declare_hash("SIG", IndexMap::new());
1218        // Reflection hashes — populated from `build.rs`-generated tables so
1219        // they track the real parser/dispatcher/LSP without hand-maintenance.
1220        // Seven hashes; all lookups are O(1). Forward maps:
1221        //   %b  / %stryke::builtins      — name → category ("parallel", "string", …)
1222        //   %pc / %stryke::perl_compats  — subset: Perl 5 core only
1223        //   %e  / %stryke::extensions    — subset: stryke-only
1224        //   %a  / %stryke::aliases       — alias → primary
1225        //   %d  / %stryke::descriptions  — name → LSP one-liner (sparse)
1226        // Inverted indexes for constant-time reverse queries:
1227        //   %c  / %stryke::categories    — category → arrayref of names
1228        //   %p  / %stryke::primaries     — primary → arrayref of aliases
1229        //
1230        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1231        // together they cover `keys %builtins`. Short aliases use the
1232        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1233        let builtins_map = crate::builtins::builtins_hash_map();
1234        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1235        let extensions_map = crate::builtins::extensions_hash_map();
1236        let aliases_map = crate::builtins::aliases_hash_map();
1237        let descriptions_map = crate::builtins::descriptions_hash_map();
1238        let categories_map = crate::builtins::categories_hash_map();
1239        let primaries_map = crate::builtins::primaries_hash_map();
1240        let all_map = crate::builtins::all_hash_map();
1241        scope.declare_hash("stryke::builtins", builtins_map.clone());
1242        scope.declare_hash("stryke::perl_compats", perl_compats_map.clone());
1243        scope.declare_hash("stryke::extensions", extensions_map.clone());
1244        scope.declare_hash("stryke::aliases", aliases_map.clone());
1245        scope.declare_hash("stryke::descriptions", descriptions_map.clone());
1246        scope.declare_hash("stryke::categories", categories_map.clone());
1247        scope.declare_hash("stryke::primaries", primaries_map.clone());
1248        scope.declare_hash("stryke::all", all_map.clone());
1249        scope.declare_scalar(
1250            "stryke::VERSION",
1251            PerlValue::string(env!("CARGO_PKG_VERSION").to_string()),
1252        );
1253        scope.declare_hash("b", builtins_map);
1254        scope.declare_hash("pc", perl_compats_map);
1255        scope.declare_hash("e", extensions_map);
1256        scope.declare_hash("a", aliases_map);
1257        scope.declare_hash("d", descriptions_map);
1258        scope.declare_hash("c", categories_map);
1259        scope.declare_hash("p", primaries_map);
1260        scope.declare_hash("all", all_map);
1261        scope.declare_array("-", vec![]);
1262        scope.declare_array("+", vec![]);
1263        scope.declare_array("^CAPTURE", vec![]);
1264        scope.declare_array("^CAPTURE_ALL", vec![]);
1265        scope.declare_hash("^HOOK", IndexMap::new());
1266        scope.declare_scalar("~", PerlValue::string("STDOUT".to_string()));
1267
1268        let script_start_time = std::time::SystemTime::now()
1269            .duration_since(std::time::UNIX_EPOCH)
1270            .map(|d| d.as_secs() as i64)
1271            .unwrap_or(0);
1272
1273        let executable_path = cached_executable_path();
1274
1275        let mut special_caret_scalars: HashMap<String, PerlValue> = HashMap::new();
1276        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1277            special_caret_scalars.insert(format!("^{}", name), PerlValue::UNDEF);
1278        }
1279
1280        let mut s = Self {
1281            scope,
1282            subs: HashMap::new(),
1283            struct_defs: HashMap::new(),
1284            enum_defs: HashMap::new(),
1285            class_defs: HashMap::new(),
1286            trait_defs: HashMap::new(),
1287            file: "-e".to_string(),
1288            output_handles: HashMap::new(),
1289            input_handles: HashMap::new(),
1290            ofs: String::new(),
1291            ors: String::new(),
1292            irs: Some("\n".to_string()),
1293            errno: String::new(),
1294            errno_code: 0,
1295            eval_error: String::new(),
1296            eval_error_code: 0,
1297            eval_error_value: None,
1298            argv: Vec::new(),
1299            env: IndexMap::new(),
1300            env_materialized: false,
1301            program_name: "stryke".to_string(),
1302            line_number: 0,
1303            last_readline_handle: String::new(),
1304            last_stdin_die_bracket: "<STDIN>".to_string(),
1305            handle_line_numbers: HashMap::new(),
1306            flip_flop_active: Vec::new(),
1307            flip_flop_exclusive_left_line: Vec::new(),
1308            flip_flop_sequence: Vec::new(),
1309            flip_flop_last_dot: Vec::new(),
1310            flip_flop_tree: HashMap::new(),
1311            sigint_pending_caret: Cell::new(false),
1312            auto_split: false,
1313            field_separator: None,
1314            begin_blocks: Vec::new(),
1315            unit_check_blocks: Vec::new(),
1316            check_blocks: Vec::new(),
1317            init_blocks: Vec::new(),
1318            end_blocks: Vec::new(),
1319            warnings: false,
1320            output_autoflush: false,
1321            default_print_handle: "STDOUT".to_string(),
1322            suppress_stdout: false,
1323            child_exit_status: 0,
1324            last_match: String::new(),
1325            prematch: String::new(),
1326            postmatch: String::new(),
1327            last_paren_match: String::new(),
1328            list_separator: " ".to_string(),
1329            script_start_time,
1330            compile_hints: 0,
1331            warning_bits: 0,
1332            global_phase: "RUN".to_string(),
1333            subscript_sep: "\x1c".to_string(),
1334            inplace_edit: String::new(),
1335            debug_flags: 0,
1336            perl_debug_flags: 0,
1337            eval_nesting: 0,
1338            argv_current_file: String::new(),
1339            diamond_next_idx: 0,
1340            diamond_reader: None,
1341            strict_refs: false,
1342            strict_subs: false,
1343            strict_vars: false,
1344            utf8_pragma: false,
1345            open_pragma_utf8: false,
1346            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1347            feature_bits: FEAT_SAY,
1348            num_threads: 0, // lazily read from rayon on first parallel op
1349            regex_cache: HashMap::new(),
1350            regex_last: None,
1351            regex_match_memo: None,
1352            regex_capture_scope_fresh: false,
1353            regex_pos: HashMap::new(),
1354            state_vars: HashMap::new(),
1355            state_bindings_stack: Vec::new(),
1356            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1357            dir_handles: HashMap::new(),
1358            io_file_slots: HashMap::new(),
1359            pipe_children: HashMap::new(),
1360            socket_handles: HashMap::new(),
1361            wantarray_kind: WantarrayCtx::Scalar,
1362            profiler: None,
1363            module_export_lists: HashMap::new(),
1364            tied_hashes: HashMap::new(),
1365            tied_scalars: HashMap::new(),
1366            tied_arrays: HashMap::new(),
1367            overload_table: HashMap::new(),
1368            format_templates: HashMap::new(),
1369            special_caret_scalars,
1370            format_page_number: 0,
1371            format_lines_per_page: 60,
1372            format_lines_left: 0,
1373            format_line_break_chars: "\n".to_string(),
1374            format_top_name: String::new(),
1375            accumulator_format: String::new(),
1376            max_system_fd: 2,
1377            emergency_memory: String::new(),
1378            last_subpattern_name: String::new(),
1379            inc_hook_index: 0,
1380            multiline_match: false,
1381            executable_path,
1382            formfeed_string: "\x0c".to_string(),
1383            glob_handle_alias: HashMap::new(),
1384            glob_restore_frames: vec![Vec::new()],
1385            special_var_restore_frames: vec![Vec::new()],
1386            english_enabled: false,
1387            english_no_match_vars: false,
1388            english_match_vars_ever_enabled: false,
1389            english_lexical_scalars: vec![HashSet::new()],
1390            our_lexical_scalars: vec![HashSet::new()],
1391            vm_jit_enabled: !matches!(
1392                std::env::var("STRYKE_NO_JIT"),
1393                Ok(v)
1394                    if v == "1"
1395                        || v.eq_ignore_ascii_case("true")
1396                        || v.eq_ignore_ascii_case("yes")
1397            ),
1398            disasm_bytecode: false,
1399            pec_precompiled_chunk: None,
1400            pec_cache_fingerprint: None,
1401            in_generator: false,
1402            line_mode_skip_main: false,
1403            line_mode_eof_pending: false,
1404            line_mode_stdin_pending: VecDeque::new(),
1405            rate_limit_slots: Vec::new(),
1406            log_level_override: None,
1407            current_sub_stack: Vec::new(),
1408            debugger: None,
1409            debug_call_stack: Vec::new(),
1410        };
1411        s.install_overload_pragma_stubs();
1412        crate::list_util::install_scalar_util(&mut s);
1413        crate::list_util::install_sub_util(&mut s);
1414        s.install_utf8_unicode_to_native_stub();
1415        s
1416    }
1417
1418    /// `utf8::unicode_to_native` — core XS in perl; JSON::PP calls it from BEGIN before utf8_heavy.
1419    fn install_utf8_unicode_to_native_stub(&mut self) {
1420        let empty: Block = vec![];
1421        let key = "utf8::unicode_to_native".to_string();
1422        self.subs.insert(
1423            key.clone(),
1424            Arc::new(PerlSub {
1425                name: key,
1426                params: vec![],
1427                body: empty,
1428                prototype: None,
1429                closure_env: None,
1430                fib_like: None,
1431            }),
1432        );
1433    }
1434
1435    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1436    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1437    /// strict subs and to satisfy `use overload ();` call sites.
1438    fn install_overload_pragma_stubs(&mut self) {
1439        let empty: Block = vec![];
1440        for key in ["overload::import", "overload::unimport"] {
1441            let name = key.to_string();
1442            self.subs.insert(
1443                name.clone(),
1444                Arc::new(PerlSub {
1445                    name,
1446                    params: vec![],
1447                    body: empty.clone(),
1448                    prototype: None,
1449                    closure_env: None,
1450                    fib_like: None,
1451                }),
1452            );
1453        }
1454    }
1455
1456    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1457    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1458    pub fn line_mode_worker_clone(&self) -> Interpreter {
1459        Interpreter {
1460            scope: self.scope.clone(),
1461            subs: self.subs.clone(),
1462            struct_defs: self.struct_defs.clone(),
1463            enum_defs: self.enum_defs.clone(),
1464            class_defs: self.class_defs.clone(),
1465            trait_defs: self.trait_defs.clone(),
1466            file: self.file.clone(),
1467            output_handles: HashMap::new(),
1468            input_handles: HashMap::new(),
1469            ofs: self.ofs.clone(),
1470            ors: self.ors.clone(),
1471            irs: self.irs.clone(),
1472            errno: self.errno.clone(),
1473            errno_code: self.errno_code,
1474            eval_error: self.eval_error.clone(),
1475            eval_error_code: self.eval_error_code,
1476            eval_error_value: self.eval_error_value.clone(),
1477            argv: self.argv.clone(),
1478            env: self.env.clone(),
1479            env_materialized: self.env_materialized,
1480            program_name: self.program_name.clone(),
1481            line_number: 0,
1482            last_readline_handle: String::new(),
1483            last_stdin_die_bracket: "<STDIN>".to_string(),
1484            handle_line_numbers: HashMap::new(),
1485            flip_flop_active: Vec::new(),
1486            flip_flop_exclusive_left_line: Vec::new(),
1487            flip_flop_sequence: Vec::new(),
1488            flip_flop_last_dot: Vec::new(),
1489            flip_flop_tree: HashMap::new(),
1490            sigint_pending_caret: Cell::new(false),
1491            auto_split: self.auto_split,
1492            field_separator: self.field_separator.clone(),
1493            begin_blocks: self.begin_blocks.clone(),
1494            unit_check_blocks: self.unit_check_blocks.clone(),
1495            check_blocks: self.check_blocks.clone(),
1496            init_blocks: self.init_blocks.clone(),
1497            end_blocks: self.end_blocks.clone(),
1498            warnings: self.warnings,
1499            output_autoflush: self.output_autoflush,
1500            default_print_handle: self.default_print_handle.clone(),
1501            suppress_stdout: self.suppress_stdout,
1502            child_exit_status: self.child_exit_status,
1503            last_match: self.last_match.clone(),
1504            prematch: self.prematch.clone(),
1505            postmatch: self.postmatch.clone(),
1506            last_paren_match: self.last_paren_match.clone(),
1507            list_separator: self.list_separator.clone(),
1508            script_start_time: self.script_start_time,
1509            compile_hints: self.compile_hints,
1510            warning_bits: self.warning_bits,
1511            global_phase: self.global_phase.clone(),
1512            subscript_sep: self.subscript_sep.clone(),
1513            inplace_edit: self.inplace_edit.clone(),
1514            debug_flags: self.debug_flags,
1515            perl_debug_flags: self.perl_debug_flags,
1516            eval_nesting: self.eval_nesting,
1517            argv_current_file: String::new(),
1518            diamond_next_idx: 0,
1519            diamond_reader: None,
1520            strict_refs: self.strict_refs,
1521            strict_subs: self.strict_subs,
1522            strict_vars: self.strict_vars,
1523            utf8_pragma: self.utf8_pragma,
1524            open_pragma_utf8: self.open_pragma_utf8,
1525            feature_bits: self.feature_bits,
1526            num_threads: 0,
1527            regex_cache: self.regex_cache.clone(),
1528            regex_last: self.regex_last.clone(),
1529            regex_match_memo: self.regex_match_memo.clone(),
1530            regex_capture_scope_fresh: false,
1531            regex_pos: self.regex_pos.clone(),
1532            state_vars: self.state_vars.clone(),
1533            state_bindings_stack: Vec::new(),
1534            rand_rng: self.rand_rng.clone(),
1535            dir_handles: HashMap::new(),
1536            io_file_slots: HashMap::new(),
1537            pipe_children: HashMap::new(),
1538            socket_handles: HashMap::new(),
1539            wantarray_kind: self.wantarray_kind,
1540            profiler: None,
1541            module_export_lists: self.module_export_lists.clone(),
1542            tied_hashes: self.tied_hashes.clone(),
1543            tied_scalars: self.tied_scalars.clone(),
1544            tied_arrays: self.tied_arrays.clone(),
1545            overload_table: self.overload_table.clone(),
1546            format_templates: self.format_templates.clone(),
1547            special_caret_scalars: self.special_caret_scalars.clone(),
1548            format_page_number: self.format_page_number,
1549            format_lines_per_page: self.format_lines_per_page,
1550            format_lines_left: self.format_lines_left,
1551            format_line_break_chars: self.format_line_break_chars.clone(),
1552            format_top_name: self.format_top_name.clone(),
1553            accumulator_format: self.accumulator_format.clone(),
1554            max_system_fd: self.max_system_fd,
1555            emergency_memory: self.emergency_memory.clone(),
1556            last_subpattern_name: self.last_subpattern_name.clone(),
1557            inc_hook_index: self.inc_hook_index,
1558            multiline_match: self.multiline_match,
1559            executable_path: self.executable_path.clone(),
1560            formfeed_string: self.formfeed_string.clone(),
1561            glob_handle_alias: self.glob_handle_alias.clone(),
1562            glob_restore_frames: self.glob_restore_frames.clone(),
1563            special_var_restore_frames: self.special_var_restore_frames.clone(),
1564            english_enabled: self.english_enabled,
1565            english_no_match_vars: self.english_no_match_vars,
1566            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
1567            english_lexical_scalars: self.english_lexical_scalars.clone(),
1568            our_lexical_scalars: self.our_lexical_scalars.clone(),
1569            vm_jit_enabled: self.vm_jit_enabled,
1570            disasm_bytecode: self.disasm_bytecode,
1571            // Sideband cache fields belong to the top-level driver, not line-mode workers.
1572            pec_precompiled_chunk: None,
1573            pec_cache_fingerprint: None,
1574            in_generator: false,
1575            line_mode_skip_main: false,
1576            line_mode_eof_pending: false,
1577            line_mode_stdin_pending: VecDeque::new(),
1578            rate_limit_slots: Vec::new(),
1579            log_level_override: self.log_level_override,
1580            current_sub_stack: Vec::new(),
1581            debugger: None,
1582            debug_call_stack: Vec::new(),
1583        }
1584    }
1585
1586    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
1587    pub(crate) fn parallel_thread_count(&mut self) -> usize {
1588        if self.num_threads == 0 {
1589            self.num_threads = rayon::current_num_threads();
1590        }
1591        self.num_threads
1592    }
1593
1594    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
1595    pub(crate) fn eval_par_list_call(
1596        &mut self,
1597        name: &str,
1598        args: &[PerlValue],
1599        ctx: WantarrayCtx,
1600        line: usize,
1601    ) -> PerlResult<PerlValue> {
1602        match name {
1603            "puniq" => {
1604                let (list_src, show_prog) = match args.len() {
1605                    0 => return Err(PerlError::runtime("puniq: expected LIST", line)),
1606                    1 => (&args[0], false),
1607                    2 => (&args[0], args[1].is_true()),
1608                    _ => {
1609                        return Err(PerlError::runtime(
1610                            "puniq: expected LIST [, progress => EXPR]",
1611                            line,
1612                        ));
1613                    }
1614                };
1615                let list = list_src.to_list();
1616                let n_threads = self.parallel_thread_count();
1617                let pmap_progress = PmapProgress::new(show_prog, list.len());
1618                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
1619                pmap_progress.finish();
1620                if ctx == WantarrayCtx::List {
1621                    Ok(PerlValue::array(out))
1622                } else {
1623                    Ok(PerlValue::integer(out.len() as i64))
1624                }
1625            }
1626            "pfirst" => {
1627                let (code_val, list_src, show_prog) = match args.len() {
1628                    2 => (&args[0], &args[1], false),
1629                    3 => (&args[0], &args[1], args[2].is_true()),
1630                    _ => {
1631                        return Err(PerlError::runtime(
1632                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
1633                            line,
1634                        ));
1635                    }
1636                };
1637                let Some(sub) = code_val.as_code_ref() else {
1638                    return Err(PerlError::runtime(
1639                        "pfirst: first argument must be a code reference",
1640                        line,
1641                    ));
1642                };
1643                let sub = sub.clone();
1644                let list = list_src.to_list();
1645                if list.is_empty() {
1646                    return Ok(PerlValue::UNDEF);
1647                }
1648                let pmap_progress = PmapProgress::new(show_prog, list.len());
1649                let subs = self.subs.clone();
1650                let (scope_capture, atomic_arrays, atomic_hashes) =
1651                    self.scope.capture_with_atomics();
1652                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
1653                    let mut local_interp = Interpreter::new();
1654                    local_interp.subs = subs.clone();
1655                    local_interp.scope.restore_capture(&scope_capture);
1656                    local_interp
1657                        .scope
1658                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1659                    local_interp.enable_parallel_guard();
1660                    local_interp.scope.set_topic(item);
1661                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1662                        Ok(v) => v.is_true(),
1663                        Err(_) => false,
1664                    }
1665                });
1666                pmap_progress.finish();
1667                Ok(out.unwrap_or(PerlValue::UNDEF))
1668            }
1669            "pany" => {
1670                let (code_val, list_src, show_prog) = match args.len() {
1671                    2 => (&args[0], &args[1], false),
1672                    3 => (&args[0], &args[1], args[2].is_true()),
1673                    _ => {
1674                        return Err(PerlError::runtime(
1675                            "pany: expected BLOCK, LIST [, progress => EXPR]",
1676                            line,
1677                        ));
1678                    }
1679                };
1680                let Some(sub) = code_val.as_code_ref() else {
1681                    return Err(PerlError::runtime(
1682                        "pany: first argument must be a code reference",
1683                        line,
1684                    ));
1685                };
1686                let sub = sub.clone();
1687                let list = list_src.to_list();
1688                let pmap_progress = PmapProgress::new(show_prog, list.len());
1689                let subs = self.subs.clone();
1690                let (scope_capture, atomic_arrays, atomic_hashes) =
1691                    self.scope.capture_with_atomics();
1692                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
1693                    let mut local_interp = Interpreter::new();
1694                    local_interp.subs = subs.clone();
1695                    local_interp.scope.restore_capture(&scope_capture);
1696                    local_interp
1697                        .scope
1698                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1699                    local_interp.enable_parallel_guard();
1700                    local_interp.scope.set_topic(item);
1701                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1702                        Ok(v) => v.is_true(),
1703                        Err(_) => false,
1704                    }
1705                });
1706                pmap_progress.finish();
1707                Ok(PerlValue::integer(if b { 1 } else { 0 }))
1708            }
1709            _ => Err(PerlError::runtime(
1710                format!("internal: unknown par_list builtin {name}"),
1711                line,
1712            )),
1713        }
1714    }
1715
1716    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
1717        #[cfg(unix)]
1718        if let Some(sig) = s.signal() {
1719            return sig as i64 & 0x7f;
1720        }
1721        let code = s.code().unwrap_or(0) as i64;
1722        code << 8
1723    }
1724
1725    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
1726        self.child_exit_status = self.encode_exit_status(s);
1727    }
1728
1729    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
1730    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
1731        self.errno = e.to_string();
1732        self.errno_code = e.raw_os_error().unwrap_or(0);
1733    }
1734
1735    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
1736    ///
1737    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
1738    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
1739    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
1740    ///
1741    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
1742    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
1743    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
1744    /// `~/.ssh/config` and keys apply.
1745    pub(crate) fn ssh_builtin_execute(&mut self, args: &[PerlValue]) -> PerlResult<PerlValue> {
1746        use std::process::Command;
1747        let mut cmd = Command::new("ssh");
1748        #[cfg(unix)]
1749        {
1750            use libc::geteuid;
1751            let home_for_ssh = if unsafe { geteuid() } == 0 {
1752                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
1753            } else {
1754                None
1755            };
1756            if let Some(h) = home_for_ssh {
1757                cmd.env("HOME", h);
1758            } else if std::env::var_os("HOME").is_none() {
1759                if let Some(h) = pw_home_dir_for_current_uid() {
1760                    cmd.env("HOME", h);
1761                }
1762            }
1763        }
1764        for a in args {
1765            cmd.arg(a.to_string());
1766        }
1767        match cmd.status() {
1768            Ok(s) => {
1769                self.record_child_exit_status(s);
1770                Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
1771            }
1772            Err(e) => {
1773                self.apply_io_error_to_errno(&e);
1774                Ok(PerlValue::integer(-1))
1775            }
1776        }
1777    }
1778
1779    /// Set `$@` message; numeric side is `0` if empty, else `1`.
1780    pub(crate) fn set_eval_error(&mut self, msg: String) {
1781        self.eval_error = msg;
1782        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
1783        self.eval_error_value = None;
1784    }
1785
1786    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &PerlError) {
1787        self.eval_error = e.to_string();
1788        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
1789        self.eval_error_value = e.die_value.clone();
1790    }
1791
1792    pub(crate) fn clear_eval_error(&mut self) {
1793        self.eval_error = String::new();
1794        self.eval_error_code = 0;
1795        self.eval_error_value = None;
1796    }
1797
1798    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
1799    fn bump_line_for_handle(&mut self, handle_key: &str) {
1800        self.last_readline_handle = handle_key.to_string();
1801        *self
1802            .handle_line_numbers
1803            .entry(handle_key.to_string())
1804            .or_insert(0) += 1;
1805    }
1806
1807    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
1808    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
1809        if name.starts_with('^') {
1810            return name.to_string();
1811        }
1812        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
1813            let pkg = self.current_package();
1814            if !pkg.is_empty() && pkg != "main" {
1815                return format!("{}::{}", pkg, name);
1816            }
1817        }
1818        name.to_string()
1819    }
1820
1821    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
1822    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
1823        if name.contains("::") {
1824            return name.to_string();
1825        }
1826        let pkg = self.current_package();
1827        if pkg.is_empty() || pkg == "main" {
1828            format!("main::{}", name)
1829        } else {
1830            format!("{}::{}", pkg, name)
1831        }
1832    }
1833
1834    /// Tree-walker: bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
1835    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
1836        if name.contains("::") {
1837            return name.to_string();
1838        }
1839        for (lex, our) in self
1840            .english_lexical_scalars
1841            .iter()
1842            .zip(self.our_lexical_scalars.iter())
1843            .rev()
1844        {
1845            if lex.contains(name) {
1846                if our.contains(name) {
1847                    return self.stash_scalar_name_for_package(name);
1848                }
1849                return name.to_string();
1850            }
1851        }
1852        name.to_string()
1853    }
1854
1855    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
1856    pub(crate) fn tie_execute(
1857        &mut self,
1858        target_kind: u8,
1859        target_name: &str,
1860        class_and_args: Vec<PerlValue>,
1861        line: usize,
1862    ) -> PerlResult<PerlValue> {
1863        let mut it = class_and_args.into_iter();
1864        let class = it.next().unwrap_or(PerlValue::UNDEF);
1865        let pkg = class.to_string();
1866        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
1867        let tie_ctor = match target_kind {
1868            0 => "TIESCALAR",
1869            1 => "TIEARRAY",
1870            2 => "TIEHASH",
1871            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
1872        };
1873        let tie_fn = format!("{}::{}", pkg, tie_ctor);
1874        let sub = self
1875            .subs
1876            .get(&tie_fn)
1877            .cloned()
1878            .ok_or_else(|| PerlError::runtime(format!("tie: cannot find &{}", tie_fn), line))?;
1879        let mut call_args = vec![PerlValue::string(pkg.clone())];
1880        call_args.extend(it);
1881        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
1882            Ok(v) => v,
1883            Err(FlowOrError::Flow(_)) => PerlValue::UNDEF,
1884            Err(FlowOrError::Error(e)) => return Err(e),
1885        };
1886        match target_kind {
1887            0 => {
1888                self.tied_scalars.insert(target_name.to_string(), obj);
1889            }
1890            1 => {
1891                let key = self.stash_array_name_for_package(target_name);
1892                self.tied_arrays.insert(key, obj);
1893            }
1894            2 => {
1895                self.tied_hashes.insert(target_name.to_string(), obj);
1896            }
1897            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
1898        }
1899        Ok(PerlValue::UNDEF)
1900    }
1901
1902    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
1903    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
1904        let key = format!("{}::ISA", class);
1905        self.scope
1906            .get_array(&key)
1907            .into_iter()
1908            .map(|v| v.to_string())
1909            .collect()
1910    }
1911
1912    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
1913        let p = |c: &str| self.parents_of_class(c);
1914        linearize_c3(class, &p, 0)
1915    }
1916
1917    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
1918    pub(crate) fn resolve_method_full_name(
1919        &self,
1920        invocant_class: &str,
1921        method: &str,
1922        super_mode: bool,
1923    ) -> Option<String> {
1924        let mro = self.mro_linearize(invocant_class);
1925        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
1926        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
1927        // even when running `C::meth`.
1928        let start = if super_mode {
1929            mro.iter()
1930                .position(|p| p == invocant_class)
1931                .map(|i| i + 1)
1932                // If the class string does not appear in MRO (should be rare), skip the first
1933                // entry so we still search parents before giving up.
1934                .unwrap_or(1)
1935        } else {
1936            0
1937        };
1938        for pkg in mro.iter().skip(start) {
1939            if pkg == "UNIVERSAL" {
1940                continue;
1941            }
1942            let fq = format!("{}::{}", pkg, method);
1943            if self.subs.contains_key(&fq) {
1944                return Some(fq);
1945            }
1946        }
1947        mro.iter()
1948            .skip(start)
1949            .find(|p| *p != "UNIVERSAL")
1950            .map(|pkg| format!("{}::{}", pkg, method))
1951    }
1952
1953    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
1954        if let Some(alias) = self.glob_handle_alias.get(name) {
1955            return alias.clone();
1956        }
1957        // `print $fh …` stores the handle as "$varname"; resolve it by
1958        // reading the scalar variable which holds the IO handle name.
1959        if let Some(var_name) = name.strip_prefix('$') {
1960            let val = self.scope.get_scalar(var_name);
1961            let s = val.to_string();
1962            if !s.is_empty() {
1963                return self.resolve_io_handle_name(&s);
1964            }
1965        }
1966        name.to_string()
1967    }
1968
1969    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
1970    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
1971        if name.contains("::") {
1972            name.to_string()
1973        } else {
1974            self.qualify_sub_key(name)
1975        }
1976    }
1977
1978    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
1979    pub(crate) fn copy_typeglob_slots(
1980        &mut self,
1981        lhs: &str,
1982        rhs: &str,
1983        line: usize,
1984    ) -> PerlResult<()> {
1985        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
1986        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
1987        match self.subs.get(&rhs_sub).cloned() {
1988            Some(s) => {
1989                self.subs.insert(lhs_sub, s);
1990            }
1991            None => {
1992                self.subs.remove(&lhs_sub);
1993            }
1994        }
1995        let sv = self.scope.get_scalar(rhs);
1996        self.scope
1997            .set_scalar(lhs, sv.clone())
1998            .map_err(|e| e.at_line(line))?;
1999        let lhs_an = self.stash_array_name_for_package(lhs);
2000        let rhs_an = self.stash_array_name_for_package(rhs);
2001        let av = self.scope.get_array(&rhs_an);
2002        self.scope
2003            .set_array(&lhs_an, av.clone())
2004            .map_err(|e| e.at_line(line))?;
2005        let hv = self.scope.get_hash(rhs);
2006        self.scope
2007            .set_hash(lhs, hv.clone())
2008            .map_err(|e| e.at_line(line))?;
2009        match self.glob_handle_alias.get(rhs).cloned() {
2010            Some(t) => {
2011                self.glob_handle_alias.insert(lhs.to_string(), t);
2012            }
2013            None => {
2014                self.glob_handle_alias.remove(lhs);
2015            }
2016        }
2017        Ok(())
2018    }
2019
2020    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2021    pub(crate) fn install_format_decl(
2022        &mut self,
2023        basename: &str,
2024        lines: &[String],
2025        line: usize,
2026    ) -> PerlResult<()> {
2027        let pkg = self.current_package();
2028        let key = format!("{}::{}", pkg, basename);
2029        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2030        self.format_templates.insert(key, Arc::new(tmpl));
2031        Ok(())
2032    }
2033
2034    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2035    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2036        let pkg = self.current_package();
2037        let ent = self.overload_table.entry(pkg).or_default();
2038        for (k, v) in pairs {
2039            ent.insert(k.clone(), v.clone());
2040        }
2041    }
2042
2043    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2044    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2045    pub(crate) fn local_declare_typeglob(
2046        &mut self,
2047        lhs: &str,
2048        rhs: Option<&str>,
2049        line: usize,
2050    ) -> PerlResult<()> {
2051        let old = self.glob_handle_alias.remove(lhs);
2052        let Some(frame) = self.glob_restore_frames.last_mut() else {
2053            return Err(PerlError::runtime(
2054                "internal: no glob restore frame for local *GLOB",
2055                line,
2056            ));
2057        };
2058        frame.push((lhs.to_string(), old));
2059        if let Some(r) = rhs {
2060            self.glob_handle_alias
2061                .insert(lhs.to_string(), r.to_string());
2062        }
2063        Ok(())
2064    }
2065
2066    pub(crate) fn scope_push_hook(&mut self) {
2067        self.scope.push_frame();
2068        self.glob_restore_frames.push(Vec::new());
2069        self.special_var_restore_frames.push(Vec::new());
2070        self.english_lexical_scalars.push(HashSet::new());
2071        self.our_lexical_scalars.push(HashSet::new());
2072        self.state_bindings_stack.push(Vec::new());
2073    }
2074
2075    #[inline]
2076    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2077        if let Some(s) = self.english_lexical_scalars.last_mut() {
2078            s.insert(name.to_string());
2079        }
2080    }
2081
2082    #[inline]
2083    fn note_our_scalar(&mut self, bare_name: &str) {
2084        if let Some(s) = self.our_lexical_scalars.last_mut() {
2085            s.insert(bare_name.to_string());
2086        }
2087    }
2088
2089    pub(crate) fn scope_pop_hook(&mut self) {
2090        if !self.scope.can_pop_frame() {
2091            return;
2092        }
2093        // Execute deferred blocks in LIFO order before popping the frame.
2094        // Important: defer blocks run in the CURRENT scope (not a new frame),
2095        // so they can modify variables in the enclosing scope.
2096        let defers = self.scope.take_defers();
2097        for coderef in defers {
2098            if let Some(sub) = coderef.as_code_ref() {
2099                // Execute the defer block body directly in the current scope,
2100                // without creating a new frame or restoring closure captures.
2101                // This allows defer { $x = 100 } to modify the outer $x.
2102                let saved_wa = self.wantarray_kind;
2103                self.wantarray_kind = WantarrayCtx::Void;
2104                let _ = self.exec_block_no_scope(&sub.body);
2105                self.wantarray_kind = saved_wa;
2106            }
2107        }
2108        // Save state variable values back before popping the frame
2109        if let Some(bindings) = self.state_bindings_stack.pop() {
2110            for (var_name, state_key) in &bindings {
2111                let val = self.scope.get_scalar(var_name).clone();
2112                self.state_vars.insert(state_key.clone(), val);
2113            }
2114        }
2115        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2116        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2117        if let Some(entries) = self.special_var_restore_frames.pop() {
2118            for (name, old) in entries.into_iter().rev() {
2119                let _ = self.set_special_var(&name, &old);
2120            }
2121        }
2122        if let Some(entries) = self.glob_restore_frames.pop() {
2123            for (name, old) in entries.into_iter().rev() {
2124                match old {
2125                    Some(s) => {
2126                        self.glob_handle_alias.insert(name, s);
2127                    }
2128                    None => {
2129                        self.glob_handle_alias.remove(&name);
2130                    }
2131                }
2132            }
2133        }
2134        self.scope.pop_frame();
2135        let _ = self.english_lexical_scalars.pop();
2136        let _ = self.our_lexical_scalars.pop();
2137    }
2138
2139    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2140    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2141    #[inline]
2142    pub(crate) fn enable_parallel_guard(&mut self) {
2143        self.scope.set_parallel_guard(true);
2144    }
2145
2146    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues so a later tree-walker
2147    /// run does not execute them again.
2148    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2149        self.begin_blocks.clear();
2150        self.unit_check_blocks.clear();
2151        self.check_blocks.clear();
2152        self.init_blocks.clear();
2153        self.end_blocks.clear();
2154    }
2155
2156    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2157    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2158    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2159    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2160    /// there because it only calls [`Scope::pop_frame`].
2161    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2162        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2163            self.scope_pop_hook();
2164        }
2165    }
2166
2167    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2168    ///
2169    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2170    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2171    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2172    /// workers that call `perl_signal::poll`).
2173    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> PerlResult<()> {
2174        self.touch_env_hash("SIG");
2175        let v = self.scope.get_hash_element("SIG", sig);
2176        if v.is_undef() {
2177            return Self::default_sig_action(sig);
2178        }
2179        if let Some(s) = v.as_str() {
2180            if s == "IGNORE" {
2181                return Ok(());
2182            }
2183            if s == "DEFAULT" {
2184                return Self::default_sig_action(sig);
2185            }
2186        }
2187        if let Some(sub) = v.as_code_ref() {
2188            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2189                Ok(_) => Ok(()),
2190                Err(FlowOrError::Flow(_)) => Ok(()),
2191                Err(FlowOrError::Error(e)) => Err(e),
2192            }
2193        } else {
2194            Self::default_sig_action(sig)
2195        }
2196    }
2197
2198    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2199    #[inline]
2200    fn default_sig_action(sig: &str) -> PerlResult<()> {
2201        match sig {
2202            // 128 + signal number (common shell convention)
2203            "INT" => std::process::exit(130),
2204            "TERM" => std::process::exit(143),
2205            "ALRM" => std::process::exit(142),
2206            // Default for SIGCHLD is ignore
2207            "CHLD" => Ok(()),
2208            _ => Ok(()),
2209        }
2210    }
2211
2212    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2213    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2214    pub fn materialize_env_if_needed(&mut self) {
2215        if self.env_materialized {
2216            return;
2217        }
2218        self.env = std::env::vars()
2219            .map(|(k, v)| (k, PerlValue::string(v)))
2220            .collect();
2221        self.scope
2222            .set_hash("ENV", self.env.clone())
2223            .expect("set %ENV");
2224        self.env_materialized = true;
2225    }
2226
2227    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2228    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2229        self.materialize_env_if_needed();
2230        if let Some(x) = self.log_level_override {
2231            return x;
2232        }
2233        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2234        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2235    }
2236
2237    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2238    pub(crate) fn no_color_effective(&mut self) -> bool {
2239        self.materialize_env_if_needed();
2240        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2241        if v.is_undef() {
2242            return false;
2243        }
2244        !v.to_string().is_empty()
2245    }
2246
2247    #[inline]
2248    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2249        if hash_name == "ENV" {
2250            self.materialize_env_if_needed();
2251        }
2252    }
2253
2254    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2255    pub(crate) fn exists_arrow_hash_element(
2256        &self,
2257        container: PerlValue,
2258        key: &str,
2259        line: usize,
2260    ) -> PerlResult<bool> {
2261        if let Some(r) = container.as_hash_ref() {
2262            return Ok(r.read().contains_key(key));
2263        }
2264        if let Some(b) = container.as_blessed_ref() {
2265            let data = b.data.read();
2266            if let Some(r) = data.as_hash_ref() {
2267                return Ok(r.read().contains_key(key));
2268            }
2269            if let Some(hm) = data.as_hash_map() {
2270                return Ok(hm.contains_key(key));
2271            }
2272            return Err(PerlError::runtime(
2273                "exists argument is not a HASH reference",
2274                line,
2275            ));
2276        }
2277        Err(PerlError::runtime(
2278            "exists argument is not a HASH reference",
2279            line,
2280        ))
2281    }
2282
2283    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
2284    pub(crate) fn delete_arrow_hash_element(
2285        &self,
2286        container: PerlValue,
2287        key: &str,
2288        line: usize,
2289    ) -> PerlResult<PerlValue> {
2290        if let Some(r) = container.as_hash_ref() {
2291            return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2292        }
2293        if let Some(b) = container.as_blessed_ref() {
2294            let mut data = b.data.write();
2295            if let Some(r) = data.as_hash_ref() {
2296                return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2297            }
2298            if let Some(mut map) = data.as_hash_map() {
2299                let v = map.shift_remove(key).unwrap_or(PerlValue::UNDEF);
2300                *data = PerlValue::hash(map);
2301                return Ok(v);
2302            }
2303            return Err(PerlError::runtime(
2304                "delete argument is not a HASH reference",
2305                line,
2306            ));
2307        }
2308        Err(PerlError::runtime(
2309            "delete argument is not a HASH reference",
2310            line,
2311        ))
2312    }
2313
2314    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
2315    pub(crate) fn exists_arrow_array_element(
2316        &self,
2317        container: PerlValue,
2318        idx: i64,
2319        line: usize,
2320    ) -> PerlResult<bool> {
2321        if let Some(a) = container.as_array_ref() {
2322            let arr = a.read();
2323            let i = if idx < 0 {
2324                (arr.len() as i64 + idx) as usize
2325            } else {
2326                idx as usize
2327            };
2328            return Ok(i < arr.len());
2329        }
2330        Err(PerlError::runtime(
2331            "exists argument is not an ARRAY reference",
2332            line,
2333        ))
2334    }
2335
2336    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
2337    pub(crate) fn delete_arrow_array_element(
2338        &self,
2339        container: PerlValue,
2340        idx: i64,
2341        line: usize,
2342    ) -> PerlResult<PerlValue> {
2343        if let Some(a) = container.as_array_ref() {
2344            let mut arr = a.write();
2345            let i = if idx < 0 {
2346                (arr.len() as i64 + idx) as usize
2347            } else {
2348                idx as usize
2349            };
2350            if i >= arr.len() {
2351                return Ok(PerlValue::UNDEF);
2352            }
2353            let old = arr.get(i).cloned().unwrap_or(PerlValue::UNDEF);
2354            arr[i] = PerlValue::UNDEF;
2355            return Ok(old);
2356        }
2357        Err(PerlError::runtime(
2358            "delete argument is not an ARRAY reference",
2359            line,
2360        ))
2361    }
2362
2363    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
2364    pub(crate) fn inc_directories(&self) -> Vec<String> {
2365        let mut v: Vec<String> = self
2366            .scope
2367            .get_array("INC")
2368            .into_iter()
2369            .map(|x| x.to_string())
2370            .filter(|s| !s.is_empty())
2371            .collect();
2372        if v.is_empty() {
2373            v.push(".".to_string());
2374        }
2375        v
2376    }
2377
2378    #[inline]
2379    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
2380        matches!(
2381            name,
2382            "_" | "0"
2383                | "!"
2384                | "@"
2385                | "/"
2386                | "\\"
2387                | ","
2388                | "."
2389                | "__PACKAGE__"
2390                | "$$"
2391                | "|"
2392                | "?"
2393                | "\""
2394                | "&"
2395                | "`"
2396                | "'"
2397                | "+"
2398                | "<"
2399                | ">"
2400                | "("
2401                | ")"
2402                | "]"
2403                | ";"
2404                | "ARGV"
2405                | "%"
2406                | "="
2407                | "-"
2408                | ":"
2409                | "*"
2410                | "INC"
2411        ) || name.chars().all(|c| c.is_ascii_digit())
2412            || name.starts_with('^')
2413            || (name.starts_with('#') && name.len() > 1)
2414    }
2415
2416    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2417        if !self.strict_vars
2418            || Self::strict_scalar_exempt(name)
2419            || name.contains("::")
2420            || self.scope.scalar_binding_exists(name)
2421        {
2422            return Ok(());
2423        }
2424        Err(PerlError::runtime(
2425            format!(
2426                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
2427                name, name
2428            ),
2429            line,
2430        )
2431        .into())
2432    }
2433
2434    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2435        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
2436            return Ok(());
2437        }
2438        Err(PerlError::runtime(
2439            format!(
2440                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
2441                name, name
2442            ),
2443            line,
2444        )
2445        .into())
2446    }
2447
2448    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2449        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
2450        if !self.strict_vars
2451            || name.contains("::")
2452            || self.scope.hash_binding_exists(name)
2453            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
2454        {
2455            return Ok(());
2456        }
2457        Err(PerlError::runtime(
2458            format!(
2459                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
2460                name, name
2461            ),
2462            line,
2463        )
2464        .into())
2465    }
2466
2467    fn looks_like_version_only(spec: &str) -> bool {
2468        let t = spec.trim();
2469        !t.is_empty()
2470            && !t.contains('/')
2471            && !t.contains('\\')
2472            && !t.contains("::")
2473            && t.chars()
2474                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
2475            && t.chars().any(|c| c.is_ascii_digit())
2476    }
2477
2478    fn module_spec_to_relpath(spec: &str) -> String {
2479        let t = spec.trim();
2480        if t.contains("::") {
2481            format!("{}.pm", t.replace("::", "/"))
2482        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
2483            t.replace('\\', "/")
2484        } else {
2485            format!("{}.pm", t)
2486        }
2487    }
2488
2489    /// `sub name` in `package P` → stash key `P::name` (otherwise `name` in `main`).
2490    /// `sub Q::name { }` is already fully qualified — do not prepend the current package.
2491    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
2492        if name.contains("::") {
2493            return name.to_string();
2494        }
2495        let pkg = self.current_package();
2496        if pkg.is_empty() || pkg == "main" {
2497            name.to_string()
2498        } else {
2499            format!("{}::{}", pkg, name)
2500        }
2501    }
2502
2503    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
2504    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
2505        let mut msg = format!("Undefined subroutine &{}", name);
2506        if self.strict_subs {
2507            msg.push_str(
2508                " (strict subs: declare the sub or use a fully qualified name before calling)",
2509            );
2510        }
2511        msg
2512    }
2513
2514    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
2515    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
2516        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
2517        if self.strict_subs {
2518            msg.push_str(
2519                " (strict subs: declare the sub or use a fully qualified name before calling)",
2520            );
2521        }
2522        msg
2523    }
2524
2525    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
2526    fn import_alias_key(&self, short: &str) -> String {
2527        self.qualify_sub_key(short)
2528    }
2529
2530    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
2531    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
2532        if imports.len() == 1 {
2533            match &imports[0].kind {
2534                ExprKind::QW(ws) => return ws.is_empty(),
2535                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
2536                ExprKind::List(xs) => return xs.is_empty(),
2537                _ => {}
2538            }
2539        }
2540        false
2541    }
2542
2543    /// After `require`, copy `Module::export` → caller stash per `use` list.
2544    fn apply_module_import(
2545        &mut self,
2546        module: &str,
2547        imports: &[Expr],
2548        line: usize,
2549    ) -> PerlResult<()> {
2550        if imports.is_empty() {
2551            return self.import_all_from_module(module, line);
2552        }
2553        if Self::is_explicit_empty_import_list(imports) {
2554            return Ok(());
2555        }
2556        let names = Self::pragma_import_strings(imports, line)?;
2557        if names.is_empty() {
2558            return Ok(());
2559        }
2560        for name in names {
2561            self.import_one_symbol(module, &name, line)?;
2562        }
2563        Ok(())
2564    }
2565
2566    fn import_all_from_module(&mut self, module: &str, line: usize) -> PerlResult<()> {
2567        if module == "List::Util" {
2568            crate::list_util::ensure_list_util(self);
2569        }
2570        if let Some(lists) = self.module_export_lists.get(module) {
2571            let export: Vec<String> = lists.export.clone();
2572            for short in export {
2573                self.import_named_sub(module, &short, line)?;
2574            }
2575            return Ok(());
2576        }
2577        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
2578        let prefix = format!("{}::", module);
2579        let keys: Vec<String> = self
2580            .subs
2581            .keys()
2582            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
2583            .cloned()
2584            .collect();
2585        for k in keys {
2586            let short = k[prefix.len()..].to_string();
2587            if let Some(sub) = self.subs.get(&k).cloned() {
2588                let alias = self.import_alias_key(&short);
2589                self.subs.insert(alias, sub);
2590            }
2591        }
2592        Ok(())
2593    }
2594
2595    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
2596    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> PerlResult<()> {
2597        if module == "List::Util" {
2598            crate::list_util::ensure_list_util(self);
2599        }
2600        let qual = format!("{}::{}", module, short);
2601        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
2602            PerlError::runtime(
2603                format!(
2604                    "`{}` is not defined in module `{}` (expected `{}`)",
2605                    short, module, qual
2606                ),
2607                line,
2608            )
2609        })?;
2610        let alias = self.import_alias_key(short);
2611        self.subs.insert(alias, sub);
2612        Ok(())
2613    }
2614
2615    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> PerlResult<()> {
2616        if let Some(lists) = self.module_export_lists.get(module) {
2617            let allowed: HashSet<&str> = lists
2618                .export
2619                .iter()
2620                .map(|s| s.as_str())
2621                .chain(lists.export_ok.iter().map(|s| s.as_str()))
2622                .collect();
2623            if !allowed.contains(export) {
2624                return Err(PerlError::runtime(
2625                    format!(
2626                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
2627                        export, module
2628                    ),
2629                    line,
2630                ));
2631            }
2632        }
2633        self.import_named_sub(module, export, line)
2634    }
2635
2636    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
2637    fn record_exporter_our_array_name(&mut self, name: &str, items: &[PerlValue]) {
2638        if name != "EXPORT" && name != "EXPORT_OK" {
2639            return;
2640        }
2641        let pkg = self.current_package();
2642        if pkg.is_empty() || pkg == "main" {
2643            return;
2644        }
2645        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
2646        let ent = self.module_export_lists.entry(pkg).or_default();
2647        if name == "EXPORT" {
2648            ent.export = names;
2649        } else {
2650            ent.export_ok = names;
2651        }
2652    }
2653
2654    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
2655    /// Refresh [`PerlSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
2656    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
2657    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
2658        let key = self.qualify_sub_key(name);
2659        let Some(sub) = self.subs.get(&key).cloned() else {
2660            return;
2661        };
2662        let captured = self.scope.capture();
2663        let closure_env = if captured.is_empty() {
2664            None
2665        } else {
2666            Some(captured)
2667        };
2668        let mut new_sub = (*sub).clone();
2669        new_sub.closure_env = closure_env;
2670        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
2671        self.subs.insert(key, Arc::new(new_sub));
2672    }
2673
2674    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<PerlSub>> {
2675        if let Some(s) = self.subs.get(name) {
2676            return Some(s.clone());
2677        }
2678        if !name.contains("::") {
2679            let pkg = self.current_package();
2680            if !pkg.is_empty() && pkg != "main" {
2681                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
2682                q.push_str(&pkg);
2683                q.push_str("::");
2684                q.push_str(name);
2685                return self.subs.get(&q).cloned();
2686            }
2687        }
2688        None
2689    }
2690
2691    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
2692    /// before calling `import`).
2693    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
2694        if let Some(first) = imports.first() {
2695            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
2696                return &imports[1..];
2697            }
2698        }
2699        imports
2700    }
2701
2702    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
2703    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> PerlResult<Vec<String>> {
2704        let mut out = Vec::new();
2705        for e in imports {
2706            match &e.kind {
2707                ExprKind::String(s) => out.push(s.clone()),
2708                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
2709                ExprKind::Integer(n) => out.push(n.to_string()),
2710                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
2711                // a single interpolated variable.  Reconstruct the sigil+name form.
2712                ExprKind::InterpolatedString(parts) => {
2713                    let mut s = String::new();
2714                    for p in parts {
2715                        match p {
2716                            StringPart::Literal(l) => s.push_str(l),
2717                            StringPart::ScalarVar(v) => {
2718                                s.push('$');
2719                                s.push_str(v);
2720                            }
2721                            StringPart::ArrayVar(v) => {
2722                                s.push('@');
2723                                s.push_str(v);
2724                            }
2725                            _ => {
2726                                return Err(PerlError::runtime(
2727                                    "pragma import must be a compile-time string, qw(), or integer",
2728                                    e.line.max(default_line),
2729                                ));
2730                            }
2731                        }
2732                    }
2733                    out.push(s);
2734                }
2735                _ => {
2736                    return Err(PerlError::runtime(
2737                        "pragma import must be a compile-time string, qw(), or integer",
2738                        e.line.max(default_line),
2739                    ));
2740                }
2741            }
2742        }
2743        Ok(out)
2744    }
2745
2746    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2747        if imports.is_empty() {
2748            self.strict_refs = true;
2749            self.strict_subs = true;
2750            self.strict_vars = true;
2751            return Ok(());
2752        }
2753        let names = Self::pragma_import_strings(imports, line)?;
2754        for name in names {
2755            match name.as_str() {
2756                "refs" => self.strict_refs = true,
2757                "subs" => self.strict_subs = true,
2758                "vars" => self.strict_vars = true,
2759                _ => {
2760                    return Err(PerlError::runtime(
2761                        format!("Unknown strict mode `{}`", name),
2762                        line,
2763                    ));
2764                }
2765            }
2766        }
2767        Ok(())
2768    }
2769
2770    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2771        if imports.is_empty() {
2772            self.strict_refs = false;
2773            self.strict_subs = false;
2774            self.strict_vars = false;
2775            return Ok(());
2776        }
2777        let names = Self::pragma_import_strings(imports, line)?;
2778        for name in names {
2779            match name.as_str() {
2780                "refs" => self.strict_refs = false,
2781                "subs" => self.strict_subs = false,
2782                "vars" => self.strict_vars = false,
2783                _ => {
2784                    return Err(PerlError::runtime(
2785                        format!("Unknown strict mode `{}`", name),
2786                        line,
2787                    ));
2788                }
2789            }
2790        }
2791        Ok(())
2792    }
2793
2794    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2795        let items = Self::pragma_import_strings(imports, line)?;
2796        if items.is_empty() {
2797            return Err(PerlError::runtime(
2798                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
2799                line,
2800            ));
2801        }
2802        for item in items {
2803            let s = item.trim();
2804            if let Some(rest) = s.strip_prefix(':') {
2805                self.apply_feature_bundle(rest, line)?;
2806            } else {
2807                self.apply_feature_name(s, true, line)?;
2808            }
2809        }
2810        Ok(())
2811    }
2812
2813    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2814        if imports.is_empty() {
2815            self.feature_bits = 0;
2816            return Ok(());
2817        }
2818        let items = Self::pragma_import_strings(imports, line)?;
2819        for item in items {
2820            let s = item.trim();
2821            if let Some(rest) = s.strip_prefix(':') {
2822                self.clear_feature_bundle(rest);
2823            } else {
2824                self.apply_feature_name(s, false, line)?;
2825            }
2826        }
2827        Ok(())
2828    }
2829
2830    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> PerlResult<()> {
2831        let key = v.trim();
2832        match key {
2833            "5.10" | "5.010" | "5.10.0" => {
2834                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
2835            }
2836            "5.12" | "5.012" | "5.12.0" => {
2837                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
2838            }
2839            _ => {
2840                return Err(PerlError::runtime(
2841                    format!("unsupported feature bundle :{}", key),
2842                    line,
2843                ));
2844            }
2845        }
2846        Ok(())
2847    }
2848
2849    fn clear_feature_bundle(&mut self, v: &str) {
2850        let key = v.trim();
2851        if matches!(
2852            key,
2853            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
2854        ) {
2855            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
2856        }
2857    }
2858
2859    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> PerlResult<()> {
2860        let bit = match name {
2861            "say" => FEAT_SAY,
2862            "state" => FEAT_STATE,
2863            "switch" => FEAT_SWITCH,
2864            "unicode_strings" => FEAT_UNICODE_STRINGS,
2865            // Features that stryke accepts as known but tracks no separate bit for —
2866            // either always-on, always-off, or syntactic sugar already enabled.
2867            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
2868            "postderef"
2869            | "postderef_qq"
2870            | "evalbytes"
2871            | "current_sub"
2872            | "fc"
2873            | "lexical_subs"
2874            | "signatures"
2875            | "refaliasing"
2876            | "bitwise"
2877            | "isa"
2878            | "indirect"
2879            | "multidimensional"
2880            | "bareword_filehandles"
2881            | "try"
2882            | "defer"
2883            | "extra_paired_delimiters"
2884            | "module_true"
2885            | "class"
2886            | "array_base" => return Ok(()),
2887            _ => {
2888                return Err(PerlError::runtime(
2889                    format!("unknown feature `{}`", name),
2890                    line,
2891                ));
2892            }
2893        };
2894        if enable {
2895            self.feature_bits |= bit;
2896        } else {
2897            self.feature_bits &= !bit;
2898        }
2899        Ok(())
2900    }
2901
2902    /// `require EXPR` — load once, record `%INC`, return `1` on success.
2903    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> PerlResult<PerlValue> {
2904        let t = spec.trim();
2905        if t.is_empty() {
2906            return Err(PerlError::runtime("require: empty argument", line));
2907        }
2908        match t {
2909            "strict" => {
2910                self.apply_use_strict(&[], line)?;
2911                return Ok(PerlValue::integer(1));
2912            }
2913            "utf8" => {
2914                self.utf8_pragma = true;
2915                return Ok(PerlValue::integer(1));
2916            }
2917            "feature" | "v5" => {
2918                return Ok(PerlValue::integer(1));
2919            }
2920            "warnings" => {
2921                self.warnings = true;
2922                return Ok(PerlValue::integer(1));
2923            }
2924            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
2925                return Ok(PerlValue::integer(1));
2926            }
2927            _ => {}
2928        }
2929        let p = Path::new(t);
2930        if p.is_absolute() {
2931            return self.require_absolute_path(p, line);
2932        }
2933        if Self::looks_like_version_only(t) {
2934            return Ok(PerlValue::integer(1));
2935        }
2936        let relpath = Self::module_spec_to_relpath(t);
2937        self.require_from_inc(&relpath, line)
2938    }
2939
2940    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
2941    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> PerlResult<()> {
2942        let v = self.scope.get_hash_element("^HOOK", key);
2943        if v.is_undef() {
2944            return Ok(());
2945        }
2946        let Some(sub) = v.as_code_ref() else {
2947            return Ok(());
2948        };
2949        let r = self.call_sub(
2950            sub.as_ref(),
2951            vec![PerlValue::string(path.to_string())],
2952            WantarrayCtx::Scalar,
2953            line,
2954        );
2955        match r {
2956            Ok(_) => Ok(()),
2957            Err(FlowOrError::Error(e)) => Err(e),
2958            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
2959            Err(FlowOrError::Flow(other)) => Err(PerlError::runtime(
2960                format!(
2961                    "require hook {:?} returned unexpected control flow: {:?}",
2962                    key, other
2963                ),
2964                line,
2965            )),
2966        }
2967    }
2968
2969    fn require_absolute_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
2970        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
2971        let key = canon.to_string_lossy().into_owned();
2972        if self.scope.exists_hash_element("INC", &key) {
2973            return Ok(PerlValue::integer(1));
2974        }
2975        self.invoke_require_hook("require__before", &key, line)?;
2976        let code = read_file_text_perl_compat(&canon).map_err(|e| {
2977            PerlError::runtime(
2978                format!("Can't open {} for reading: {}", canon.display(), e),
2979                line,
2980            )
2981        })?;
2982        let code = crate::data_section::strip_perl_end_marker(&code);
2983        self.scope
2984            .set_hash_element("INC", &key, PerlValue::string(key.clone()))?;
2985        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
2986        let r = crate::parse_and_run_string_in_file(code, self, &key);
2987        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
2988        r?;
2989        self.invoke_require_hook("require__after", &key, line)?;
2990        Ok(PerlValue::integer(1))
2991    }
2992
2993    fn require_from_inc(&mut self, relpath: &str, line: usize) -> PerlResult<PerlValue> {
2994        if self.scope.exists_hash_element("INC", relpath) {
2995            return Ok(PerlValue::integer(1));
2996        }
2997        self.invoke_require_hook("require__before", relpath, line)?;
2998        for dir in self.inc_directories() {
2999            let full = Path::new(&dir).join(relpath);
3000            if full.is_file() {
3001                let code = read_file_text_perl_compat(&full).map_err(|e| {
3002                    PerlError::runtime(
3003                        format!("Can't open {} for reading: {}", full.display(), e),
3004                        line,
3005                    )
3006                })?;
3007                let code = crate::data_section::strip_perl_end_marker(&code);
3008                let abs = full.canonicalize().unwrap_or(full);
3009                let abs_s = abs.to_string_lossy().into_owned();
3010                self.scope
3011                    .set_hash_element("INC", relpath, PerlValue::string(abs_s.clone()))?;
3012                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3013                let r = crate::parse_and_run_string_in_file(code, self, &abs_s);
3014                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3015                r?;
3016                self.invoke_require_hook("require__after", relpath, line)?;
3017                return Ok(PerlValue::integer(1));
3018            }
3019        }
3020        Err(PerlError::runtime(
3021            format!(
3022                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
3023                relpath
3024            ),
3025            line,
3026        ))
3027    }
3028
3029    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
3030    pub(crate) fn exec_use_stmt(
3031        &mut self,
3032        module: &str,
3033        imports: &[Expr],
3034        line: usize,
3035    ) -> PerlResult<()> {
3036        match module {
3037            "strict" => self.apply_use_strict(imports, line),
3038            "utf8" => {
3039                if !imports.is_empty() {
3040                    return Err(PerlError::runtime("use utf8 takes no arguments", line));
3041                }
3042                self.utf8_pragma = true;
3043                Ok(())
3044            }
3045            "feature" => self.apply_use_feature(imports, line),
3046            "v5" => Ok(()),
3047            "warnings" => {
3048                self.warnings = true;
3049                Ok(())
3050            }
3051            "English" => {
3052                self.english_enabled = true;
3053                let args = Self::pragma_import_strings(imports, line)?;
3054                let no_match = args.iter().any(|a| a == "-no_match_vars");
3055                // Once match vars are exported (use English without -no_match_vars),
3056                // they stay available for the rest of the program — Perl exports them
3057                // into the caller's namespace and later pragmas cannot un-export them.
3058                if !no_match {
3059                    self.english_match_vars_ever_enabled = true;
3060                }
3061                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
3062                Ok(())
3063            }
3064            "Env" => self.apply_use_env(imports, line),
3065            "open" => self.apply_use_open(imports, line),
3066            "constant" => self.apply_use_constant(imports, line),
3067            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3068            _ => {
3069                self.require_execute(module, line)?;
3070                let imports = Self::imports_after_leading_use_version(imports);
3071                self.apply_module_import(module, imports, line)?;
3072                Ok(())
3073            }
3074        }
3075    }
3076
3077    /// `no strict 'refs'`, `no warnings`, `no feature`, …
3078    pub(crate) fn exec_no_stmt(
3079        &mut self,
3080        module: &str,
3081        imports: &[Expr],
3082        line: usize,
3083    ) -> PerlResult<()> {
3084        match module {
3085            "strict" => self.apply_no_strict(imports, line),
3086            "utf8" => {
3087                if !imports.is_empty() {
3088                    return Err(PerlError::runtime("no utf8 takes no arguments", line));
3089                }
3090                self.utf8_pragma = false;
3091                Ok(())
3092            }
3093            "feature" => self.apply_no_feature(imports, line),
3094            "v5" => Ok(()),
3095            "warnings" => {
3096                self.warnings = false;
3097                Ok(())
3098            }
3099            "English" => {
3100                self.english_enabled = false;
3101                // Don't reset no_match_vars here — if match vars were ever enabled,
3102                // they persist (Perl's export cannot be un-exported).
3103                if !self.english_match_vars_ever_enabled {
3104                    self.english_no_match_vars = false;
3105                }
3106                Ok(())
3107            }
3108            "open" => {
3109                self.open_pragma_utf8 = false;
3110                Ok(())
3111            }
3112            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3113            _ => Ok(()),
3114        }
3115    }
3116
3117    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
3118    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3119        let names = Self::pragma_import_strings(imports, line)?;
3120        for n in names {
3121            let key = n.trim_start_matches('@');
3122            if key.eq_ignore_ascii_case("PATH") {
3123                let path_env = std::env::var("PATH").unwrap_or_default();
3124                let path_vec: Vec<PerlValue> = std::env::split_paths(&path_env)
3125                    .map(|p| PerlValue::string(p.to_string_lossy().into_owned()))
3126                    .collect();
3127                let aname = self.stash_array_name_for_package("PATH");
3128                self.scope.declare_array(&aname, path_vec);
3129            }
3130        }
3131        Ok(())
3132    }
3133
3134    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
3135    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3136        let items = Self::pragma_import_strings(imports, line)?;
3137        for item in items {
3138            let s = item.trim();
3139            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
3140                self.open_pragma_utf8 = true;
3141                continue;
3142            }
3143            if let Some(rest) = s.strip_prefix(":encoding(") {
3144                if let Some(inner) = rest.strip_suffix(')') {
3145                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
3146                        self.open_pragma_utf8 = true;
3147                    }
3148                }
3149            }
3150        }
3151        Ok(())
3152    }
3153
3154    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
3155    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3156        if imports.is_empty() {
3157            return Ok(());
3158        }
3159        // `use constant 1.03;` — version check only (ignored here).
3160        if imports.len() == 1 {
3161            match &imports[0].kind {
3162                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
3163                _ => {}
3164            }
3165        }
3166        for imp in imports {
3167            match &imp.kind {
3168                ExprKind::List(items) => {
3169                    if items.len() % 2 != 0 {
3170                        return Err(PerlError::runtime(
3171                            format!(
3172                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
3173                                items.len()
3174                            ),
3175                            line,
3176                        ));
3177                    }
3178                    let mut i = 0;
3179                    while i < items.len() {
3180                        let name = match &items[i].kind {
3181                            ExprKind::String(s) => s.clone(),
3182                            _ => {
3183                                return Err(PerlError::runtime(
3184                                    "use constant: constant name must be a string literal",
3185                                    line,
3186                                ));
3187                            }
3188                        };
3189                        let val = match self.eval_expr(&items[i + 1]) {
3190                            Ok(v) => v,
3191                            Err(FlowOrError::Error(e)) => return Err(e),
3192                            Err(FlowOrError::Flow(_)) => {
3193                                return Err(PerlError::runtime(
3194                                    "use constant: unexpected control flow in initializer",
3195                                    line,
3196                                ));
3197                            }
3198                        };
3199                        self.install_constant_sub(&name, &val, line)?;
3200                        i += 2;
3201                    }
3202                }
3203                _ => {
3204                    return Err(PerlError::runtime(
3205                        "use constant: expected list of NAME => VALUE pairs",
3206                        line,
3207                    ));
3208                }
3209            }
3210        }
3211        Ok(())
3212    }
3213
3214    fn install_constant_sub(&mut self, name: &str, val: &PerlValue, line: usize) -> PerlResult<()> {
3215        let key = self.qualify_sub_key(name);
3216        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
3217        let body = vec![Statement {
3218            label: None,
3219            kind: StmtKind::Return(Some(ret_expr)),
3220            line,
3221        }];
3222        self.subs.insert(
3223            key.clone(),
3224            Arc::new(PerlSub {
3225                name: key,
3226                params: vec![],
3227                body,
3228                prototype: None,
3229                closure_env: None,
3230                fib_like: None,
3231            }),
3232        );
3233        Ok(())
3234    }
3235
3236    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
3237    fn perl_value_to_const_literal_expr(&self, v: &PerlValue, line: usize) -> PerlResult<Expr> {
3238        if v.is_undef() {
3239            return Ok(Expr {
3240                kind: ExprKind::Undef,
3241                line,
3242            });
3243        }
3244        if let Some(n) = v.as_integer() {
3245            return Ok(Expr {
3246                kind: ExprKind::Integer(n),
3247                line,
3248            });
3249        }
3250        if let Some(f) = v.as_float() {
3251            return Ok(Expr {
3252                kind: ExprKind::Float(f),
3253                line,
3254            });
3255        }
3256        if let Some(s) = v.as_str() {
3257            return Ok(Expr {
3258                kind: ExprKind::String(s),
3259                line,
3260            });
3261        }
3262        if let Some(arr) = v.as_array_vec() {
3263            let mut elems = Vec::with_capacity(arr.len());
3264            for e in &arr {
3265                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3266            }
3267            return Ok(Expr {
3268                kind: ExprKind::ArrayRef(elems),
3269                line,
3270            });
3271        }
3272        if let Some(h) = v.as_hash_map() {
3273            let mut pairs = Vec::with_capacity(h.len());
3274            for (k, vv) in h.iter() {
3275                pairs.push((
3276                    Expr {
3277                        kind: ExprKind::String(k.clone()),
3278                        line,
3279                    },
3280                    self.perl_value_to_const_literal_expr(vv, line)?,
3281                ));
3282            }
3283            return Ok(Expr {
3284                kind: ExprKind::HashRef(pairs),
3285                line,
3286            });
3287        }
3288        if let Some(aref) = v.as_array_ref() {
3289            let arr = aref.read();
3290            let mut elems = Vec::with_capacity(arr.len());
3291            for e in arr.iter() {
3292                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3293            }
3294            return Ok(Expr {
3295                kind: ExprKind::ArrayRef(elems),
3296                line,
3297            });
3298        }
3299        if let Some(href) = v.as_hash_ref() {
3300            let h = href.read();
3301            let mut pairs = Vec::with_capacity(h.len());
3302            for (k, vv) in h.iter() {
3303                pairs.push((
3304                    Expr {
3305                        kind: ExprKind::String(k.clone()),
3306                        line,
3307                    },
3308                    self.perl_value_to_const_literal_expr(vv, line)?,
3309                ));
3310            }
3311            return Ok(Expr {
3312                kind: ExprKind::HashRef(pairs),
3313                line,
3314            });
3315        }
3316        Err(PerlError::runtime(
3317            format!("use constant: unsupported value type ({v:?})"),
3318            line,
3319        ))
3320    }
3321
3322    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
3323    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> PerlResult<()> {
3324        if crate::list_util::program_needs_list_util(program) {
3325            crate::list_util::ensure_list_util(self);
3326        }
3327        for stmt in &program.statements {
3328            match &stmt.kind {
3329                StmtKind::Package { name } => {
3330                    let _ = self
3331                        .scope
3332                        .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
3333                }
3334                StmtKind::SubDecl {
3335                    name,
3336                    params,
3337                    body,
3338                    prototype,
3339                } => {
3340                    let key = self.qualify_sub_key(name);
3341                    let mut sub = PerlSub {
3342                        name: name.clone(),
3343                        params: params.clone(),
3344                        body: body.clone(),
3345                        closure_env: None,
3346                        prototype: prototype.clone(),
3347                        fib_like: None,
3348                    };
3349                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
3350                    self.subs.insert(key, Arc::new(sub));
3351                }
3352                StmtKind::UsePerlVersion { .. } => {}
3353                StmtKind::Use { module, imports } => {
3354                    self.exec_use_stmt(module, imports, stmt.line)?;
3355                }
3356                StmtKind::UseOverload { pairs } => {
3357                    self.install_use_overload_pairs(pairs);
3358                }
3359                StmtKind::FormatDecl { name, lines } => {
3360                    self.install_format_decl(name, lines, stmt.line)?;
3361                }
3362                StmtKind::No { module, imports } => {
3363                    self.exec_no_stmt(module, imports, stmt.line)?;
3364                }
3365                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
3366                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
3367                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
3368                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
3369                StmtKind::End(block) => self.end_blocks.push(block.clone()),
3370                _ => {}
3371            }
3372        }
3373        Ok(())
3374    }
3375
3376    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
3377    pub fn install_data_handle(&mut self, data: Vec<u8>) {
3378        self.input_handles.insert(
3379            "DATA".to_string(),
3380            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
3381        );
3382    }
3383
3384    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
3385    ///
3386    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
3387    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
3388    /// [`piped_shell_command`]).
3389    pub(crate) fn open_builtin_execute(
3390        &mut self,
3391        handle_name: String,
3392        mode_s: String,
3393        file_opt: Option<String>,
3394        line: usize,
3395    ) -> PerlResult<PerlValue> {
3396        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
3397        // - leading `|`  → pipe to command (write to child's stdin)
3398        // - trailing `|` → pipe from command (read child's stdout)
3399        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
3400        let (actual_mode, path) = if let Some(f) = file_opt {
3401            (mode_s, f)
3402        } else {
3403            let trimmed = mode_s.trim();
3404            if let Some(rest) = trimmed.strip_prefix('|') {
3405                ("|-".to_string(), rest.trim_start().to_string())
3406            } else if trimmed.ends_with('|') {
3407                let mut cmd = trimmed.to_string();
3408                cmd.pop(); // trailing `|` that selects pipe-from-command
3409                ("-|".to_string(), cmd.trim_end().to_string())
3410            } else if let Some(rest) = trimmed.strip_prefix(">>") {
3411                (">>".to_string(), rest.trim().to_string())
3412            } else if let Some(rest) = trimmed.strip_prefix('>') {
3413                (">".to_string(), rest.trim().to_string())
3414            } else if let Some(rest) = trimmed.strip_prefix('<') {
3415                ("<".to_string(), rest.trim().to_string())
3416            } else {
3417                ("<".to_string(), trimmed.to_string())
3418            }
3419        };
3420        let handle_return = handle_name.clone();
3421        match actual_mode.as_str() {
3422            "-|" => {
3423                let mut cmd = piped_shell_command(&path);
3424                cmd.stdout(Stdio::piped());
3425                let mut child = cmd.spawn().map_err(|e| {
3426                    self.apply_io_error_to_errno(&e);
3427                    PerlError::runtime(format!("Can't open pipe from command: {}", e), line)
3428                })?;
3429                let stdout = child
3430                    .stdout
3431                    .take()
3432                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdout", line))?;
3433                self.input_handles
3434                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
3435                self.pipe_children.insert(handle_name, child);
3436            }
3437            "|-" => {
3438                let mut cmd = piped_shell_command(&path);
3439                cmd.stdin(Stdio::piped());
3440                let mut child = cmd.spawn().map_err(|e| {
3441                    self.apply_io_error_to_errno(&e);
3442                    PerlError::runtime(format!("Can't open pipe to command: {}", e), line)
3443                })?;
3444                let stdin = child
3445                    .stdin
3446                    .take()
3447                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdin", line))?;
3448                self.output_handles
3449                    .insert(handle_name.clone(), Box::new(stdin));
3450                self.pipe_children.insert(handle_name, child);
3451            }
3452            "<" => {
3453                let file = std::fs::File::open(&path).map_err(|e| {
3454                    self.apply_io_error_to_errno(&e);
3455                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3456                })?;
3457                let shared = Arc::new(Mutex::new(file));
3458                self.io_file_slots
3459                    .insert(handle_name.clone(), Arc::clone(&shared));
3460                self.input_handles.insert(
3461                    handle_name.clone(),
3462                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
3463                );
3464            }
3465            ">" => {
3466                let file = std::fs::File::create(&path).map_err(|e| {
3467                    self.apply_io_error_to_errno(&e);
3468                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3469                })?;
3470                let shared = Arc::new(Mutex::new(file));
3471                self.io_file_slots
3472                    .insert(handle_name.clone(), Arc::clone(&shared));
3473                self.output_handles.insert(
3474                    handle_name.clone(),
3475                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3476                );
3477            }
3478            ">>" => {
3479                let file = std::fs::OpenOptions::new()
3480                    .append(true)
3481                    .create(true)
3482                    .open(&path)
3483                    .map_err(|e| {
3484                        self.apply_io_error_to_errno(&e);
3485                        PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3486                    })?;
3487                let shared = Arc::new(Mutex::new(file));
3488                self.io_file_slots
3489                    .insert(handle_name.clone(), Arc::clone(&shared));
3490                self.output_handles.insert(
3491                    handle_name.clone(),
3492                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3493                );
3494            }
3495            _ => {
3496                return Err(PerlError::runtime(
3497                    format!("Unknown open mode '{}'", actual_mode),
3498                    line,
3499                ));
3500            }
3501        }
3502        Ok(PerlValue::io_handle(handle_return))
3503    }
3504
3505    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
3506    /// matches the previous key under [`PerlValue::str_eq`]. Returns a list of arrayrefs
3507    /// (same outer shape as `chunked`).
3508    pub(crate) fn eval_chunk_by_builtin(
3509        &mut self,
3510        key_spec: &Expr,
3511        list_expr: &Expr,
3512        ctx: WantarrayCtx,
3513        line: usize,
3514    ) -> ExecResult {
3515        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
3516        let chunks = match &key_spec.kind {
3517            ExprKind::CodeRef { .. } => {
3518                let cr = self.eval_expr(key_spec)?;
3519                let Some(sub) = cr.as_code_ref() else {
3520                    return Err(PerlError::runtime(
3521                        "group_by/chunk_by: first argument must be { BLOCK }",
3522                        line,
3523                    )
3524                    .into());
3525                };
3526                let sub = sub.clone();
3527                let mut chunks: Vec<PerlValue> = Vec::new();
3528                let mut run: Vec<PerlValue> = Vec::new();
3529                let mut prev_key: Option<PerlValue> = None;
3530                for item in list {
3531                    self.scope.set_topic(item.clone());
3532                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
3533                        Ok(k) => k,
3534                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
3535                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
3536                        Err(_) => PerlValue::UNDEF,
3537                    };
3538                    match &prev_key {
3539                        None => {
3540                            run.push(item);
3541                            prev_key = Some(key);
3542                        }
3543                        Some(pk) => {
3544                            if key.str_eq(pk) {
3545                                run.push(item);
3546                            } else {
3547                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3548                                    std::mem::take(&mut run),
3549                                ))));
3550                                run.push(item);
3551                                prev_key = Some(key);
3552                            }
3553                        }
3554                    }
3555                }
3556                if !run.is_empty() {
3557                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3558                }
3559                chunks
3560            }
3561            _ => {
3562                let mut chunks: Vec<PerlValue> = Vec::new();
3563                let mut run: Vec<PerlValue> = Vec::new();
3564                let mut prev_key: Option<PerlValue> = None;
3565                for item in list {
3566                    self.scope.set_topic(item.clone());
3567                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
3568                    match &prev_key {
3569                        None => {
3570                            run.push(item);
3571                            prev_key = Some(key);
3572                        }
3573                        Some(pk) => {
3574                            if key.str_eq(pk) {
3575                                run.push(item);
3576                            } else {
3577                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3578                                    std::mem::take(&mut run),
3579                                ))));
3580                                run.push(item);
3581                                prev_key = Some(key);
3582                            }
3583                        }
3584                    }
3585                }
3586                if !run.is_empty() {
3587                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3588                }
3589                chunks
3590            }
3591        };
3592        Ok(match ctx {
3593            WantarrayCtx::List => PerlValue::array(chunks),
3594            WantarrayCtx::Scalar => PerlValue::integer(chunks.len() as i64),
3595            WantarrayCtx::Void => PerlValue::UNDEF,
3596        })
3597    }
3598
3599    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
3600    pub(crate) fn list_higher_order_block_builtin(
3601        &mut self,
3602        name: &str,
3603        args: &[PerlValue],
3604        line: usize,
3605    ) -> PerlResult<PerlValue> {
3606        match self.list_higher_order_block_builtin_exec(name, args, line) {
3607            Ok(v) => Ok(v),
3608            Err(FlowOrError::Error(e)) => Err(e),
3609            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
3610            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
3611                format!("{name}: unsupported control flow in block"),
3612                line,
3613            )),
3614        }
3615    }
3616
3617    fn list_higher_order_block_builtin_exec(
3618        &mut self,
3619        name: &str,
3620        args: &[PerlValue],
3621        line: usize,
3622    ) -> ExecResult {
3623        if args.is_empty() {
3624            return Err(
3625                PerlError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
3626            );
3627        }
3628        let Some(sub) = args[0].as_code_ref() else {
3629            return Err(PerlError::runtime(
3630                format!("{name}: first argument must be {{ BLOCK }}"),
3631                line,
3632            )
3633            .into());
3634        };
3635        let sub = sub.clone();
3636        let items: Vec<PerlValue> = args[1..].to_vec();
3637        if matches!(name, "tap" | "peek") && items.len() == 1 {
3638            if let Some(p) = items[0].as_pipeline() {
3639                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
3640                return Ok(PerlValue::pipeline(Arc::clone(&p)));
3641            }
3642            let v = &items[0];
3643            if v.is_iterator() || v.as_array_vec().is_some() {
3644                let source = crate::map_stream::into_pull_iter(v.clone());
3645                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
3646                return Ok(PerlValue::iterator(Arc::new(
3647                    crate::map_stream::TapIterator::new(
3648                        source,
3649                        sub,
3650                        self.subs.clone(),
3651                        capture,
3652                        atomic_arrays,
3653                        atomic_hashes,
3654                    ),
3655                )));
3656            }
3657        }
3658        // Streaming optimization disabled for these functions because the pre-captured
3659        // coderef from args[0] has its closure_env populated at parse time, which causes
3660        // $_ to get stale values on subsequent calls. These functions work correctly in
3661        // the non-streaming eager path below.
3662        let wa = self.wantarray_kind;
3663        match name {
3664            "take_while" => {
3665                let mut out = Vec::new();
3666                for item in items {
3667                    self.scope_push_hook();
3668                    self.scope.set_topic(item.clone());
3669                    let pred = self.exec_block(&sub.body)?;
3670                    self.scope_pop_hook();
3671                    if !pred.is_true() {
3672                        break;
3673                    }
3674                    out.push(item);
3675                }
3676                Ok(match wa {
3677                    WantarrayCtx::List => PerlValue::array(out),
3678                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
3679                    WantarrayCtx::Void => PerlValue::UNDEF,
3680                })
3681            }
3682            "drop_while" | "skip_while" => {
3683                let mut i = 0usize;
3684                while i < items.len() {
3685                    self.scope_push_hook();
3686                    self.scope.set_topic(items[i].clone());
3687                    let pred = self.exec_block(&sub.body)?;
3688                    self.scope_pop_hook();
3689                    if !pred.is_true() {
3690                        break;
3691                    }
3692                    i += 1;
3693                }
3694                let rest = items[i..].to_vec();
3695                Ok(match wa {
3696                    WantarrayCtx::List => PerlValue::array(rest),
3697                    WantarrayCtx::Scalar => PerlValue::integer(rest.len() as i64),
3698                    WantarrayCtx::Void => PerlValue::UNDEF,
3699                })
3700            }
3701            "reject" => {
3702                let mut out = Vec::new();
3703                for item in items {
3704                    self.scope_push_hook();
3705                    self.scope.set_topic(item.clone());
3706                    let pred = self.exec_block(&sub.body)?;
3707                    self.scope_pop_hook();
3708                    if !pred.is_true() {
3709                        out.push(item);
3710                    }
3711                }
3712                Ok(match wa {
3713                    WantarrayCtx::List => PerlValue::array(out),
3714                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
3715                    WantarrayCtx::Void => PerlValue::UNDEF,
3716                })
3717            }
3718            "tap" | "peek" => {
3719                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
3720                Ok(match wa {
3721                    WantarrayCtx::List => PerlValue::array(items),
3722                    WantarrayCtx::Scalar => PerlValue::integer(items.len() as i64),
3723                    WantarrayCtx::Void => PerlValue::UNDEF,
3724                })
3725            }
3726            "partition" => {
3727                let mut yes = Vec::new();
3728                let mut no = Vec::new();
3729                for item in items {
3730                    self.scope.set_topic(item.clone());
3731                    let pred = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3732                    if pred.is_true() {
3733                        yes.push(item);
3734                    } else {
3735                        no.push(item);
3736                    }
3737                }
3738                let yes_ref = PerlValue::array_ref(Arc::new(RwLock::new(yes)));
3739                let no_ref = PerlValue::array_ref(Arc::new(RwLock::new(no)));
3740                Ok(match wa {
3741                    WantarrayCtx::List => PerlValue::array(vec![yes_ref, no_ref]),
3742                    WantarrayCtx::Scalar => PerlValue::integer(2),
3743                    WantarrayCtx::Void => PerlValue::UNDEF,
3744                })
3745            }
3746            "min_by" => {
3747                let mut best: Option<(PerlValue, PerlValue)> = None;
3748                for item in items {
3749                    self.scope.set_topic(item.clone());
3750                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3751                    best = Some(match best {
3752                        None => (item, key),
3753                        Some((bv, bk)) => {
3754                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
3755                                (item, key)
3756                            } else {
3757                                (bv, bk)
3758                            }
3759                        }
3760                    });
3761                }
3762                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
3763            }
3764            "max_by" => {
3765                let mut best: Option<(PerlValue, PerlValue)> = None;
3766                for item in items {
3767                    self.scope.set_topic(item.clone());
3768                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3769                    best = Some(match best {
3770                        None => (item, key),
3771                        Some((bv, bk)) => {
3772                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
3773                                (item, key)
3774                            } else {
3775                                (bv, bk)
3776                            }
3777                        }
3778                    });
3779                }
3780                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
3781            }
3782            "zip_with" => {
3783                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
3784                // Flatten items, then treat each array ref/binding as a separate list.
3785                let flat: Vec<PerlValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
3786                let refs: Vec<Vec<PerlValue>> = flat
3787                    .iter()
3788                    .map(|el| {
3789                        if let Some(ar) = el.as_array_ref() {
3790                            ar.read().clone()
3791                        } else if let Some(name) = el.as_array_binding_name() {
3792                            self.scope.get_array(&name)
3793                        } else {
3794                            vec![el.clone()]
3795                        }
3796                    })
3797                    .collect();
3798                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
3799                let mut out = Vec::with_capacity(max_len);
3800                for i in 0..max_len {
3801                    let pair: Vec<PerlValue> = refs
3802                        .iter()
3803                        .map(|l| l.get(i).cloned().unwrap_or(PerlValue::UNDEF))
3804                        .collect();
3805                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
3806                    out.push(result);
3807                }
3808                Ok(match wa {
3809                    WantarrayCtx::List => PerlValue::array(out),
3810                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
3811                    WantarrayCtx::Void => PerlValue::UNDEF,
3812                })
3813            }
3814            "count_by" => {
3815                let mut counts = indexmap::IndexMap::new();
3816                for item in items {
3817                    self.scope.set_topic(item.clone());
3818                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3819                    let k = key.to_string();
3820                    let entry = counts.entry(k).or_insert(PerlValue::integer(0));
3821                    *entry = PerlValue::integer(entry.to_int() + 1);
3822                }
3823                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(counts))))
3824            }
3825            _ => Err(PerlError::runtime(
3826                format!("internal: unknown list block builtin `{name}`"),
3827                line,
3828            )
3829            .into()),
3830        }
3831    }
3832
3833    /// `rmdir LIST` — remove empty directories; returns count removed.
3834    pub(crate) fn builtin_rmdir_execute(
3835        &mut self,
3836        args: &[PerlValue],
3837        _line: usize,
3838    ) -> PerlResult<PerlValue> {
3839        let mut count = 0i64;
3840        for a in args {
3841            let p = a.to_string();
3842            if p.is_empty() {
3843                continue;
3844            }
3845            if std::fs::remove_dir(&p).is_ok() {
3846                count += 1;
3847            }
3848        }
3849        Ok(PerlValue::integer(count))
3850    }
3851
3852    /// `touch FILE, ...` — create if absent, update timestamps to now.
3853    pub(crate) fn builtin_touch_execute(
3854        &mut self,
3855        args: &[PerlValue],
3856        _line: usize,
3857    ) -> PerlResult<PerlValue> {
3858        let paths: Vec<String> = args.iter().map(|v| v.to_string()).collect();
3859        Ok(PerlValue::integer(crate::perl_fs::touch_paths(&paths)))
3860    }
3861
3862    /// `utime ATIME, MTIME, LIST`
3863    pub(crate) fn builtin_utime_execute(
3864        &mut self,
3865        args: &[PerlValue],
3866        line: usize,
3867    ) -> PerlResult<PerlValue> {
3868        if args.len() < 3 {
3869            return Err(PerlError::runtime(
3870                "utime requires at least three arguments (atime, mtime, files...)",
3871                line,
3872            ));
3873        }
3874        let at = args[0].to_int();
3875        let mt = args[1].to_int();
3876        let paths: Vec<String> = args.iter().skip(2).map(|v| v.to_string()).collect();
3877        let n = crate::perl_fs::utime_paths(at, mt, &paths);
3878        #[cfg(not(unix))]
3879        if !paths.is_empty() && n == 0 {
3880            return Err(PerlError::runtime(
3881                "utime is not supported on this platform",
3882                line,
3883            ));
3884        }
3885        Ok(PerlValue::integer(n))
3886    }
3887
3888    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
3889    pub(crate) fn builtin_umask_execute(
3890        &mut self,
3891        args: &[PerlValue],
3892        line: usize,
3893    ) -> PerlResult<PerlValue> {
3894        #[cfg(unix)]
3895        {
3896            let _ = line;
3897            if args.is_empty() {
3898                let cur = unsafe { libc::umask(0) };
3899                unsafe { libc::umask(cur) };
3900                return Ok(PerlValue::integer(cur as i64));
3901            }
3902            let new_m = args[0].to_int() as libc::mode_t;
3903            let old = unsafe { libc::umask(new_m) };
3904            Ok(PerlValue::integer(old as i64))
3905        }
3906        #[cfg(not(unix))]
3907        {
3908            let _ = args;
3909            Err(PerlError::runtime(
3910                "umask is not supported on this platform",
3911                line,
3912            ))
3913        }
3914    }
3915
3916    /// `getcwd` — current directory or undef on failure.
3917    pub(crate) fn builtin_getcwd_execute(
3918        &mut self,
3919        args: &[PerlValue],
3920        line: usize,
3921    ) -> PerlResult<PerlValue> {
3922        if !args.is_empty() {
3923            return Err(PerlError::runtime("getcwd takes no arguments", line));
3924        }
3925        match std::env::current_dir() {
3926            Ok(p) => Ok(PerlValue::string(p.to_string_lossy().into_owned())),
3927            Err(e) => {
3928                self.apply_io_error_to_errno(&e);
3929                Ok(PerlValue::UNDEF)
3930            }
3931        }
3932    }
3933
3934    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
3935    pub(crate) fn builtin_realpath_execute(
3936        &mut self,
3937        args: &[PerlValue],
3938        line: usize,
3939    ) -> PerlResult<PerlValue> {
3940        let path = args
3941            .first()
3942            .ok_or_else(|| PerlError::runtime("realpath: need path", line))?
3943            .to_string();
3944        if path.is_empty() {
3945            return Err(PerlError::runtime("realpath: need path", line));
3946        }
3947        match crate::perl_fs::realpath_resolved(&path) {
3948            Ok(s) => Ok(PerlValue::string(s)),
3949            Err(e) => {
3950                self.apply_io_error_to_errno(&e);
3951                Ok(PerlValue::UNDEF)
3952            }
3953        }
3954    }
3955
3956    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
3957    pub(crate) fn builtin_pipe_execute(
3958        &mut self,
3959        args: &[PerlValue],
3960        line: usize,
3961    ) -> PerlResult<PerlValue> {
3962        if args.len() != 2 {
3963            return Err(PerlError::runtime(
3964                "pipe requires exactly two arguments",
3965                line,
3966            ));
3967        }
3968        #[cfg(unix)]
3969        {
3970            use std::fs::File;
3971            use std::os::unix::io::FromRawFd;
3972
3973            let read_name = args[0].to_string();
3974            let write_name = args[1].to_string();
3975            if read_name.is_empty() || write_name.is_empty() {
3976                return Err(PerlError::runtime("pipe: invalid handle name", line));
3977            }
3978            let mut fds = [0i32; 2];
3979            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
3980                let e = std::io::Error::last_os_error();
3981                self.apply_io_error_to_errno(&e);
3982                return Ok(PerlValue::integer(0));
3983            }
3984            let read_file = unsafe { File::from_raw_fd(fds[0]) };
3985            let write_file = unsafe { File::from_raw_fd(fds[1]) };
3986
3987            let read_shared = Arc::new(Mutex::new(read_file));
3988            let write_shared = Arc::new(Mutex::new(write_file));
3989
3990            self.close_builtin_execute(read_name.clone()).ok();
3991            self.close_builtin_execute(write_name.clone()).ok();
3992
3993            self.io_file_slots
3994                .insert(read_name.clone(), Arc::clone(&read_shared));
3995            self.input_handles.insert(
3996                read_name,
3997                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
3998            );
3999
4000            self.io_file_slots
4001                .insert(write_name.clone(), Arc::clone(&write_shared));
4002            self.output_handles
4003                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
4004
4005            Ok(PerlValue::integer(1))
4006        }
4007        #[cfg(not(unix))]
4008        {
4009            let _ = args;
4010            Err(PerlError::runtime(
4011                "pipe is not supported on this platform",
4012                line,
4013            ))
4014        }
4015    }
4016
4017    pub(crate) fn close_builtin_execute(&mut self, name: String) -> PerlResult<PerlValue> {
4018        self.output_handles.remove(&name);
4019        self.input_handles.remove(&name);
4020        self.io_file_slots.remove(&name);
4021        if let Some(mut child) = self.pipe_children.remove(&name) {
4022            if let Ok(st) = child.wait() {
4023                self.record_child_exit_status(st);
4024            }
4025        }
4026        Ok(PerlValue::integer(1))
4027    }
4028
4029    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
4030        self.input_handles.contains_key(name)
4031    }
4032
4033    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
4034    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
4035    /// readline-level EOF tracking exists.
4036    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
4037        self.line_mode_eof_pending
4038    }
4039
4040    /// `eof` / `eof()` / `eof FH` — shared by the tree walker, [`crate::vm::VM`], and
4041    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
4042    /// not [`ExprKind::Eof`]).
4043    pub(crate) fn eof_builtin_execute(
4044        &self,
4045        args: &[PerlValue],
4046        line: usize,
4047    ) -> PerlResult<PerlValue> {
4048        match args.len() {
4049            0 => Ok(PerlValue::integer(if self.eof_without_arg_is_true() {
4050                1
4051            } else {
4052                0
4053            })),
4054            1 => {
4055                let name = args[0].to_string();
4056                let at_eof = !self.has_input_handle(&name);
4057                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
4058            }
4059            _ => Err(PerlError::runtime("eof: too many arguments", line)),
4060        }
4061    }
4062
4063    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
4064    /// `0`, stringifies to `""`) for `""`.
4065    pub(crate) fn study_return_value(s: &str) -> PerlValue {
4066        if s.is_empty() {
4067            PerlValue::string(String::new())
4068        } else {
4069            PerlValue::integer(1)
4070        }
4071    }
4072
4073    pub(crate) fn readline_builtin_execute(
4074        &mut self,
4075        handle: Option<&str>,
4076    ) -> PerlResult<PerlValue> {
4077        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
4078        if handle.is_none() {
4079            let argv = self.scope.get_array("ARGV");
4080            if !argv.is_empty() {
4081                loop {
4082                    if self.diamond_reader.is_none() {
4083                        while self.diamond_next_idx < argv.len() {
4084                            let path = argv[self.diamond_next_idx].to_string();
4085                            self.diamond_next_idx += 1;
4086                            match File::open(&path) {
4087                                Ok(f) => {
4088                                    self.argv_current_file = path;
4089                                    self.diamond_reader = Some(BufReader::new(f));
4090                                    break;
4091                                }
4092                                Err(e) => {
4093                                    self.apply_io_error_to_errno(&e);
4094                                }
4095                            }
4096                        }
4097                        if self.diamond_reader.is_none() {
4098                            return Ok(PerlValue::UNDEF);
4099                        }
4100                    }
4101                    let mut line_str = String::new();
4102                    let read_result: Result<usize, io::Error> =
4103                        if let Some(reader) = self.diamond_reader.as_mut() {
4104                            if self.open_pragma_utf8 {
4105                                let mut buf = Vec::new();
4106                                reader.read_until(b'\n', &mut buf).inspect(|n| {
4107                                    if *n > 0 {
4108                                        line_str = String::from_utf8_lossy(&buf).into_owned();
4109                                    }
4110                                })
4111                            } else {
4112                                let mut buf = Vec::new();
4113                                match reader.read_until(b'\n', &mut buf) {
4114                                    Ok(n) => {
4115                                        if n > 0 {
4116                                            line_str =
4117                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
4118                                                &buf,
4119                                            );
4120                                        }
4121                                        Ok(n)
4122                                    }
4123                                    Err(e) => Err(e),
4124                                }
4125                            }
4126                        } else {
4127                            unreachable!()
4128                        };
4129                    match read_result {
4130                        Ok(0) => {
4131                            self.diamond_reader = None;
4132                            continue;
4133                        }
4134                        Ok(_) => {
4135                            self.bump_line_for_handle(&self.argv_current_file.clone());
4136                            return Ok(PerlValue::string(line_str));
4137                        }
4138                        Err(e) => {
4139                            self.apply_io_error_to_errno(&e);
4140                            self.diamond_reader = None;
4141                            continue;
4142                        }
4143                    }
4144                }
4145            } else {
4146                self.argv_current_file.clear();
4147            }
4148        }
4149
4150        let handle_name = handle.unwrap_or("STDIN");
4151        let mut line_str = String::new();
4152        if handle_name == "STDIN" {
4153            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
4154                self.last_stdin_die_bracket = if handle.is_none() {
4155                    "<>".to_string()
4156                } else {
4157                    "<STDIN>".to_string()
4158                };
4159                self.bump_line_for_handle("STDIN");
4160                return Ok(PerlValue::string(queued));
4161            }
4162            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4163                let mut buf = Vec::new();
4164                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
4165                    if *n > 0 {
4166                        line_str = String::from_utf8_lossy(&buf).into_owned();
4167                    }
4168                })
4169            } else {
4170                let mut buf = Vec::new();
4171                let mut lock = io::stdin().lock();
4172                match lock.read_until(b'\n', &mut buf) {
4173                    Ok(n) => {
4174                        if n > 0 {
4175                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4176                        }
4177                        Ok(n)
4178                    }
4179                    Err(e) => Err(e),
4180                }
4181            };
4182            match r {
4183                Ok(0) => Ok(PerlValue::UNDEF),
4184                Ok(_) => {
4185                    self.last_stdin_die_bracket = if handle.is_none() {
4186                        "<>".to_string()
4187                    } else {
4188                        "<STDIN>".to_string()
4189                    };
4190                    self.bump_line_for_handle("STDIN");
4191                    Ok(PerlValue::string(line_str))
4192                }
4193                Err(e) => {
4194                    self.apply_io_error_to_errno(&e);
4195                    Ok(PerlValue::UNDEF)
4196                }
4197            }
4198        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
4199            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4200                let mut buf = Vec::new();
4201                reader.read_until(b'\n', &mut buf).inspect(|n| {
4202                    if *n > 0 {
4203                        line_str = String::from_utf8_lossy(&buf).into_owned();
4204                    }
4205                })
4206            } else {
4207                let mut buf = Vec::new();
4208                match reader.read_until(b'\n', &mut buf) {
4209                    Ok(n) => {
4210                        if n > 0 {
4211                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4212                        }
4213                        Ok(n)
4214                    }
4215                    Err(e) => Err(e),
4216                }
4217            };
4218            match r {
4219                Ok(0) => Ok(PerlValue::UNDEF),
4220                Ok(_) => {
4221                    self.bump_line_for_handle(handle_name);
4222                    Ok(PerlValue::string(line_str))
4223                }
4224                Err(e) => {
4225                    self.apply_io_error_to_errno(&e);
4226                    Ok(PerlValue::UNDEF)
4227                }
4228            }
4229        } else {
4230            Ok(PerlValue::UNDEF)
4231        }
4232    }
4233
4234    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
4235    pub(crate) fn readline_builtin_execute_list(
4236        &mut self,
4237        handle: Option<&str>,
4238    ) -> PerlResult<PerlValue> {
4239        let mut lines = Vec::new();
4240        loop {
4241            let v = self.readline_builtin_execute(handle)?;
4242            if v.is_undef() {
4243                break;
4244            }
4245            lines.push(v);
4246        }
4247        Ok(PerlValue::array(lines))
4248    }
4249
4250    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> PerlValue {
4251        match std::fs::read_dir(path) {
4252            Ok(rd) => {
4253                let entries: Vec<String> = rd
4254                    .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
4255                    .collect();
4256                self.dir_handles
4257                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
4258                PerlValue::integer(1)
4259            }
4260            Err(e) => {
4261                self.apply_io_error_to_errno(&e);
4262                PerlValue::integer(0)
4263            }
4264        }
4265    }
4266
4267    pub(crate) fn readdir_handle(&mut self, handle: &str) -> PerlValue {
4268        if let Some(dh) = self.dir_handles.get_mut(handle) {
4269            if dh.pos < dh.entries.len() {
4270                let s = dh.entries[dh.pos].clone();
4271                dh.pos += 1;
4272                PerlValue::string(s)
4273            } else {
4274                PerlValue::UNDEF
4275            }
4276        } else {
4277            PerlValue::UNDEF
4278        }
4279    }
4280
4281    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
4282    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> PerlValue {
4283        if let Some(dh) = self.dir_handles.get_mut(handle) {
4284            let rest: Vec<PerlValue> = dh.entries[dh.pos..]
4285                .iter()
4286                .cloned()
4287                .map(PerlValue::string)
4288                .collect();
4289            dh.pos = dh.entries.len();
4290            PerlValue::array(rest)
4291        } else {
4292            PerlValue::array(Vec::new())
4293        }
4294    }
4295
4296    pub(crate) fn closedir_handle(&mut self, handle: &str) -> PerlValue {
4297        PerlValue::integer(if self.dir_handles.remove(handle).is_some() {
4298            1
4299        } else {
4300            0
4301        })
4302    }
4303
4304    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> PerlValue {
4305        if let Some(dh) = self.dir_handles.get_mut(handle) {
4306            dh.pos = 0;
4307            PerlValue::integer(1)
4308        } else {
4309            PerlValue::integer(0)
4310        }
4311    }
4312
4313    pub(crate) fn telldir_handle(&mut self, handle: &str) -> PerlValue {
4314        self.dir_handles
4315            .get(handle)
4316            .map(|dh| PerlValue::integer(dh.pos as i64))
4317            .unwrap_or(PerlValue::UNDEF)
4318    }
4319
4320    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> PerlValue {
4321        if let Some(dh) = self.dir_handles.get_mut(handle) {
4322            dh.pos = pos.min(dh.entries.len());
4323            PerlValue::integer(1)
4324        } else {
4325            PerlValue::integer(0)
4326        }
4327    }
4328
4329    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
4330    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
4331    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
4332    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
4333    #[inline]
4334    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
4335        crate::special_vars::is_regex_match_scalar_name(name)
4336    }
4337
4338    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
4339    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
4340    /// `apply_regex_captures` instead of short-circuiting.
4341    #[inline]
4342    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
4343        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
4344            self.regex_capture_scope_fresh = false;
4345        }
4346    }
4347
4348    pub(crate) fn apply_regex_captures(
4349        &mut self,
4350        haystack: &str,
4351        offset: usize,
4352        re: &PerlCompiledRegex,
4353        caps: &PerlCaptures<'_>,
4354        capture_all: CaptureAllMode,
4355    ) -> Result<(), FlowOrError> {
4356        let m0 = caps.get(0).expect("regex capture 0");
4357        let s0 = offset + m0.start;
4358        let e0 = offset + m0.end;
4359        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
4360        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
4361        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
4362        let mut last_paren = String::new();
4363        for i in 1..caps.len() {
4364            if let Some(m) = caps.get(i) {
4365                last_paren = m.text.to_string();
4366            }
4367        }
4368        self.last_paren_match = last_paren;
4369        self.last_subpattern_name = String::new();
4370        for n in re.capture_names().flatten() {
4371            if caps.name(n).is_some() {
4372                self.last_subpattern_name = n.to_string();
4373            }
4374        }
4375        self.scope
4376            .set_scalar("&", PerlValue::string(self.last_match.clone()))?;
4377        self.scope
4378            .set_scalar("`", PerlValue::string(self.prematch.clone()))?;
4379        self.scope
4380            .set_scalar("'", PerlValue::string(self.postmatch.clone()))?;
4381        self.scope
4382            .set_scalar("+", PerlValue::string(self.last_paren_match.clone()))?;
4383        for i in 1..caps.len() {
4384            if let Some(m) = caps.get(i) {
4385                self.scope
4386                    .set_scalar(&i.to_string(), PerlValue::string(m.text.to_string()))?;
4387            }
4388        }
4389        let mut start_arr = vec![PerlValue::integer(s0 as i64)];
4390        let mut end_arr = vec![PerlValue::integer(e0 as i64)];
4391        for i in 1..caps.len() {
4392            if let Some(m) = caps.get(i) {
4393                start_arr.push(PerlValue::integer((offset + m.start) as i64));
4394                end_arr.push(PerlValue::integer((offset + m.end) as i64));
4395            } else {
4396                start_arr.push(PerlValue::integer(-1));
4397                end_arr.push(PerlValue::integer(-1));
4398            }
4399        }
4400        self.scope.set_array("-", start_arr)?;
4401        self.scope.set_array("+", end_arr)?;
4402        let mut named = IndexMap::new();
4403        for name in re.capture_names().flatten() {
4404            if let Some(m) = caps.name(name) {
4405                named.insert(name.to_string(), PerlValue::string(m.text.to_string()));
4406            }
4407        }
4408        self.scope.set_hash("+", named.clone())?;
4409        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
4410        let mut named_minus = IndexMap::new();
4411        for (name, val) in &named {
4412            named_minus.insert(
4413                name.clone(),
4414                PerlValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
4415            );
4416        }
4417        self.scope.set_hash("-", named_minus)?;
4418        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
4419        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
4420        match capture_all {
4421            CaptureAllMode::Empty => {
4422                self.scope.set_array("^CAPTURE_ALL", vec![])?;
4423            }
4424            CaptureAllMode::Append => {
4425                let mut rows = self.scope.get_array("^CAPTURE_ALL");
4426                rows.push(PerlValue::array(cap_flat));
4427                self.scope.set_array("^CAPTURE_ALL", rows)?;
4428            }
4429            CaptureAllMode::Skip => {}
4430        }
4431        Ok(())
4432    }
4433
4434    pub(crate) fn clear_flip_flop_state(&mut self) {
4435        self.flip_flop_active.clear();
4436        self.flip_flop_exclusive_left_line.clear();
4437        self.flip_flop_sequence.clear();
4438        self.flip_flop_last_dot.clear();
4439        self.flip_flop_tree.clear();
4440    }
4441
4442    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
4443        self.flip_flop_active.resize(slots as usize, false);
4444        self.flip_flop_active.fill(false);
4445        self.flip_flop_exclusive_left_line
4446            .resize(slots as usize, None);
4447        self.flip_flop_exclusive_left_line.fill(None);
4448        self.flip_flop_sequence.resize(slots as usize, 0);
4449        self.flip_flop_sequence.fill(0);
4450        self.flip_flop_last_dot.resize(slots as usize, None);
4451        self.flip_flop_last_dot.fill(None);
4452    }
4453
4454    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
4455    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
4456    /// [`Self::handle_line_numbers`]).
4457    #[inline]
4458    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
4459        if self.last_readline_handle.is_empty() {
4460            self.line_number
4461        } else {
4462            *self
4463                .handle_line_numbers
4464                .get(&self.last_readline_handle)
4465                .unwrap_or(&0)
4466        }
4467    }
4468
4469    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
4470    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
4471    ///
4472    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
4473    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
4474    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
4475    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
4476    /// get `0` / `N` from [`PerlValue::to_int`].
4477    pub(crate) fn scalar_flip_flop_eval(
4478        &mut self,
4479        left: i64,
4480        right: i64,
4481        slot: usize,
4482        exclusive: bool,
4483    ) -> PerlResult<PerlValue> {
4484        if self.flip_flop_active.len() <= slot {
4485            self.flip_flop_active.resize(slot + 1, false);
4486        }
4487        if self.flip_flop_exclusive_left_line.len() <= slot {
4488            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4489        }
4490        if self.flip_flop_sequence.len() <= slot {
4491            self.flip_flop_sequence.resize(slot + 1, 0);
4492        }
4493        if self.flip_flop_last_dot.len() <= slot {
4494            self.flip_flop_last_dot.resize(slot + 1, None);
4495        }
4496        let dot = self.scalar_flipflop_dot_line();
4497        let active = &mut self.flip_flop_active[slot];
4498        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4499        let seq = &mut self.flip_flop_sequence[slot];
4500        let last_dot = &mut self.flip_flop_last_dot[slot];
4501        if !*active {
4502            if dot == left {
4503                *active = true;
4504                *seq = 1;
4505                *last_dot = Some(dot);
4506                if exclusive {
4507                    *excl_left = Some(dot);
4508                } else {
4509                    *excl_left = None;
4510                    if dot == right {
4511                        *active = false;
4512                        return Ok(PerlValue::string(format!("{}E0", *seq)));
4513                    }
4514                }
4515                return Ok(PerlValue::string(seq.to_string()));
4516            }
4517            *last_dot = Some(dot);
4518            return Ok(PerlValue::string(String::new()));
4519        }
4520        // Already active: increment the sequence once per new `$.`, so a second evaluation on
4521        // the same line reads the same number (matches Perl `pp_flop`).
4522        if *last_dot != Some(dot) {
4523            *seq += 1;
4524            *last_dot = Some(dot);
4525        }
4526        let cur_seq = *seq;
4527        if let Some(ll) = *excl_left {
4528            if dot == right && dot > ll {
4529                *active = false;
4530                *excl_left = None;
4531                *seq = 0;
4532                return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4533            }
4534        } else if dot == right {
4535            *active = false;
4536            *seq = 0;
4537            return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4538        }
4539        Ok(PerlValue::string(cur_seq.to_string()))
4540    }
4541
4542    fn regex_flip_flop_transition(
4543        active: &mut bool,
4544        excl_left: &mut Option<i64>,
4545        exclusive: bool,
4546        dot: i64,
4547        left_m: bool,
4548        right_m: bool,
4549    ) -> i64 {
4550        if !*active {
4551            if left_m {
4552                *active = true;
4553                if exclusive {
4554                    *excl_left = Some(dot);
4555                } else {
4556                    *excl_left = None;
4557                    if right_m {
4558                        *active = false;
4559                    }
4560                }
4561                return 1;
4562            }
4563            return 0;
4564        }
4565        if let Some(ll) = *excl_left {
4566            if right_m && dot > ll {
4567                *active = false;
4568                *excl_left = None;
4569            }
4570        } else if right_m {
4571            *active = false;
4572        }
4573        1
4574    }
4575
4576    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
4577    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
4578    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
4579    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
4580    pub(crate) fn regex_flip_flop_eval(
4581        &mut self,
4582        left_pat: &str,
4583        left_flags: &str,
4584        right_pat: &str,
4585        right_flags: &str,
4586        slot: usize,
4587        exclusive: bool,
4588        line: usize,
4589    ) -> PerlResult<PerlValue> {
4590        let dot = self.scalar_flipflop_dot_line();
4591        let subject = self.scope.get_scalar("_").to_string();
4592        let left_re = self
4593            .compile_regex(left_pat, left_flags, line)
4594            .map_err(|e| match e {
4595                FlowOrError::Error(err) => err,
4596                FlowOrError::Flow(_) => {
4597                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4598                }
4599            })?;
4600        let right_re = self
4601            .compile_regex(right_pat, right_flags, line)
4602            .map_err(|e| match e {
4603                FlowOrError::Error(err) => err,
4604                FlowOrError::Flow(_) => {
4605                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4606                }
4607            })?;
4608        let left_m = left_re.is_match(&subject);
4609        let right_m = right_re.is_match(&subject);
4610        if self.flip_flop_active.len() <= slot {
4611            self.flip_flop_active.resize(slot + 1, false);
4612        }
4613        if self.flip_flop_exclusive_left_line.len() <= slot {
4614            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4615        }
4616        let active = &mut self.flip_flop_active[slot];
4617        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4618        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4619            active, excl_left, exclusive, dot, left_m, right_m,
4620        )))
4621    }
4622
4623    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
4624    pub(crate) fn regex_flip_flop_eval_dynamic_right(
4625        &mut self,
4626        left_pat: &str,
4627        left_flags: &str,
4628        slot: usize,
4629        exclusive: bool,
4630        line: usize,
4631        right_m: bool,
4632    ) -> PerlResult<PerlValue> {
4633        let dot = self.scalar_flipflop_dot_line();
4634        let subject = self.scope.get_scalar("_").to_string();
4635        let left_re = self
4636            .compile_regex(left_pat, left_flags, line)
4637            .map_err(|e| match e {
4638                FlowOrError::Error(err) => err,
4639                FlowOrError::Flow(_) => {
4640                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4641                }
4642            })?;
4643        let left_m = left_re.is_match(&subject);
4644        if self.flip_flop_active.len() <= slot {
4645            self.flip_flop_active.resize(slot + 1, false);
4646        }
4647        if self.flip_flop_exclusive_left_line.len() <= slot {
4648            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4649        }
4650        let active = &mut self.flip_flop_active[slot];
4651        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4652        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4653            active, excl_left, exclusive, dot, left_m, right_m,
4654        )))
4655    }
4656
4657    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
4658    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
4659        &mut self,
4660        left_pat: &str,
4661        left_flags: &str,
4662        slot: usize,
4663        exclusive: bool,
4664        line: usize,
4665        rhs_line: i64,
4666    ) -> PerlResult<PerlValue> {
4667        let dot = self.scalar_flipflop_dot_line();
4668        let subject = self.scope.get_scalar("_").to_string();
4669        let left_re = self
4670            .compile_regex(left_pat, left_flags, line)
4671            .map_err(|e| match e {
4672                FlowOrError::Error(err) => err,
4673                FlowOrError::Flow(_) => {
4674                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4675                }
4676            })?;
4677        let left_m = left_re.is_match(&subject);
4678        let right_m = dot == rhs_line;
4679        if self.flip_flop_active.len() <= slot {
4680            self.flip_flop_active.resize(slot + 1, false);
4681        }
4682        if self.flip_flop_exclusive_left_line.len() <= slot {
4683            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4684        }
4685        let active = &mut self.flip_flop_active[slot];
4686        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4687        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4688            active, excl_left, exclusive, dot, left_m, right_m,
4689        )))
4690    }
4691
4692    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
4693    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
4694    /// right test until `$.` is strictly past the line where the left regex matched (same as
4695    /// [`Self::regex_flip_flop_eval`]).
4696    pub(crate) fn regex_eof_flip_flop_eval(
4697        &mut self,
4698        left_pat: &str,
4699        left_flags: &str,
4700        slot: usize,
4701        exclusive: bool,
4702        line: usize,
4703    ) -> PerlResult<PerlValue> {
4704        let dot = self.scalar_flipflop_dot_line();
4705        let subject = self.scope.get_scalar("_").to_string();
4706        let left_re = self
4707            .compile_regex(left_pat, left_flags, line)
4708            .map_err(|e| match e {
4709                FlowOrError::Error(err) => err,
4710                FlowOrError::Flow(_) => {
4711                    PerlError::runtime("unexpected flow in regex/eof flip-flop", line)
4712                }
4713            })?;
4714        let left_m = left_re.is_match(&subject);
4715        let right_m = self.eof_without_arg_is_true();
4716        if self.flip_flop_active.len() <= slot {
4717            self.flip_flop_active.resize(slot + 1, false);
4718        }
4719        if self.flip_flop_exclusive_left_line.len() <= slot {
4720            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4721        }
4722        let active = &mut self.flip_flop_active[slot];
4723        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4724        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4725            active, excl_left, exclusive, dot, left_m, right_m,
4726        )))
4727    }
4728
4729    /// Shared `chomp` for tree-walker and VM (mutates `target`).
4730    pub(crate) fn chomp_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
4731        let mut s = val.to_string();
4732        let removed = if s.ends_with('\n') {
4733            s.pop();
4734            1i64
4735        } else {
4736            0i64
4737        };
4738        self.assign_value(target, PerlValue::string(s))?;
4739        Ok(PerlValue::integer(removed))
4740    }
4741
4742    /// Shared `chop` for tree-walker and VM (mutates `target`).
4743    pub(crate) fn chop_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
4744        let mut s = val.to_string();
4745        let chopped = s
4746            .pop()
4747            .map(|c| PerlValue::string(c.to_string()))
4748            .unwrap_or(PerlValue::UNDEF);
4749        self.assign_value(target, PerlValue::string(s))?;
4750        Ok(chopped)
4751    }
4752
4753    /// Shared regex match for tree-walker and VM (`pos` is updated for scalar `/g`).
4754    pub(crate) fn regex_match_execute(
4755        &mut self,
4756        s: String,
4757        pattern: &str,
4758        flags: &str,
4759        scalar_g: bool,
4760        pos_key: &str,
4761        line: usize,
4762    ) -> ExecResult {
4763        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
4764        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
4765        // also keep per-pattern `pos()` state that the memo doesn't track.
4766        //
4767        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
4768        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
4769        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
4770        if !flags.contains('g') && !scalar_g {
4771            let memo_hit = {
4772                if let Some(ref mem) = self.regex_match_memo {
4773                    mem.pattern == pattern
4774                        && mem.flags == flags
4775                        && mem.multiline == self.multiline_match
4776                        && mem.haystack == s
4777                } else {
4778                    false
4779                }
4780            };
4781            if memo_hit {
4782                if self.regex_capture_scope_fresh {
4783                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
4784                }
4785                // Memo hit but scope side effects were invalidated. Re-apply captures
4786                // from the memoized haystack + a fresh compiled regex.
4787                let (memo_s, memo_result) = {
4788                    let mem = self.regex_match_memo.as_ref().expect("memo");
4789                    (mem.haystack.clone(), mem.result.clone())
4790                };
4791                let re = self.compile_regex(pattern, flags, line)?;
4792                if let Some(caps) = re.captures(&memo_s) {
4793                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
4794                }
4795                self.regex_capture_scope_fresh = true;
4796                return Ok(memo_result);
4797            }
4798        }
4799        let re = self.compile_regex(pattern, flags, line)?;
4800        if flags.contains('g') && scalar_g {
4801            let key = pos_key.to_string();
4802            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
4803            if start == 0 {
4804                self.scope.set_array("^CAPTURE_ALL", vec![])?;
4805            }
4806            if start > s.len() {
4807                self.regex_pos.insert(key, None);
4808                return Ok(PerlValue::integer(0));
4809            }
4810            let sub = s.get(start..).unwrap_or("");
4811            if let Some(caps) = re.captures(sub) {
4812                let overall = caps.get(0).expect("capture 0");
4813                let abs_end = start + overall.end;
4814                self.regex_pos.insert(key, Some(abs_end));
4815                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
4816                Ok(PerlValue::integer(1))
4817            } else {
4818                self.regex_pos.insert(key, None);
4819                Ok(PerlValue::integer(0))
4820            }
4821        } else if flags.contains('g') {
4822            let mut rows = Vec::new();
4823            let mut last_caps: Option<PerlCaptures<'_>> = None;
4824            for caps in re.captures_iter(&s) {
4825                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
4826                    &caps,
4827                )));
4828                last_caps = Some(caps);
4829            }
4830            self.scope.set_array("^CAPTURE_ALL", rows)?;
4831            let matches: Vec<PerlValue> = match &*re {
4832                PerlCompiledRegex::Rust(r) => r
4833                    .find_iter(&s)
4834                    .map(|m| PerlValue::string(m.as_str().to_string()))
4835                    .collect(),
4836                PerlCompiledRegex::Fancy(r) => r
4837                    .find_iter(&s)
4838                    .filter_map(|m| m.ok())
4839                    .map(|m| PerlValue::string(m.as_str().to_string()))
4840                    .collect(),
4841                PerlCompiledRegex::Pcre2(r) => r
4842                    .find_iter(s.as_bytes())
4843                    .filter_map(|m| m.ok())
4844                    .map(|m| {
4845                        let t = s.get(m.start()..m.end()).unwrap_or("");
4846                        PerlValue::string(t.to_string())
4847                    })
4848                    .collect(),
4849            };
4850            if matches.is_empty() {
4851                Ok(PerlValue::integer(0))
4852            } else {
4853                if let Some(caps) = last_caps {
4854                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
4855                }
4856                Ok(PerlValue::array(matches))
4857            }
4858        } else if let Some(caps) = re.captures(&s) {
4859            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
4860            let result = PerlValue::integer(1);
4861            self.regex_match_memo = Some(RegexMatchMemo {
4862                pattern: pattern.to_string(),
4863                flags: flags.to_string(),
4864                multiline: self.multiline_match,
4865                haystack: s,
4866                result: result.clone(),
4867            });
4868            self.regex_capture_scope_fresh = true;
4869            Ok(result)
4870        } else {
4871            let result = PerlValue::integer(0);
4872            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
4873            self.regex_match_memo = Some(RegexMatchMemo {
4874                pattern: pattern.to_string(),
4875                flags: flags.to_string(),
4876                multiline: self.multiline_match,
4877                haystack: s,
4878                result: result.clone(),
4879            });
4880            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
4881            // the last successful match (if any) set them to. Don't flip the flag.
4882            Ok(result)
4883        }
4884    }
4885
4886    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
4887    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
4888    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
4889    pub(crate) fn expand_env_braces_in_subst(
4890        &mut self,
4891        raw: &str,
4892        line: usize,
4893    ) -> PerlResult<String> {
4894        self.materialize_env_if_needed();
4895        let mut out = String::new();
4896        let mut rest = raw;
4897        while let Some(idx) = rest.find("$ENV{") {
4898            out.push_str(&rest[..idx]);
4899            let after = &rest[idx + 5..];
4900            let end = after
4901                .find('}')
4902                .ok_or_else(|| PerlError::runtime("Unclosed $ENV{...} in s///", line))?;
4903            let key = &after[..end];
4904            let val = self.scope.get_hash_element("ENV", key);
4905            out.push_str(&val.to_string());
4906            rest = &after[end + 1..];
4907        }
4908        out.push_str(rest);
4909        Ok(out)
4910    }
4911
4912    /// Shared `s///` for tree-walker and VM.
4913    ///
4914    /// Perl replacement strings accept both `\1` and `$1` for back-references.
4915    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
4916    /// understand `$N`, so we normalise here.
4917    pub(crate) fn regex_subst_execute(
4918        &mut self,
4919        s: String,
4920        pattern: &str,
4921        replacement: &str,
4922        flags: &str,
4923        target: &Expr,
4924        line: usize,
4925    ) -> ExecResult {
4926        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
4927        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
4928        let re = self.compile_regex(&pattern, &re_flags, line)?;
4929        if flags.contains('e') {
4930            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
4931        }
4932        let replacement =
4933            normalize_replacement_backrefs(&self.expand_env_braces_in_subst(replacement, line)?);
4934        let last_caps = if flags.contains('g') {
4935            let mut rows = Vec::new();
4936            let mut last = None;
4937            for caps in re.captures_iter(&s) {
4938                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
4939                    &caps,
4940                )));
4941                last = Some(caps);
4942            }
4943            self.scope.set_array("^CAPTURE_ALL", rows)?;
4944            last
4945        } else {
4946            re.captures(&s)
4947        };
4948        if let Some(caps) = last_caps {
4949            let mode = if flags.contains('g') {
4950                CaptureAllMode::Skip
4951            } else {
4952                CaptureAllMode::Empty
4953            };
4954            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
4955        }
4956        let (new_s, count) = if flags.contains('g') {
4957            let count = re.find_iter_count(&s);
4958            (re.replace_all(&s, replacement.as_str()), count)
4959        } else {
4960            let count = if re.is_match(&s) { 1 } else { 0 };
4961            (re.replace(&s, replacement.as_str()), count)
4962        };
4963        if flags.contains('r') {
4964            // /r — non-destructive: return the modified string, leave target unchanged
4965            Ok(PerlValue::string(new_s))
4966        } else {
4967            self.assign_value(target, PerlValue::string(new_s))?;
4968            Ok(PerlValue::integer(count as i64))
4969        }
4970    }
4971
4972    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
4973    /// and executes the string; the next round uses [`PerlValue::to_string`] of the prior value).
4974    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
4975        let prep_source = |raw: &str| -> String {
4976            let mut code = raw.trim().to_string();
4977            if !code.ends_with(';') {
4978                code.push(';');
4979            }
4980            code
4981        };
4982        let mut cur = prep_source(replacement);
4983        let mut last = PerlValue::UNDEF;
4984        for round in 0..e_count {
4985            last = crate::parse_and_run_string(&cur, self)?;
4986            if round + 1 < e_count {
4987                cur = prep_source(&last.to_string());
4988            }
4989        }
4990        Ok(last)
4991    }
4992
4993    fn regex_subst_execute_eval(
4994        &mut self,
4995        s: String,
4996        re: &PerlCompiledRegex,
4997        replacement: &str,
4998        flags: &str,
4999        target: &Expr,
5000        line: usize,
5001    ) -> ExecResult {
5002        let e_count = flags.chars().filter(|c| *c == 'e').count();
5003        if e_count == 0 {
5004            return Err(PerlError::runtime("s///e: internal error (no e flag)", line).into());
5005        }
5006
5007        if flags.contains('g') {
5008            let mut rows = Vec::new();
5009            let mut out = String::new();
5010            let mut last = 0usize;
5011            let mut count = 0usize;
5012            for caps in re.captures_iter(&s) {
5013                let m0 = caps.get(0).expect("regex capture 0");
5014                out.push_str(&s[last..m0.start]);
5015                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5016                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5017                out.push_str(&repl_val.to_string());
5018                last = m0.end;
5019                count += 1;
5020                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5021                    &caps,
5022                )));
5023            }
5024            self.scope.set_array("^CAPTURE_ALL", rows)?;
5025            out.push_str(&s[last..]);
5026            if flags.contains('r') {
5027                return Ok(PerlValue::string(out));
5028            }
5029            self.assign_value(target, PerlValue::string(out))?;
5030            return Ok(PerlValue::integer(count as i64));
5031        }
5032        if let Some(caps) = re.captures(&s) {
5033            let m0 = caps.get(0).expect("regex capture 0");
5034            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5035            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5036            let mut out = String::new();
5037            out.push_str(&s[..m0.start]);
5038            out.push_str(&repl_val.to_string());
5039            out.push_str(&s[m0.end..]);
5040            if flags.contains('r') {
5041                return Ok(PerlValue::string(out));
5042            }
5043            self.assign_value(target, PerlValue::string(out))?;
5044            return Ok(PerlValue::integer(1));
5045        }
5046        if flags.contains('r') {
5047            return Ok(PerlValue::string(s));
5048        }
5049        self.assign_value(target, PerlValue::string(s))?;
5050        Ok(PerlValue::integer(0))
5051    }
5052
5053    /// Shared `tr///` for tree-walker and VM.
5054    pub(crate) fn regex_transliterate_execute(
5055        &mut self,
5056        s: String,
5057        from: &str,
5058        to: &str,
5059        flags: &str,
5060        target: &Expr,
5061        line: usize,
5062    ) -> ExecResult {
5063        let _ = line;
5064        let from_chars = Self::tr_expand_ranges(from);
5065        let to_chars = Self::tr_expand_ranges(to);
5066        let mut count = 0i64;
5067        let new_s: String = s
5068            .chars()
5069            .map(|c| {
5070                if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
5071                    count += 1;
5072                    to_chars.get(pos).or(to_chars.last()).copied().unwrap_or(c)
5073                } else {
5074                    c
5075                }
5076            })
5077            .collect();
5078        if flags.contains('r') {
5079            // /r — non-destructive: return the modified string, leave target unchanged
5080            Ok(PerlValue::string(new_s))
5081        } else {
5082            if !flags.contains('d') {
5083                self.assign_value(target, PerlValue::string(new_s))?;
5084            }
5085            Ok(PerlValue::integer(count))
5086        }
5087    }
5088
5089    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
5090    /// A literal `-` at the start or end of the spec is kept as-is.
5091    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
5092        let raw: Vec<char> = spec.chars().collect();
5093        let mut out = Vec::with_capacity(raw.len());
5094        let mut i = 0;
5095        while i < raw.len() {
5096            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
5097                let start = raw[i] as u32;
5098                let end = raw[i + 2] as u32;
5099                for code in start..=end {
5100                    if let Some(c) = char::from_u32(code) {
5101                        out.push(c);
5102                    }
5103                }
5104                i += 3;
5105            } else {
5106                out.push(raw[i]);
5107                i += 1;
5108            }
5109        }
5110        out
5111    }
5112
5113    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
5114    pub(crate) fn splice_builtin_execute(
5115        &mut self,
5116        args: &[PerlValue],
5117        line: usize,
5118    ) -> PerlResult<PerlValue> {
5119        if args.is_empty() {
5120            return Err(PerlError::runtime("splice: missing array", line));
5121        }
5122        let arr_name = args[0].to_string();
5123        let arr_len = self.scope.array_len(&arr_name);
5124        let offset_val = args
5125            .get(1)
5126            .cloned()
5127            .unwrap_or_else(|| PerlValue::integer(0));
5128        let length_val = match args.get(2) {
5129            None => PerlValue::UNDEF,
5130            Some(v) => v.clone(),
5131        };
5132        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
5133        let rep_vals: Vec<PerlValue> = args.iter().skip(3).cloned().collect();
5134        let arr = self.scope.get_array_mut(&arr_name)?;
5135        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
5136        for (i, v) in rep_vals.into_iter().enumerate() {
5137            arr.insert(off + i, v);
5138        }
5139        Ok(match self.wantarray_kind {
5140            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
5141            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
5142        })
5143    }
5144
5145    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
5146    pub(crate) fn unshift_builtin_execute(
5147        &mut self,
5148        args: &[PerlValue],
5149        line: usize,
5150    ) -> PerlResult<PerlValue> {
5151        if args.is_empty() {
5152            return Err(PerlError::runtime("unshift: missing array", line));
5153        }
5154        let arr_name = args[0].to_string();
5155        let mut flat_vals: Vec<PerlValue> = Vec::new();
5156        for a in args.iter().skip(1) {
5157            if let Some(items) = a.as_array_vec() {
5158                flat_vals.extend(items);
5159            } else {
5160                flat_vals.push(a.clone());
5161            }
5162        }
5163        let arr = self.scope.get_array_mut(&arr_name)?;
5164        for (i, v) in flat_vals.into_iter().enumerate() {
5165            arr.insert(i, v);
5166        }
5167        Ok(PerlValue::integer(arr.len() as i64))
5168    }
5169
5170    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
5171    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
5172    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
5173        if upper == 0.0 {
5174            self.rand_rng.gen_range(0.0..1.0)
5175        } else if upper > 0.0 {
5176            self.rand_rng.gen_range(0.0..upper)
5177        } else {
5178            self.rand_rng.gen_range(upper..0.0)
5179        }
5180    }
5181
5182    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
5183    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
5184        let n = if let Some(s) = seed {
5185            s as i64
5186        } else {
5187            std::time::SystemTime::now()
5188                .duration_since(std::time::UNIX_EPOCH)
5189                .map(|d| d.as_secs() as i64)
5190                .unwrap_or(1)
5191        };
5192        let mag = n.unsigned_abs();
5193        self.rand_rng = StdRng::seed_from_u64(mag);
5194        n.abs()
5195    }
5196
5197    pub fn set_file(&mut self, file: &str) {
5198        self.file = file.to_string();
5199    }
5200
5201    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
5202    pub fn repl_completion_names(&self) -> Vec<String> {
5203        let mut v = self.scope.repl_binding_names();
5204        v.extend(self.subs.keys().cloned());
5205        v.sort();
5206        v.dedup();
5207        v
5208    }
5209
5210    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
5211    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
5212        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
5213        subs.sort();
5214        let mut classes: HashSet<String> = HashSet::new();
5215        for k in &subs {
5216            if let Some((pkg, rest)) = k.split_once("::") {
5217                if !rest.contains("::") {
5218                    classes.insert(pkg.to_string());
5219                }
5220            }
5221        }
5222        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
5223        for bn in self.scope.repl_binding_names() {
5224            if let Some(r) = bn.strip_prefix('$') {
5225                let v = self.scope.get_scalar(r);
5226                if let Some(b) = v.as_blessed_ref() {
5227                    blessed_scalars.insert(r.to_string(), b.class.clone());
5228                    classes.insert(b.class.clone());
5229                }
5230            }
5231        }
5232        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
5233        for c in classes {
5234            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
5235        }
5236        ReplCompletionSnapshot {
5237            subs,
5238            blessed_scalars,
5239            isa_for_class,
5240        }
5241    }
5242
5243    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
5244        if n == 0 {
5245            return Err(FlowOrError::Error(PerlError::runtime(
5246                "bench: iteration count must be positive",
5247                line,
5248            )));
5249        }
5250        let warmup = (n / 10).clamp(1, 10);
5251        for _ in 0..warmup {
5252            self.exec_block(body)?;
5253        }
5254        let mut samples = Vec::with_capacity(n);
5255        for _ in 0..n {
5256            let start = std::time::Instant::now();
5257            self.exec_block(body)?;
5258            samples.push(start.elapsed().as_secs_f64() * 1000.0);
5259        }
5260        let mut sorted = samples.clone();
5261        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
5262        let min_ms = sorted[0];
5263        let mean = samples.iter().sum::<f64>() / n as f64;
5264        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
5265            .saturating_sub(1)
5266            .min(n - 1);
5267        let p99_ms = sorted[p99_idx];
5268        Ok(PerlValue::string(format!(
5269            "bench: n={} warmup={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
5270            n, warmup, min_ms, mean, p99_ms
5271        )))
5272    }
5273
5274    pub fn execute(&mut self, program: &Program) -> PerlResult<PerlValue> {
5275        // `-n`/`-p`: main must run only inside [`Self::process_line`], not as a full-program VM/tree
5276        // run (would execute `print` once before any input, etc.).
5277        if self.line_mode_skip_main {
5278            return self.execute_tree(program);
5279        }
5280        // With `--profile`, the VM records per-opcode line times and sub enter/return (JIT off).
5281        // Try bytecode VM first — falls back to tree-walker on unsupported features
5282        if let Some(result) = crate::try_vm_execute(program, self) {
5283            return result;
5284        }
5285
5286        // Tree-walker fallback
5287        self.execute_tree(program)
5288    }
5289
5290    /// Run `END` blocks (after `-n`/`-p` line loop when prelude used [`Self::line_mode_skip_main`]).
5291    pub fn run_end_blocks(&mut self) -> PerlResult<()> {
5292        self.global_phase = "END".to_string();
5293        let ends = std::mem::take(&mut self.end_blocks);
5294        for block in &ends {
5295            self.exec_block(block).map_err(|e| match e {
5296                FlowOrError::Error(e) => e,
5297                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in END", 0),
5298            })?;
5299        }
5300        Ok(())
5301    }
5302
5303    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
5304    /// and drain remaining `DESTROY` callbacks.
5305    pub fn run_global_teardown(&mut self) -> PerlResult<()> {
5306        self.global_phase = "DESTRUCT".to_string();
5307        self.drain_pending_destroys(0)
5308    }
5309
5310    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
5311    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> PerlResult<()> {
5312        loop {
5313            let batch = crate::pending_destroy::take_queue();
5314            if batch.is_empty() {
5315                break;
5316            }
5317            for (class, payload) in batch {
5318                let fq = format!("{}::DESTROY", class);
5319                let Some(sub) = self.subs.get(&fq).cloned() else {
5320                    continue;
5321                };
5322                let inv = PerlValue::blessed(Arc::new(
5323                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
5324                ));
5325                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
5326                    Ok(_) => {}
5327                    Err(FlowOrError::Error(e)) => return Err(e),
5328                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
5329                    Err(FlowOrError::Flow(other)) => {
5330                        return Err(PerlError::runtime(
5331                            format!("DESTROY: unexpected control flow ({other:?})"),
5332                            line,
5333                        ));
5334                    }
5335                }
5336            }
5337        }
5338        Ok(())
5339    }
5340
5341    /// Tree-walking execution (fallback when bytecode compilation fails).
5342    pub fn execute_tree(&mut self, program: &Program) -> PerlResult<PerlValue> {
5343        // `${^GLOBAL_PHASE}` — each program starts in `RUN` (Perl before any `BEGIN` runs).
5344        self.global_phase = "RUN".to_string();
5345        self.clear_flip_flop_state();
5346        // First pass: subs, `use` (source order), BEGIN/END collection
5347        self.prepare_program_top_level(program)?;
5348
5349        // Execute BEGIN blocks (Perl uses phase `START` here).
5350        let begins = std::mem::take(&mut self.begin_blocks);
5351        if !begins.is_empty() {
5352            self.global_phase = "START".to_string();
5353        }
5354        for block in &begins {
5355            self.exec_block(block).map_err(|e| match e {
5356                FlowOrError::Error(e) => e,
5357                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in BEGIN", 0),
5358            })?;
5359        }
5360
5361        // UNITCHECK — reverse order of compilation (end of unit, before CHECK).
5362        // Perl keeps `${^GLOBAL_PHASE}` as **`START`** during these blocks (not `UNITCHECK`).
5363        let ucs = std::mem::take(&mut self.unit_check_blocks);
5364        for block in ucs.iter().rev() {
5365            self.exec_block(block).map_err(|e| match e {
5366                FlowOrError::Error(e) => e,
5367                FlowOrError::Flow(_) => {
5368                    PerlError::runtime("Unexpected flow control in UNITCHECK", 0)
5369                }
5370            })?;
5371        }
5372
5373        // CHECK — reverse order (end of compile phase).
5374        let checks = std::mem::take(&mut self.check_blocks);
5375        if !checks.is_empty() {
5376            self.global_phase = "CHECK".to_string();
5377        }
5378        for block in checks.iter().rev() {
5379            self.exec_block(block).map_err(|e| match e {
5380                FlowOrError::Error(e) => e,
5381                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in CHECK", 0),
5382            })?;
5383        }
5384
5385        // INIT — forward order (before main runtime).
5386        let inits = std::mem::take(&mut self.init_blocks);
5387        if !inits.is_empty() {
5388            self.global_phase = "INIT".to_string();
5389        }
5390        for block in &inits {
5391            self.exec_block(block).map_err(|e| match e {
5392                FlowOrError::Error(e) => e,
5393                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in INIT", 0),
5394            })?;
5395        }
5396
5397        self.global_phase = "RUN".to_string();
5398
5399        if self.line_mode_skip_main {
5400            // Body runs once per input line in [`Self::process_line`]; `END` runs after the loop
5401            // via [`Self::run_end_blocks`].
5402            return Ok(PerlValue::UNDEF);
5403        }
5404
5405        // Execute main program
5406        let mut last = PerlValue::UNDEF;
5407        for stmt in &program.statements {
5408            match &stmt.kind {
5409                StmtKind::Begin(_)
5410                | StmtKind::UnitCheck(_)
5411                | StmtKind::Check(_)
5412                | StmtKind::Init(_)
5413                | StmtKind::End(_)
5414                | StmtKind::UsePerlVersion { .. }
5415                | StmtKind::Use { .. }
5416                | StmtKind::No { .. }
5417                | StmtKind::FormatDecl { .. } => continue,
5418                _ => {
5419                    match self.exec_statement(stmt) {
5420                        Ok(val) => last = val,
5421                        Err(FlowOrError::Error(e)) => {
5422                            // Execute END blocks before propagating (all exit codes, including 0)
5423                            self.global_phase = "END".to_string();
5424                            let ends = std::mem::take(&mut self.end_blocks);
5425                            for block in &ends {
5426                                let _ = self.exec_block(block);
5427                            }
5428                            return Err(e);
5429                        }
5430                        Err(FlowOrError::Flow(Flow::Return(v))) => {
5431                            last = v;
5432                            break;
5433                        }
5434                        Err(FlowOrError::Flow(_)) => {}
5435                    }
5436                }
5437            }
5438        }
5439
5440        // Execute END blocks (Perl uses phase `END` here).
5441        self.global_phase = "END".to_string();
5442        let ends = std::mem::take(&mut self.end_blocks);
5443        for block in &ends {
5444            let _ = self.exec_block(block);
5445        }
5446
5447        self.drain_pending_destroys(0)?;
5448        Ok(last)
5449    }
5450
5451    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
5452        self.exec_block_with_tail(block, WantarrayCtx::Void)
5453    }
5454
5455    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
5456    /// Non-final statements stay void context.
5457    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5458        let uses_goto = block
5459            .iter()
5460            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
5461        if uses_goto {
5462            self.scope_push_hook();
5463            let r = self.exec_block_with_goto_tail(block, tail);
5464            self.scope_pop_hook();
5465            r
5466        } else {
5467            self.scope_push_hook();
5468            let result = self.exec_block_no_scope_with_tail(block, tail);
5469            self.scope_pop_hook();
5470            result
5471        }
5472    }
5473
5474    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5475        let mut map: HashMap<String, usize> = HashMap::new();
5476        for (i, s) in block.iter().enumerate() {
5477            if let Some(l) = &s.label {
5478                map.insert(l.clone(), i);
5479            }
5480        }
5481        let mut pc = 0usize;
5482        let mut last = PerlValue::UNDEF;
5483        let last_idx = block.len().saturating_sub(1);
5484        while pc < block.len() {
5485            if let StmtKind::Goto { target } = &block[pc].kind {
5486                let line = block[pc].line;
5487                let name = self.eval_expr(target)?.to_string();
5488                pc = *map.get(&name).ok_or_else(|| {
5489                    FlowOrError::Error(PerlError::runtime(
5490                        format!("goto: unknown label {}", name),
5491                        line,
5492                    ))
5493                })?;
5494                continue;
5495            }
5496            let v = if pc == last_idx {
5497                match &block[pc].kind {
5498                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
5499                    _ => self.exec_statement(&block[pc])?,
5500                }
5501            } else {
5502                self.exec_statement(&block[pc])?
5503            };
5504            last = v;
5505            pc += 1;
5506        }
5507        Ok(last)
5508    }
5509
5510    /// Execute block statements without pushing/popping a scope frame.
5511    /// Used internally by loops and the VM for sub calls.
5512    #[inline]
5513    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
5514        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
5515    }
5516
5517    pub(crate) fn exec_block_no_scope_with_tail(
5518        &mut self,
5519        block: &Block,
5520        tail: WantarrayCtx,
5521    ) -> ExecResult {
5522        if block.is_empty() {
5523            return Ok(PerlValue::UNDEF);
5524        }
5525        let last_i = block.len() - 1;
5526        for (i, stmt) in block.iter().enumerate() {
5527            if i < last_i {
5528                self.exec_statement(stmt)?;
5529            } else {
5530                return match &stmt.kind {
5531                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
5532                    _ => self.exec_statement(stmt),
5533                };
5534            }
5535        }
5536        Ok(PerlValue::UNDEF)
5537    }
5538
5539    /// Spawn `block` on a worker thread; returns an [`PerlValue::AsyncTask`] handle (`async { }` / `spawn { }`).
5540    pub(crate) fn spawn_async_block(&self, block: &Block) -> PerlValue {
5541        use parking_lot::Mutex as ParkMutex;
5542
5543        let block = block.clone();
5544        let subs = self.subs.clone();
5545        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5546        let result = Arc::new(ParkMutex::new(None));
5547        let join = Arc::new(ParkMutex::new(None));
5548        let result2 = result.clone();
5549        let h = std::thread::spawn(move || {
5550            let mut interp = Interpreter::new();
5551            interp.subs = subs;
5552            interp.scope.restore_capture(&scalars);
5553            interp.scope.restore_atomics(&aar, &ahash);
5554            interp.enable_parallel_guard();
5555            let r = match interp.exec_block(&block) {
5556                Ok(v) => Ok(v),
5557                Err(FlowOrError::Error(e)) => Err(e),
5558                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5559                    Err(PerlError::runtime("yield inside async/spawn block", 0))
5560                }
5561                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5562            };
5563            *result2.lock() = Some(r);
5564        });
5565        *join.lock() = Some(h);
5566        PerlValue::async_task(Arc::new(PerlAsyncTask { result, join }))
5567    }
5568
5569    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
5570    pub(crate) fn eval_timeout_block(
5571        &mut self,
5572        body: &Block,
5573        secs: f64,
5574        line: usize,
5575    ) -> ExecResult {
5576        use std::sync::mpsc::channel;
5577        use std::time::Duration;
5578
5579        let block = body.clone();
5580        let subs = self.subs.clone();
5581        let struct_defs = self.struct_defs.clone();
5582        let enum_defs = self.enum_defs.clone();
5583        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5584        self.materialize_env_if_needed();
5585        let env = self.env.clone();
5586        let argv = self.argv.clone();
5587        let inc = self.scope.get_array("INC");
5588        let (tx, rx) = channel::<PerlResult<PerlValue>>();
5589        let _handle = std::thread::spawn(move || {
5590            let mut interp = Interpreter::new();
5591            interp.subs = subs;
5592            interp.struct_defs = struct_defs;
5593            interp.enum_defs = enum_defs;
5594            interp.env = env.clone();
5595            interp.argv = argv.clone();
5596            interp.scope.declare_array(
5597                "ARGV",
5598                argv.iter().map(|s| PerlValue::string(s.clone())).collect(),
5599            );
5600            for (k, v) in env {
5601                interp
5602                    .scope
5603                    .set_hash_element("ENV", &k, v)
5604                    .expect("set ENV in timeout thread");
5605            }
5606            interp.scope.declare_array("INC", inc);
5607            interp.scope.restore_capture(&scalars);
5608            interp.scope.restore_atomics(&aar, &ahash);
5609            interp.enable_parallel_guard();
5610            let out: PerlResult<PerlValue> = match interp.exec_block(&block) {
5611                Ok(v) => Ok(v),
5612                Err(FlowOrError::Error(e)) => Err(e),
5613                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5614                    Err(PerlError::runtime("yield inside eval_timeout block", 0))
5615                }
5616                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5617            };
5618            let _ = tx.send(out);
5619        });
5620        let dur = Duration::from_secs_f64(secs.max(0.0));
5621        match rx.recv_timeout(dur) {
5622            Ok(Ok(v)) => Ok(v),
5623            Ok(Err(e)) => Err(FlowOrError::Error(e)),
5624            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(PerlError::runtime(
5625                format!(
5626                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
5627                    secs
5628                ),
5629                line,
5630            )
5631            .into()),
5632            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(PerlError::runtime(
5633                "eval_timeout: worker thread panicked or disconnected",
5634                line,
5635            )
5636            .into()),
5637        }
5638    }
5639
5640    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
5641        let mut last = PerlValue::UNDEF;
5642        for stmt in body {
5643            match &stmt.kind {
5644                StmtKind::When { cond, body: wb } => {
5645                    if self.when_matches(cond)? {
5646                        return self.exec_block_smart(wb);
5647                    }
5648                }
5649                StmtKind::DefaultCase { body: db } => {
5650                    return self.exec_block_smart(db);
5651                }
5652                _ => {
5653                    last = self.exec_statement(stmt)?;
5654                }
5655            }
5656        }
5657        Ok(last)
5658    }
5659
5660    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
5661    pub(crate) fn exec_given_with_topic_value(
5662        &mut self,
5663        topic: PerlValue,
5664        body: &Block,
5665    ) -> ExecResult {
5666        self.scope_push_hook();
5667        self.scope.declare_scalar("_", topic);
5668        self.english_note_lexical_scalar("_");
5669        let r = self.exec_given_body(body);
5670        self.scope_pop_hook();
5671        r
5672    }
5673
5674    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
5675        let t = self.eval_expr(topic)?;
5676        self.exec_given_with_topic_value(t, body)
5677    }
5678
5679    /// `when (COND)` — topic is `$_` (set by `given`).
5680    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
5681        let topic = self.scope.get_scalar("_");
5682        let line = cond.line;
5683        match &cond.kind {
5684            ExprKind::Regex(pattern, flags) => {
5685                let re = self.compile_regex(pattern, flags, line)?;
5686                let s = topic.to_string();
5687                Ok(re.is_match(&s))
5688            }
5689            ExprKind::String(s) => Ok(topic.to_string() == *s),
5690            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
5691            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
5692            _ => {
5693                let c = self.eval_expr(cond)?;
5694                Ok(self.smartmatch_when(&topic, &c))
5695            }
5696        }
5697    }
5698
5699    fn smartmatch_when(&self, topic: &PerlValue, c: &PerlValue) -> bool {
5700        if let Some(re) = c.as_regex() {
5701            return re.is_match(&topic.to_string());
5702        }
5703        topic.to_string() == c.to_string()
5704    }
5705
5706    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
5707    pub(crate) fn eval_boolean_rvalue_condition(
5708        &mut self,
5709        cond: &Expr,
5710    ) -> Result<bool, FlowOrError> {
5711        match &cond.kind {
5712            ExprKind::Regex(pattern, flags) => {
5713                let topic = self.scope.get_scalar("_");
5714                let line = cond.line;
5715                let s = topic.to_string();
5716                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
5717                Ok(v.is_true())
5718            }
5719            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
5720            ExprKind::ReadLine(_) => {
5721                let v = self.eval_expr(cond)?;
5722                self.scope.set_topic(v.clone());
5723                Ok(!v.is_undef())
5724            }
5725            _ => {
5726                let v = self.eval_expr(cond)?;
5727                Ok(v.is_true())
5728            }
5729        }
5730    }
5731
5732    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
5733    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
5734        self.eval_boolean_rvalue_condition(cond)
5735    }
5736
5737    pub(crate) fn eval_algebraic_match(
5738        &mut self,
5739        subject: &Expr,
5740        arms: &[MatchArm],
5741        line: usize,
5742    ) -> ExecResult {
5743        let val = self.eval_algebraic_match_subject(subject, line)?;
5744        self.eval_algebraic_match_with_subject_value(val, arms, line)
5745    }
5746
5747    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
5748    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
5749        match &subject.kind {
5750            ExprKind::ArrayVar(name) => {
5751                self.check_strict_array_var(name, line)?;
5752                let aname = self.stash_array_name_for_package(name);
5753                Ok(PerlValue::array_binding_ref(aname))
5754            }
5755            ExprKind::HashVar(name) => {
5756                self.check_strict_hash_var(name, line)?;
5757                self.touch_env_hash(name);
5758                Ok(PerlValue::hash_binding_ref(name.clone()))
5759            }
5760            _ => self.eval_expr(subject),
5761        }
5762    }
5763
5764    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
5765    pub(crate) fn eval_algebraic_match_with_subject_value(
5766        &mut self,
5767        val: PerlValue,
5768        arms: &[MatchArm],
5769        line: usize,
5770    ) -> ExecResult {
5771        // Exhaustive enum match: check variant coverage before matching
5772        if let Some(e) = val.as_enum_inst() {
5773            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
5774            if !has_catchall {
5775                let covered: Vec<String> = arms
5776                    .iter()
5777                    .filter_map(|a| {
5778                        if let MatchPattern::Value(expr) = &a.pattern {
5779                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
5780                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
5781                            }
5782                        }
5783                        None
5784                    })
5785                    .collect();
5786                let missing: Vec<&str> = e
5787                    .def
5788                    .variants
5789                    .iter()
5790                    .filter(|v| !covered.contains(&v.name))
5791                    .map(|v| v.name.as_str())
5792                    .collect();
5793                if !missing.is_empty() {
5794                    return Err(PerlError::runtime(
5795                        format!(
5796                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
5797                            e.def.name,
5798                            missing.join(", ")
5799                        ),
5800                        line,
5801                    )
5802                    .into());
5803                }
5804            }
5805        }
5806        for arm in arms {
5807            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
5808                let re = self.compile_regex(pattern, flags, line)?;
5809                let s = val.to_string();
5810                if let Some(caps) = re.captures(&s) {
5811                    self.scope_push_hook();
5812                    self.scope.declare_scalar("_", val.clone());
5813                    self.english_note_lexical_scalar("_");
5814                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
5815                    let guard_ok = if let Some(g) = &arm.guard {
5816                        self.eval_expr(g)?.is_true()
5817                    } else {
5818                        true
5819                    };
5820                    if !guard_ok {
5821                        self.scope_pop_hook();
5822                        continue;
5823                    }
5824                    let out = self.eval_expr(&arm.body);
5825                    self.scope_pop_hook();
5826                    return out;
5827                }
5828                continue;
5829            }
5830            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
5831                self.scope_push_hook();
5832                self.scope.declare_scalar("_", val.clone());
5833                self.english_note_lexical_scalar("_");
5834                for b in bindings {
5835                    match b {
5836                        PatternBinding::Scalar(name, v) => {
5837                            self.scope.declare_scalar(&name, v);
5838                            self.english_note_lexical_scalar(&name);
5839                        }
5840                        PatternBinding::Array(name, elems) => {
5841                            self.scope.declare_array(&name, elems);
5842                        }
5843                    }
5844                }
5845                let guard_ok = if let Some(g) = &arm.guard {
5846                    self.eval_expr(g)?.is_true()
5847                } else {
5848                    true
5849                };
5850                if !guard_ok {
5851                    self.scope_pop_hook();
5852                    continue;
5853                }
5854                let out = self.eval_expr(&arm.body);
5855                self.scope_pop_hook();
5856                return out;
5857            }
5858        }
5859        Err(PerlError::runtime(
5860            "match: no arm matched the value (add a `_` catch-all)",
5861            line,
5862        )
5863        .into())
5864    }
5865
5866    fn parse_duration_seconds(pv: &PerlValue) -> Option<f64> {
5867        let s = pv.to_string();
5868        let s = s.trim();
5869        if let Some(rest) = s.strip_suffix("ms") {
5870            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
5871        }
5872        if let Some(rest) = s.strip_suffix('s') {
5873            return rest.trim().parse::<f64>().ok();
5874        }
5875        if let Some(rest) = s.strip_suffix('m') {
5876            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
5877        }
5878        s.parse::<f64>().ok()
5879    }
5880
5881    fn eval_retry_block(
5882        &mut self,
5883        body: &Block,
5884        times: &Expr,
5885        backoff: RetryBackoff,
5886        _line: usize,
5887    ) -> ExecResult {
5888        let max = self.eval_expr(times)?.to_int().max(1) as usize;
5889        let base_ms: u64 = 10;
5890        let mut attempt = 0usize;
5891        loop {
5892            attempt += 1;
5893            match self.exec_block(body) {
5894                Ok(v) => return Ok(v),
5895                Err(FlowOrError::Error(e)) => {
5896                    if attempt >= max {
5897                        return Err(FlowOrError::Error(e));
5898                    }
5899                    let delay_ms = match backoff {
5900                        RetryBackoff::None => 0,
5901                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
5902                        RetryBackoff::Exponential => {
5903                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
5904                        }
5905                    };
5906                    if delay_ms > 0 {
5907                        std::thread::sleep(Duration::from_millis(delay_ms));
5908                    }
5909                }
5910                Err(e) => return Err(e),
5911            }
5912        }
5913    }
5914
5915    fn eval_rate_limit_block(
5916        &mut self,
5917        slot: u32,
5918        max: &Expr,
5919        window: &Expr,
5920        body: &Block,
5921        _line: usize,
5922    ) -> ExecResult {
5923        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
5924        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
5925            .filter(|s| *s > 0.0)
5926            .unwrap_or(1.0);
5927        let window_d = Duration::from_secs_f64(window_sec);
5928        let slot = slot as usize;
5929        while self.rate_limit_slots.len() <= slot {
5930            self.rate_limit_slots.push(VecDeque::new());
5931        }
5932        {
5933            let dq = &mut self.rate_limit_slots[slot];
5934            loop {
5935                let now = Instant::now();
5936                while let Some(t0) = dq.front().copied() {
5937                    if now.duration_since(t0) >= window_d {
5938                        dq.pop_front();
5939                    } else {
5940                        break;
5941                    }
5942                }
5943                if dq.len() < max_n || max_n == 0 {
5944                    break;
5945                }
5946                let t0 = dq.front().copied().unwrap();
5947                let wait = window_d.saturating_sub(now.duration_since(t0));
5948                if wait.is_zero() {
5949                    dq.pop_front();
5950                    continue;
5951                }
5952                std::thread::sleep(wait);
5953            }
5954            dq.push_back(Instant::now());
5955        }
5956        self.exec_block(body)
5957    }
5958
5959    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
5960        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
5961            .filter(|s| *s > 0.0)
5962            .unwrap_or(1.0);
5963        loop {
5964            match self.exec_block(body) {
5965                Ok(_) => {}
5966                Err(e) => return Err(e),
5967            }
5968            std::thread::sleep(Duration::from_secs_f64(sec));
5969        }
5970    }
5971
5972    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
5973    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> PerlResult<PerlValue> {
5974        let pair = |value: PerlValue, more: i64| {
5975            PerlValue::array_ref(Arc::new(RwLock::new(vec![value, PerlValue::integer(more)])))
5976        };
5977        let mut exhausted = gen.exhausted.lock();
5978        if *exhausted {
5979            return Ok(pair(PerlValue::UNDEF, 0));
5980        }
5981        let mut pc = gen.pc.lock();
5982        let mut scope_started = gen.scope_started.lock();
5983        if *pc >= gen.block.len() {
5984            if *scope_started {
5985                self.scope_pop_hook();
5986                *scope_started = false;
5987            }
5988            *exhausted = true;
5989            return Ok(pair(PerlValue::UNDEF, 0));
5990        }
5991        if !*scope_started {
5992            self.scope_push_hook();
5993            *scope_started = true;
5994        }
5995        self.in_generator = true;
5996        while *pc < gen.block.len() {
5997            let stmt = &gen.block[*pc];
5998            match self.exec_statement(stmt) {
5999                Ok(_) => {
6000                    *pc += 1;
6001                }
6002                Err(FlowOrError::Flow(Flow::Yield(v))) => {
6003                    *pc += 1;
6004                    self.in_generator = false;
6005                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
6006                    // binds in the caller block, not inside a frame left across yield.
6007                    if *scope_started {
6008                        self.scope_pop_hook();
6009                        *scope_started = false;
6010                    }
6011                    return Ok(pair(v, 1));
6012                }
6013                Err(e) => {
6014                    self.in_generator = false;
6015                    if *scope_started {
6016                        self.scope_pop_hook();
6017                        *scope_started = false;
6018                    }
6019                    return Err(match e {
6020                        FlowOrError::Error(ee) => ee,
6021                        FlowOrError::Flow(Flow::Yield(_)) => {
6022                            unreachable!("yield handled above")
6023                        }
6024                        FlowOrError::Flow(flow) => PerlError::runtime(
6025                            format!("unexpected control flow in generator: {:?}", flow),
6026                            0,
6027                        ),
6028                    });
6029                }
6030            }
6031        }
6032        self.in_generator = false;
6033        if *scope_started {
6034            self.scope_pop_hook();
6035            *scope_started = false;
6036        }
6037        *exhausted = true;
6038        Ok(pair(PerlValue::UNDEF, 0))
6039    }
6040
6041    fn match_pattern_try(
6042        &mut self,
6043        subject: &PerlValue,
6044        pattern: &MatchPattern,
6045        line: usize,
6046    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6047        match pattern {
6048            MatchPattern::Any => Ok(Some(vec![])),
6049            MatchPattern::Regex { .. } => {
6050                unreachable!("regex arms are handled in eval_algebraic_match")
6051            }
6052            MatchPattern::Value(expr) => {
6053                let pv = self.eval_expr(expr)?;
6054                if self.smartmatch_when(subject, &pv) {
6055                    Ok(Some(vec![]))
6056                } else {
6057                    Ok(None)
6058                }
6059            }
6060            MatchPattern::Array(elems) => {
6061                let Some(arr) = self.match_subject_as_array(subject) else {
6062                    return Ok(None);
6063                };
6064                self.match_array_pattern_elems(&arr, elems, line)
6065            }
6066            MatchPattern::Hash(pairs) => {
6067                let Some(h) = self.match_subject_as_hash(subject) else {
6068                    return Ok(None);
6069                };
6070                self.match_hash_pattern_pairs(&h, pairs, line)
6071            }
6072            MatchPattern::OptionSome(name) => {
6073                let Some(arr) = self.match_subject_as_array(subject) else {
6074                    return Ok(None);
6075                };
6076                if arr.len() < 2 {
6077                    return Ok(None);
6078                }
6079                if !arr[1].is_true() {
6080                    return Ok(None);
6081                }
6082                Ok(Some(vec![PatternBinding::Scalar(
6083                    name.clone(),
6084                    arr[0].clone(),
6085                )]))
6086            }
6087        }
6088    }
6089
6090    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
6091    fn match_subject_as_array(&self, v: &PerlValue) -> Option<Vec<PerlValue>> {
6092        if let Some(a) = v.as_array_vec() {
6093            return Some(a);
6094        }
6095        if let Some(r) = v.as_array_ref() {
6096            return Some(r.read().clone());
6097        }
6098        if let Some(name) = v.as_array_binding_name() {
6099            return Some(self.scope.get_array(&name));
6100        }
6101        None
6102    }
6103
6104    fn match_subject_as_hash(&mut self, v: &PerlValue) -> Option<IndexMap<String, PerlValue>> {
6105        if let Some(h) = v.as_hash_map() {
6106            return Some(h);
6107        }
6108        if let Some(r) = v.as_hash_ref() {
6109            return Some(r.read().clone());
6110        }
6111        if let Some(name) = v.as_hash_binding_name() {
6112            self.touch_env_hash(&name);
6113            return Some(self.scope.get_hash(&name));
6114        }
6115        None
6116    }
6117
6118    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
6119    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
6120    pub(crate) fn hash_slice_deref_values(
6121        &mut self,
6122        container: &PerlValue,
6123        key_values: &[PerlValue],
6124        line: usize,
6125    ) -> Result<PerlValue, FlowOrError> {
6126        let h = if let Some(m) = self.match_subject_as_hash(container) {
6127            m
6128        } else {
6129            return Err(PerlError::runtime(
6130                "Hash slice dereference needs a hash or hash reference value",
6131                line,
6132            )
6133            .into());
6134        };
6135        let mut result = Vec::new();
6136        for kv in key_values {
6137            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6138                vv.iter().map(|x| x.to_string()).collect()
6139            } else {
6140                vec![kv.to_string()]
6141            };
6142            for k in key_strings {
6143                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6144            }
6145        }
6146        Ok(PerlValue::array(result))
6147    }
6148
6149    /// Single-key write for a hash slice container (hash ref or package hash name).
6150    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
6151    pub(crate) fn assign_hash_slice_one_key(
6152        &mut self,
6153        container: PerlValue,
6154        key: &str,
6155        val: PerlValue,
6156        line: usize,
6157    ) -> Result<PerlValue, FlowOrError> {
6158        if let Some(r) = container.as_hash_ref() {
6159            r.write().insert(key.to_string(), val);
6160            return Ok(PerlValue::UNDEF);
6161        }
6162        if let Some(name) = container.as_hash_binding_name() {
6163            self.touch_env_hash(&name);
6164            self.scope
6165                .set_hash_element(&name, key, val)
6166                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6167            return Ok(PerlValue::UNDEF);
6168        }
6169        if let Some(s) = container.as_str() {
6170            self.touch_env_hash(&s);
6171            if self.strict_refs {
6172                return Err(PerlError::runtime(
6173                    format!(
6174                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6175                        s
6176                    ),
6177                    line,
6178                )
6179                .into());
6180            }
6181            self.scope
6182                .set_hash_element(&s, key, val)
6183                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6184            return Ok(PerlValue::UNDEF);
6185        }
6186        Err(PerlError::runtime(
6187            "Hash slice assignment needs a hash or hash reference value",
6188            line,
6189        )
6190        .into())
6191    }
6192
6193    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
6194    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
6195    pub(crate) fn assign_named_hash_slice(
6196        &mut self,
6197        hash: &str,
6198        key_values: Vec<PerlValue>,
6199        val: PerlValue,
6200        line: usize,
6201    ) -> Result<PerlValue, FlowOrError> {
6202        self.touch_env_hash(hash);
6203        let mut ks: Vec<String> = Vec::new();
6204        for kv in key_values {
6205            if let Some(vv) = kv.as_array_vec() {
6206                ks.extend(vv.iter().map(|x| x.to_string()));
6207            } else {
6208                ks.push(kv.to_string());
6209            }
6210        }
6211        if ks.is_empty() {
6212            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6213        }
6214        let items = val.to_list();
6215        for (i, k) in ks.iter().enumerate() {
6216            let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6217            self.scope
6218                .set_hash_element(hash, k, v)
6219                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6220        }
6221        Ok(PerlValue::UNDEF)
6222    }
6223
6224    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
6225    pub(crate) fn assign_hash_slice_deref(
6226        &mut self,
6227        container: PerlValue,
6228        key_values: Vec<PerlValue>,
6229        val: PerlValue,
6230        line: usize,
6231    ) -> Result<PerlValue, FlowOrError> {
6232        let mut ks: Vec<String> = Vec::new();
6233        for kv in key_values {
6234            if let Some(vv) = kv.as_array_vec() {
6235                ks.extend(vv.iter().map(|x| x.to_string()));
6236            } else {
6237                ks.push(kv.to_string());
6238            }
6239        }
6240        if ks.is_empty() {
6241            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6242        }
6243        let items = val.to_list();
6244        if let Some(r) = container.as_hash_ref() {
6245            let mut h = r.write();
6246            for (i, k) in ks.iter().enumerate() {
6247                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6248                h.insert(k.clone(), v);
6249            }
6250            return Ok(PerlValue::UNDEF);
6251        }
6252        if let Some(name) = container.as_hash_binding_name() {
6253            self.touch_env_hash(&name);
6254            for (i, k) in ks.iter().enumerate() {
6255                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6256                self.scope
6257                    .set_hash_element(&name, k, v)
6258                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6259            }
6260            return Ok(PerlValue::UNDEF);
6261        }
6262        if let Some(s) = container.as_str() {
6263            if self.strict_refs {
6264                return Err(PerlError::runtime(
6265                    format!(
6266                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6267                        s
6268                    ),
6269                    line,
6270                )
6271                .into());
6272            }
6273            self.touch_env_hash(&s);
6274            for (i, k) in ks.iter().enumerate() {
6275                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6276                self.scope
6277                    .set_hash_element(&s, k, v)
6278                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6279            }
6280            return Ok(PerlValue::UNDEF);
6281        }
6282        Err(PerlError::runtime(
6283            "Hash slice assignment needs a hash or hash reference value",
6284            line,
6285        )
6286        .into())
6287    }
6288
6289    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
6290    /// Perl 5 applies the compound op only to the **last** slice element.
6291    pub(crate) fn compound_assign_hash_slice_deref(
6292        &mut self,
6293        container: PerlValue,
6294        key_values: Vec<PerlValue>,
6295        op: BinOp,
6296        rhs: PerlValue,
6297        line: usize,
6298    ) -> Result<PerlValue, FlowOrError> {
6299        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6300        let last_old = old_list
6301            .to_list()
6302            .last()
6303            .cloned()
6304            .unwrap_or(PerlValue::UNDEF);
6305        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6306        let mut ks: Vec<String> = Vec::new();
6307        for kv in &key_values {
6308            if let Some(vv) = kv.as_array_vec() {
6309                ks.extend(vv.iter().map(|x| x.to_string()));
6310            } else {
6311                ks.push(kv.to_string());
6312            }
6313        }
6314        if ks.is_empty() {
6315            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6316        }
6317        let last_key = ks.last().expect("non-empty ks");
6318        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6319        Ok(new_val)
6320    }
6321
6322    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
6323    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
6324    /// the **old** value of that last element.
6325    ///
6326    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
6327    pub(crate) fn hash_slice_deref_inc_dec(
6328        &mut self,
6329        container: PerlValue,
6330        key_values: Vec<PerlValue>,
6331        kind: u8,
6332        line: usize,
6333    ) -> Result<PerlValue, FlowOrError> {
6334        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6335        let last_old = old_list
6336            .to_list()
6337            .last()
6338            .cloned()
6339            .unwrap_or(PerlValue::UNDEF);
6340        let new_val = if kind & 1 == 0 {
6341            PerlValue::integer(last_old.to_int() + 1)
6342        } else {
6343            PerlValue::integer(last_old.to_int() - 1)
6344        };
6345        let mut ks: Vec<String> = Vec::new();
6346        for kv in &key_values {
6347            if let Some(vv) = kv.as_array_vec() {
6348                ks.extend(vv.iter().map(|x| x.to_string()));
6349            } else {
6350                ks.push(kv.to_string());
6351            }
6352        }
6353        let last_key = ks.last().ok_or_else(|| {
6354            PerlError::runtime("Hash slice increment needs at least one key", line)
6355        })?;
6356        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6357        Ok(if kind < 2 { new_val } else { last_old })
6358    }
6359
6360    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[PerlValue]) -> PerlValue {
6361        self.touch_env_hash(hash);
6362        let h = self.scope.get_hash(hash);
6363        let mut result = Vec::new();
6364        for kv in key_values {
6365            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6366                vv.iter().map(|x| x.to_string()).collect()
6367            } else {
6368                vec![kv.to_string()]
6369            };
6370            for k in key_strings {
6371                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6372            }
6373        }
6374        PerlValue::array(result)
6375    }
6376
6377    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
6378    pub(crate) fn compound_assign_named_hash_slice(
6379        &mut self,
6380        hash: &str,
6381        key_values: Vec<PerlValue>,
6382        op: BinOp,
6383        rhs: PerlValue,
6384        line: usize,
6385    ) -> Result<PerlValue, FlowOrError> {
6386        let old_list = self.hash_slice_named_values(hash, &key_values);
6387        let last_old = old_list
6388            .to_list()
6389            .last()
6390            .cloned()
6391            .unwrap_or(PerlValue::UNDEF);
6392        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6393        let mut ks: Vec<String> = Vec::new();
6394        for kv in &key_values {
6395            if let Some(vv) = kv.as_array_vec() {
6396                ks.extend(vv.iter().map(|x| x.to_string()));
6397            } else {
6398                ks.push(kv.to_string());
6399            }
6400        }
6401        if ks.is_empty() {
6402            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6403        }
6404        let last_key = ks.last().expect("non-empty ks");
6405        let container = PerlValue::string(hash.to_string());
6406        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6407        Ok(new_val)
6408    }
6409
6410    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
6411    pub(crate) fn named_hash_slice_inc_dec(
6412        &mut self,
6413        hash: &str,
6414        key_values: Vec<PerlValue>,
6415        kind: u8,
6416        line: usize,
6417    ) -> Result<PerlValue, FlowOrError> {
6418        let old_list = self.hash_slice_named_values(hash, &key_values);
6419        let last_old = old_list
6420            .to_list()
6421            .last()
6422            .cloned()
6423            .unwrap_or(PerlValue::UNDEF);
6424        let new_val = if kind & 1 == 0 {
6425            PerlValue::integer(last_old.to_int() + 1)
6426        } else {
6427            PerlValue::integer(last_old.to_int() - 1)
6428        };
6429        let mut ks: Vec<String> = Vec::new();
6430        for kv in &key_values {
6431            if let Some(vv) = kv.as_array_vec() {
6432                ks.extend(vv.iter().map(|x| x.to_string()));
6433            } else {
6434                ks.push(kv.to_string());
6435            }
6436        }
6437        let last_key = ks.last().ok_or_else(|| {
6438            PerlError::runtime("Hash slice increment needs at least one key", line)
6439        })?;
6440        let container = PerlValue::string(hash.to_string());
6441        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6442        Ok(if kind < 2 { new_val } else { last_old })
6443    }
6444
6445    fn match_array_pattern_elems(
6446        &mut self,
6447        arr: &[PerlValue],
6448        elems: &[MatchArrayElem],
6449        line: usize,
6450    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6451        let has_rest = elems
6452            .iter()
6453            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
6454        let mut binds: Vec<PatternBinding> = Vec::new();
6455        let mut idx = 0usize;
6456        for (i, elem) in elems.iter().enumerate() {
6457            match elem {
6458                MatchArrayElem::Rest => {
6459                    if i != elems.len() - 1 {
6460                        return Err(PerlError::runtime(
6461                            "internal: `*` must be last in array match pattern",
6462                            line,
6463                        )
6464                        .into());
6465                    }
6466                    return Ok(Some(binds));
6467                }
6468                MatchArrayElem::RestBind(name) => {
6469                    if i != elems.len() - 1 {
6470                        return Err(PerlError::runtime(
6471                            "internal: `@name` rest bind must be last in array match pattern",
6472                            line,
6473                        )
6474                        .into());
6475                    }
6476                    let tail = arr[idx..].to_vec();
6477                    binds.push(PatternBinding::Array(name.clone(), tail));
6478                    return Ok(Some(binds));
6479                }
6480                MatchArrayElem::CaptureScalar(name) => {
6481                    if idx >= arr.len() {
6482                        return Ok(None);
6483                    }
6484                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
6485                    idx += 1;
6486                }
6487                MatchArrayElem::Expr(e) => {
6488                    if idx >= arr.len() {
6489                        return Ok(None);
6490                    }
6491                    let expected = self.eval_expr(e)?;
6492                    if !self.smartmatch_when(&arr[idx], &expected) {
6493                        return Ok(None);
6494                    }
6495                    idx += 1;
6496                }
6497            }
6498        }
6499        if !has_rest && idx != arr.len() {
6500            return Ok(None);
6501        }
6502        Ok(Some(binds))
6503    }
6504
6505    fn match_hash_pattern_pairs(
6506        &mut self,
6507        h: &IndexMap<String, PerlValue>,
6508        pairs: &[MatchHashPair],
6509        _line: usize,
6510    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6511        let mut binds = Vec::new();
6512        for pair in pairs {
6513            match pair {
6514                MatchHashPair::KeyOnly { key } => {
6515                    let ks = self.eval_expr(key)?.to_string();
6516                    if !h.contains_key(&ks) {
6517                        return Ok(None);
6518                    }
6519                }
6520                MatchHashPair::Capture { key, name } => {
6521                    let ks = self.eval_expr(key)?.to_string();
6522                    let Some(v) = h.get(&ks) else {
6523                        return Ok(None);
6524                    };
6525                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
6526                }
6527            }
6528        }
6529        Ok(Some(binds))
6530    }
6531
6532    /// Check if a block declares variables (needs its own scope frame).
6533    #[inline]
6534    fn block_needs_scope(block: &Block) -> bool {
6535        block.iter().any(|s| match &s.kind {
6536            StmtKind::My(_)
6537            | StmtKind::Our(_)
6538            | StmtKind::Local(_)
6539            | StmtKind::State(_)
6540            | StmtKind::LocalExpr { .. } => true,
6541            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
6542            _ => false,
6543        })
6544    }
6545
6546    /// Execute block, only pushing a scope frame if needed.
6547    #[inline]
6548    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
6549        if Self::block_needs_scope(block) {
6550            self.exec_block(block)
6551        } else {
6552            self.exec_block_no_scope(block)
6553        }
6554    }
6555
6556    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
6557        let t0 = self.profiler.is_some().then(std::time::Instant::now);
6558        let r = self.exec_statement_inner(stmt);
6559        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
6560            prof.on_line(&self.file, stmt.line, t0.elapsed());
6561        }
6562        r
6563    }
6564
6565    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
6566        if let Err(e) = crate::perl_signal::poll(self) {
6567            return Err(FlowOrError::Error(e));
6568        }
6569        if let Err(e) = self.drain_pending_destroys(stmt.line) {
6570            return Err(FlowOrError::Error(e));
6571        }
6572        match &stmt.kind {
6573            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
6574            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
6575            StmtKind::If {
6576                condition,
6577                body,
6578                elsifs,
6579                else_block,
6580            } => {
6581                if self.eval_boolean_rvalue_condition(condition)? {
6582                    return self.exec_block(body);
6583                }
6584                for (c, b) in elsifs {
6585                    if self.eval_boolean_rvalue_condition(c)? {
6586                        return self.exec_block(b);
6587                    }
6588                }
6589                if let Some(eb) = else_block {
6590                    return self.exec_block(eb);
6591                }
6592                Ok(PerlValue::UNDEF)
6593            }
6594            StmtKind::Unless {
6595                condition,
6596                body,
6597                else_block,
6598            } => {
6599                if !self.eval_boolean_rvalue_condition(condition)? {
6600                    return self.exec_block(body);
6601                }
6602                if let Some(eb) = else_block {
6603                    return self.exec_block(eb);
6604                }
6605                Ok(PerlValue::UNDEF)
6606            }
6607            StmtKind::While {
6608                condition,
6609                body,
6610                label,
6611                continue_block,
6612            } => {
6613                'outer: loop {
6614                    if !self.eval_boolean_rvalue_condition(condition)? {
6615                        break;
6616                    }
6617                    'inner: loop {
6618                        match self.exec_block_smart(body) {
6619                            Ok(_) => break 'inner,
6620                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6621                                if l == label || l.is_none() =>
6622                            {
6623                                break 'outer;
6624                            }
6625                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6626                                if l == label || l.is_none() =>
6627                            {
6628                                if let Some(cb) = continue_block {
6629                                    let _ = self.exec_block_smart(cb);
6630                                }
6631                                continue 'outer;
6632                            }
6633                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6634                                if l == label || l.is_none() =>
6635                            {
6636                                continue 'inner;
6637                            }
6638                            Err(e) => return Err(e),
6639                        }
6640                    }
6641                    if let Some(cb) = continue_block {
6642                        let _ = self.exec_block_smart(cb);
6643                    }
6644                }
6645                Ok(PerlValue::UNDEF)
6646            }
6647            StmtKind::Until {
6648                condition,
6649                body,
6650                label,
6651                continue_block,
6652            } => {
6653                'outer: loop {
6654                    if self.eval_boolean_rvalue_condition(condition)? {
6655                        break;
6656                    }
6657                    'inner: loop {
6658                        match self.exec_block(body) {
6659                            Ok(_) => break 'inner,
6660                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6661                                if l == label || l.is_none() =>
6662                            {
6663                                break 'outer;
6664                            }
6665                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6666                                if l == label || l.is_none() =>
6667                            {
6668                                if let Some(cb) = continue_block {
6669                                    let _ = self.exec_block_smart(cb);
6670                                }
6671                                continue 'outer;
6672                            }
6673                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6674                                if l == label || l.is_none() =>
6675                            {
6676                                continue 'inner;
6677                            }
6678                            Err(e) => return Err(e),
6679                        }
6680                    }
6681                    if let Some(cb) = continue_block {
6682                        let _ = self.exec_block_smart(cb);
6683                    }
6684                }
6685                Ok(PerlValue::UNDEF)
6686            }
6687            StmtKind::DoWhile { body, condition } => {
6688                loop {
6689                    self.exec_block(body)?;
6690                    if !self.eval_boolean_rvalue_condition(condition)? {
6691                        break;
6692                    }
6693                }
6694                Ok(PerlValue::UNDEF)
6695            }
6696            StmtKind::For {
6697                init,
6698                condition,
6699                step,
6700                body,
6701                label,
6702                continue_block,
6703            } => {
6704                self.scope_push_hook();
6705                if let Some(init) = init {
6706                    self.exec_statement(init)?;
6707                }
6708                'outer: loop {
6709                    if let Some(cond) = condition {
6710                        if !self.eval_boolean_rvalue_condition(cond)? {
6711                            break;
6712                        }
6713                    }
6714                    'inner: loop {
6715                        match self.exec_block_smart(body) {
6716                            Ok(_) => break 'inner,
6717                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6718                                if l == label || l.is_none() =>
6719                            {
6720                                break 'outer;
6721                            }
6722                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6723                                if l == label || l.is_none() =>
6724                            {
6725                                if let Some(cb) = continue_block {
6726                                    let _ = self.exec_block_smart(cb);
6727                                }
6728                                if let Some(step) = step {
6729                                    self.eval_expr(step)?;
6730                                }
6731                                continue 'outer;
6732                            }
6733                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6734                                if l == label || l.is_none() =>
6735                            {
6736                                continue 'inner;
6737                            }
6738                            Err(e) => {
6739                                self.scope_pop_hook();
6740                                return Err(e);
6741                            }
6742                        }
6743                    }
6744                    if let Some(cb) = continue_block {
6745                        let _ = self.exec_block_smart(cb);
6746                    }
6747                    if let Some(step) = step {
6748                        self.eval_expr(step)?;
6749                    }
6750                }
6751                self.scope_pop_hook();
6752                Ok(PerlValue::UNDEF)
6753            }
6754            StmtKind::Foreach {
6755                var,
6756                list,
6757                body,
6758                label,
6759                continue_block,
6760            } => {
6761                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
6762                let items = list_val.to_list();
6763                self.scope_push_hook();
6764                self.scope.declare_scalar(var, PerlValue::UNDEF);
6765                self.english_note_lexical_scalar(var);
6766                let mut i = 0usize;
6767                'outer: while i < items.len() {
6768                    self.scope
6769                        .set_scalar(var, items[i].clone())
6770                        .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
6771                    'inner: loop {
6772                        match self.exec_block_smart(body) {
6773                            Ok(_) => break 'inner,
6774                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6775                                if l == label || l.is_none() =>
6776                            {
6777                                break 'outer;
6778                            }
6779                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6780                                if l == label || l.is_none() =>
6781                            {
6782                                if let Some(cb) = continue_block {
6783                                    let _ = self.exec_block_smart(cb);
6784                                }
6785                                i += 1;
6786                                continue 'outer;
6787                            }
6788                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6789                                if l == label || l.is_none() =>
6790                            {
6791                                continue 'inner;
6792                            }
6793                            Err(e) => {
6794                                self.scope_pop_hook();
6795                                return Err(e);
6796                            }
6797                        }
6798                    }
6799                    if let Some(cb) = continue_block {
6800                        let _ = self.exec_block_smart(cb);
6801                    }
6802                    i += 1;
6803                }
6804                self.scope_pop_hook();
6805                Ok(PerlValue::UNDEF)
6806            }
6807            StmtKind::SubDecl {
6808                name,
6809                params,
6810                body,
6811                prototype,
6812            } => {
6813                let key = self.qualify_sub_key(name);
6814                let captured = self.scope.capture();
6815                let closure_env = if captured.is_empty() {
6816                    None
6817                } else {
6818                    Some(captured)
6819                };
6820                let mut sub = PerlSub {
6821                    name: name.clone(),
6822                    params: params.clone(),
6823                    body: body.clone(),
6824                    closure_env,
6825                    prototype: prototype.clone(),
6826                    fib_like: None,
6827                };
6828                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
6829                self.subs.insert(key, Arc::new(sub));
6830                Ok(PerlValue::UNDEF)
6831            }
6832            StmtKind::StructDecl { def } => {
6833                if self.struct_defs.contains_key(&def.name) {
6834                    return Err(PerlError::runtime(
6835                        format!("duplicate struct `{}`", def.name),
6836                        stmt.line,
6837                    )
6838                    .into());
6839                }
6840                self.struct_defs
6841                    .insert(def.name.clone(), Arc::new(def.clone()));
6842                Ok(PerlValue::UNDEF)
6843            }
6844            StmtKind::EnumDecl { def } => {
6845                if self.enum_defs.contains_key(&def.name) {
6846                    return Err(PerlError::runtime(
6847                        format!("duplicate enum `{}`", def.name),
6848                        stmt.line,
6849                    )
6850                    .into());
6851                }
6852                self.enum_defs
6853                    .insert(def.name.clone(), Arc::new(def.clone()));
6854                Ok(PerlValue::UNDEF)
6855            }
6856            StmtKind::ClassDecl { def } => {
6857                if self.class_defs.contains_key(&def.name) {
6858                    return Err(PerlError::runtime(
6859                        format!("duplicate class `{}`", def.name),
6860                        stmt.line,
6861                    )
6862                    .into());
6863                }
6864                // Final class enforcement: prevent subclassing
6865                for parent_name in &def.extends {
6866                    if let Some(parent_def) = self.class_defs.get(parent_name) {
6867                        if parent_def.is_final {
6868                            return Err(PerlError::runtime(
6869                                format!("cannot extend final class `{}`", parent_name),
6870                                stmt.line,
6871                            )
6872                            .into());
6873                        }
6874                        // Final method enforcement: prevent overriding
6875                        for m in &def.methods {
6876                            if let Some(parent_method) = parent_def.method(&m.name) {
6877                                if parent_method.is_final {
6878                                    return Err(PerlError::runtime(
6879                                        format!(
6880                                            "cannot override final method `{}` from class `{}`",
6881                                            m.name, parent_name
6882                                        ),
6883                                        stmt.line,
6884                                    )
6885                                    .into());
6886                                }
6887                            }
6888                        }
6889                    }
6890                }
6891                // Trait contract enforcement + default method inheritance
6892                let mut def = def.clone();
6893                for trait_name in &def.implements.clone() {
6894                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
6895                        for required in trait_def.required_methods() {
6896                            let has_method = def.methods.iter().any(|m| m.name == required.name);
6897                            if !has_method {
6898                                return Err(PerlError::runtime(
6899                                    format!(
6900                                        "class `{}` implements trait `{}` but does not define required method `{}`",
6901                                        def.name, trait_name, required.name
6902                                    ),
6903                                    stmt.line,
6904                                )
6905                                .into());
6906                            }
6907                        }
6908                        // Inherit default methods from trait (methods with bodies)
6909                        for tm in &trait_def.methods {
6910                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
6911                                def.methods.push(tm.clone());
6912                            }
6913                        }
6914                    }
6915                }
6916                // Abstract method enforcement: concrete subclasses must implement
6917                // all abstract methods (body-less methods) from abstract parents
6918                if !def.is_abstract {
6919                    for parent_name in &def.extends.clone() {
6920                        if let Some(parent_def) = self.class_defs.get(parent_name) {
6921                            if parent_def.is_abstract {
6922                                for m in &parent_def.methods {
6923                                    if m.body.is_none()
6924                                        && !def.methods.iter().any(|dm| dm.name == m.name)
6925                                    {
6926                                        return Err(PerlError::runtime(
6927                                            format!(
6928                                                "class `{}` must implement abstract method `{}` from `{}`",
6929                                                def.name, m.name, parent_name
6930                                            ),
6931                                            stmt.line,
6932                                        )
6933                                        .into());
6934                                    }
6935                                }
6936                            }
6937                        }
6938                    }
6939                }
6940                // Initialize static fields
6941                for sf in &def.static_fields {
6942                    let val = if let Some(ref expr) = sf.default {
6943                        self.eval_expr(expr)?
6944                    } else {
6945                        PerlValue::UNDEF
6946                    };
6947                    let key = format!("{}::{}", def.name, sf.name);
6948                    self.scope.declare_scalar(&key, val);
6949                }
6950                self.class_defs.insert(def.name.clone(), Arc::new(def));
6951                Ok(PerlValue::UNDEF)
6952            }
6953            StmtKind::TraitDecl { def } => {
6954                if self.trait_defs.contains_key(&def.name) {
6955                    return Err(PerlError::runtime(
6956                        format!("duplicate trait `{}`", def.name),
6957                        stmt.line,
6958                    )
6959                    .into());
6960                }
6961                self.trait_defs
6962                    .insert(def.name.clone(), Arc::new(def.clone()));
6963                Ok(PerlValue::UNDEF)
6964            }
6965            StmtKind::My(decls) | StmtKind::Our(decls) => {
6966                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
6967                // For list assignment my ($a, $b) = (10, 20), distribute elements.
6968                // All decls share the same initializer in the AST (parser clones it).
6969                if decls.len() > 1 && decls[0].initializer.is_some() {
6970                    let val = self.eval_expr_ctx(
6971                        decls[0].initializer.as_ref().unwrap(),
6972                        WantarrayCtx::List,
6973                    )?;
6974                    let items = val.to_list();
6975                    let mut idx = 0;
6976                    for decl in decls {
6977                        match decl.sigil {
6978                            Sigil::Scalar => {
6979                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
6980                                let skey = if is_our {
6981                                    self.stash_scalar_name_for_package(&decl.name)
6982                                } else {
6983                                    decl.name.clone()
6984                                };
6985                                self.scope.declare_scalar_frozen(
6986                                    &skey,
6987                                    v,
6988                                    decl.frozen,
6989                                    decl.type_annotation.clone(),
6990                                )?;
6991                                self.english_note_lexical_scalar(&decl.name);
6992                                if is_our {
6993                                    self.note_our_scalar(&decl.name);
6994                                }
6995                                idx += 1;
6996                            }
6997                            Sigil::Array => {
6998                                // Array slurps remaining elements
6999                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7000                                idx = items.len();
7001                                if is_our {
7002                                    self.record_exporter_our_array_name(&decl.name, &rest);
7003                                }
7004                                let aname = self.stash_array_name_for_package(&decl.name);
7005                                self.scope.declare_array(&aname, rest);
7006                            }
7007                            Sigil::Hash => {
7008                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7009                                idx = items.len();
7010                                let mut map = IndexMap::new();
7011                                let mut i = 0;
7012                                while i + 1 < rest.len() {
7013                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7014                                    i += 2;
7015                                }
7016                                self.scope.declare_hash(&decl.name, map);
7017                            }
7018                            Sigil::Typeglob => {
7019                                return Err(PerlError::runtime(
7020                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
7021                                    stmt.line,
7022                                )
7023                                .into());
7024                            }
7025                        }
7026                    }
7027                } else {
7028                    // Single decl or no initializer
7029                    for decl in decls {
7030                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
7031                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
7032                        // compound op reads the lhs (see system Exporter.pm).
7033                        let compound_init = decl
7034                            .initializer
7035                            .as_ref()
7036                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
7037
7038                        if compound_init {
7039                            match decl.sigil {
7040                                Sigil::Typeglob => {
7041                                    return Err(PerlError::runtime(
7042                                        "compound assignment on typeglob declaration is not supported",
7043                                        stmt.line,
7044                                    )
7045                                    .into());
7046                                }
7047                                Sigil::Scalar => {
7048                                    let skey = if is_our {
7049                                        self.stash_scalar_name_for_package(&decl.name)
7050                                    } else {
7051                                        decl.name.clone()
7052                                    };
7053                                    self.scope.declare_scalar_frozen(
7054                                        &skey,
7055                                        PerlValue::UNDEF,
7056                                        decl.frozen,
7057                                        decl.type_annotation.clone(),
7058                                    )?;
7059                                    self.english_note_lexical_scalar(&decl.name);
7060                                    if is_our {
7061                                        self.note_our_scalar(&decl.name);
7062                                    }
7063                                    let init = decl.initializer.as_ref().unwrap();
7064                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7065                                }
7066                                Sigil::Array => {
7067                                    let aname = self.stash_array_name_for_package(&decl.name);
7068                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
7069                                    let init = decl.initializer.as_ref().unwrap();
7070                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7071                                    if is_our {
7072                                        let items = self.scope.get_array(&aname);
7073                                        self.record_exporter_our_array_name(&decl.name, &items);
7074                                    }
7075                                }
7076                                Sigil::Hash => {
7077                                    self.scope.declare_hash_frozen(
7078                                        &decl.name,
7079                                        IndexMap::new(),
7080                                        decl.frozen,
7081                                    );
7082                                    let init = decl.initializer.as_ref().unwrap();
7083                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7084                                }
7085                            }
7086                            continue;
7087                        }
7088
7089                        let val = if let Some(init) = &decl.initializer {
7090                            let ctx = match decl.sigil {
7091                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7092                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7093                            };
7094                            self.eval_expr_ctx(init, ctx)?
7095                        } else {
7096                            PerlValue::UNDEF
7097                        };
7098                        match decl.sigil {
7099                            Sigil::Typeglob => {
7100                                return Err(PerlError::runtime(
7101                                    "`my *FH` / typeglob declaration is not supported",
7102                                    stmt.line,
7103                                )
7104                                .into());
7105                            }
7106                            Sigil::Scalar => {
7107                                let skey = if is_our {
7108                                    self.stash_scalar_name_for_package(&decl.name)
7109                                } else {
7110                                    decl.name.clone()
7111                                };
7112                                self.scope.declare_scalar_frozen(
7113                                    &skey,
7114                                    val,
7115                                    decl.frozen,
7116                                    decl.type_annotation.clone(),
7117                                )?;
7118                                self.english_note_lexical_scalar(&decl.name);
7119                                if is_our {
7120                                    self.note_our_scalar(&decl.name);
7121                                }
7122                            }
7123                            Sigil::Array => {
7124                                let items = val.to_list();
7125                                if is_our {
7126                                    self.record_exporter_our_array_name(&decl.name, &items);
7127                                }
7128                                let aname = self.stash_array_name_for_package(&decl.name);
7129                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
7130                            }
7131                            Sigil::Hash => {
7132                                let items = val.to_list();
7133                                let mut map = IndexMap::new();
7134                                let mut i = 0;
7135                                while i + 1 < items.len() {
7136                                    let k = items[i].to_string();
7137                                    let v = items[i + 1].clone();
7138                                    map.insert(k, v);
7139                                    i += 2;
7140                                }
7141                                self.scope.declare_hash_frozen(&decl.name, map, decl.frozen);
7142                            }
7143                        }
7144                    }
7145                }
7146                Ok(PerlValue::UNDEF)
7147            }
7148            StmtKind::State(decls) => {
7149                // `state` variables persist across subroutine calls.
7150                // Key by source line + name for uniqueness.
7151                for decl in decls {
7152                    let state_key = format!("{}:{}", stmt.line, decl.name);
7153                    match decl.sigil {
7154                        Sigil::Scalar => {
7155                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
7156                                // Already initialized — declare with persisted value
7157                                self.scope.declare_scalar(&decl.name, prev);
7158                            } else {
7159                                // First encounter — evaluate initializer
7160                                let val = if let Some(init) = &decl.initializer {
7161                                    self.eval_expr(init)?
7162                                } else {
7163                                    PerlValue::UNDEF
7164                                };
7165                                self.state_vars.insert(state_key.clone(), val.clone());
7166                                self.scope.declare_scalar(&decl.name, val);
7167                            }
7168                            // Register for save-back when scope pops
7169                            if let Some(frame) = self.state_bindings_stack.last_mut() {
7170                                frame.push((decl.name.clone(), state_key));
7171                            }
7172                        }
7173                        _ => {
7174                            // For arrays/hashes, fall back to simple my-like behavior
7175                            let val = if let Some(init) = &decl.initializer {
7176                                self.eval_expr(init)?
7177                            } else {
7178                                PerlValue::UNDEF
7179                            };
7180                            match decl.sigil {
7181                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
7182                                Sigil::Hash => {
7183                                    let items = val.to_list();
7184                                    let mut map = IndexMap::new();
7185                                    let mut i = 0;
7186                                    while i + 1 < items.len() {
7187                                        map.insert(items[i].to_string(), items[i + 1].clone());
7188                                        i += 2;
7189                                    }
7190                                    self.scope.declare_hash(&decl.name, map);
7191                                }
7192                                _ => {}
7193                            }
7194                        }
7195                    }
7196                }
7197                Ok(PerlValue::UNDEF)
7198            }
7199            StmtKind::Local(decls) => {
7200                if decls.len() > 1 && decls[0].initializer.is_some() {
7201                    let val = self.eval_expr_ctx(
7202                        decls[0].initializer.as_ref().unwrap(),
7203                        WantarrayCtx::List,
7204                    )?;
7205                    let items = val.to_list();
7206                    let mut idx = 0;
7207                    for decl in decls {
7208                        match decl.sigil {
7209                            Sigil::Scalar => {
7210                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7211                                idx += 1;
7212                                self.scope.local_set_scalar(&decl.name, v)?;
7213                            }
7214                            Sigil::Array => {
7215                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7216                                idx = items.len();
7217                                self.scope.local_set_array(&decl.name, rest)?;
7218                            }
7219                            Sigil::Hash => {
7220                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7221                                idx = items.len();
7222                                if decl.name == "ENV" {
7223                                    self.materialize_env_if_needed();
7224                                }
7225                                let mut map = IndexMap::new();
7226                                let mut i = 0;
7227                                while i + 1 < rest.len() {
7228                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7229                                    i += 2;
7230                                }
7231                                self.scope.local_set_hash(&decl.name, map)?;
7232                            }
7233                            Sigil::Typeglob => {
7234                                return Err(PerlError::runtime(
7235                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
7236                                    stmt.line,
7237                                )
7238                                .into());
7239                            }
7240                        }
7241                    }
7242                    Ok(val)
7243                } else {
7244                    let mut last_val = PerlValue::UNDEF;
7245                    for decl in decls {
7246                        let val = if let Some(init) = &decl.initializer {
7247                            let ctx = match decl.sigil {
7248                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7249                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7250                            };
7251                            self.eval_expr_ctx(init, ctx)?
7252                        } else {
7253                            PerlValue::UNDEF
7254                        };
7255                        last_val = val.clone();
7256                        match decl.sigil {
7257                            Sigil::Typeglob => {
7258                                let old = self.glob_handle_alias.remove(&decl.name);
7259                                if let Some(frame) = self.glob_restore_frames.last_mut() {
7260                                    frame.push((decl.name.clone(), old));
7261                                }
7262                                if let Some(init) = &decl.initializer {
7263                                    if let ExprKind::Typeglob(rhs) = &init.kind {
7264                                        self.glob_handle_alias
7265                                            .insert(decl.name.clone(), rhs.clone());
7266                                    } else {
7267                                        return Err(PerlError::runtime(
7268                                            "local *GLOB = *OTHER — right side must be a typeglob",
7269                                            stmt.line,
7270                                        )
7271                                        .into());
7272                                    }
7273                                }
7274                            }
7275                            Sigil::Scalar => {
7276                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
7277                                // must update the interpreter's backing field too — these are
7278                                // not stored in `Scope`. Save the prior value for restoration
7279                                // on `scope_pop_hook` so the block-exit restore is visible to
7280                                // print/I/O code.
7281                                if Self::is_special_scalar_name_for_set(&decl.name) {
7282                                    let old = self.get_special_var(&decl.name);
7283                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
7284                                    {
7285                                        frame.push((decl.name.clone(), old));
7286                                    }
7287                                    self.set_special_var(&decl.name, &val)
7288                                        .map_err(|e| e.at_line(stmt.line))?;
7289                                }
7290                                self.scope.local_set_scalar(&decl.name, val)?;
7291                            }
7292                            Sigil::Array => {
7293                                self.scope.local_set_array(&decl.name, val.to_list())?;
7294                            }
7295                            Sigil::Hash => {
7296                                if decl.name == "ENV" {
7297                                    self.materialize_env_if_needed();
7298                                }
7299                                let items = val.to_list();
7300                                let mut map = IndexMap::new();
7301                                let mut i = 0;
7302                                while i + 1 < items.len() {
7303                                    let k = items[i].to_string();
7304                                    let v = items[i + 1].clone();
7305                                    map.insert(k, v);
7306                                    i += 2;
7307                                }
7308                                self.scope.local_set_hash(&decl.name, map)?;
7309                            }
7310                        }
7311                    }
7312                    Ok(last_val)
7313                }
7314            }
7315            StmtKind::LocalExpr {
7316                target,
7317                initializer,
7318            } => {
7319                let rhs_name = |init: &Expr| -> PerlResult<Option<String>> {
7320                    match &init.kind {
7321                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
7322                        _ => Err(PerlError::runtime(
7323                            "local *GLOB = *OTHER — right side must be a typeglob",
7324                            stmt.line,
7325                        )),
7326                    }
7327                };
7328                match &target.kind {
7329                    ExprKind::Typeglob(name) => {
7330                        let rhs = if let Some(init) = initializer {
7331                            rhs_name(init)?
7332                        } else {
7333                            None
7334                        };
7335                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
7336                        return Ok(PerlValue::UNDEF);
7337                    }
7338                    ExprKind::Deref {
7339                        expr,
7340                        kind: Sigil::Typeglob,
7341                    } => {
7342                        let lhs = self.eval_expr(expr)?.to_string();
7343                        let rhs = if let Some(init) = initializer {
7344                            rhs_name(init)?
7345                        } else {
7346                            None
7347                        };
7348                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7349                        return Ok(PerlValue::UNDEF);
7350                    }
7351                    ExprKind::TypeglobExpr(e) => {
7352                        let lhs = self.eval_expr(e)?.to_string();
7353                        let rhs = if let Some(init) = initializer {
7354                            rhs_name(init)?
7355                        } else {
7356                            None
7357                        };
7358                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7359                        return Ok(PerlValue::UNDEF);
7360                    }
7361                    _ => {}
7362                }
7363                let val = if let Some(init) = initializer {
7364                    let ctx = match &target.kind {
7365                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
7366                        _ => WantarrayCtx::Scalar,
7367                    };
7368                    self.eval_expr_ctx(init, ctx)?
7369                } else {
7370                    PerlValue::UNDEF
7371                };
7372                match &target.kind {
7373                    ExprKind::ScalarVar(name) => {
7374                        // `local $X = …` on a special var — see twin block in
7375                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
7376                        if Self::is_special_scalar_name_for_set(name) {
7377                            let old = self.get_special_var(name);
7378                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
7379                                frame.push((name.clone(), old));
7380                            }
7381                            self.set_special_var(name, &val)
7382                                .map_err(|e| e.at_line(stmt.line))?;
7383                        }
7384                        self.scope.local_set_scalar(name, val.clone())?;
7385                    }
7386                    ExprKind::ArrayVar(name) => {
7387                        self.scope.local_set_array(name, val.to_list())?;
7388                    }
7389                    ExprKind::HashVar(name) => {
7390                        if name == "ENV" {
7391                            self.materialize_env_if_needed();
7392                        }
7393                        let items = val.to_list();
7394                        let mut map = IndexMap::new();
7395                        let mut i = 0;
7396                        while i + 1 < items.len() {
7397                            map.insert(items[i].to_string(), items[i + 1].clone());
7398                            i += 2;
7399                        }
7400                        self.scope.local_set_hash(name, map)?;
7401                    }
7402                    ExprKind::HashElement { hash, key } => {
7403                        let ks = self.eval_expr(key)?.to_string();
7404                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
7405                    }
7406                    ExprKind::ArrayElement { array, index } => {
7407                        self.check_strict_array_var(array, stmt.line)?;
7408                        let aname = self.stash_array_name_for_package(array);
7409                        let idx = self.eval_expr(index)?.to_int();
7410                        self.scope
7411                            .local_set_array_element(&aname, idx, val.clone())?;
7412                    }
7413                    _ => {
7414                        return Err(PerlError::runtime(
7415                            format!(
7416                                "local on this lvalue is not supported yet ({:?})",
7417                                target.kind
7418                            ),
7419                            stmt.line,
7420                        )
7421                        .into());
7422                    }
7423                }
7424                Ok(val)
7425            }
7426            StmtKind::MySync(decls) => {
7427                for decl in decls {
7428                    let val = if let Some(init) = &decl.initializer {
7429                        self.eval_expr(init)?
7430                    } else {
7431                        PerlValue::UNDEF
7432                    };
7433                    match decl.sigil {
7434                        Sigil::Typeglob => {
7435                            return Err(PerlError::runtime(
7436                                "`mysync` does not support typeglob variables",
7437                                stmt.line,
7438                            )
7439                            .into());
7440                        }
7441                        Sigil::Scalar => {
7442                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
7443                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
7444                            let stored = if val.is_mysync_deque_or_heap() {
7445                                val
7446                            } else {
7447                                PerlValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(val)))
7448                            };
7449                            self.scope.declare_scalar(&decl.name, stored);
7450                        }
7451                        Sigil::Array => {
7452                            self.scope.declare_atomic_array(&decl.name, val.to_list());
7453                        }
7454                        Sigil::Hash => {
7455                            let items = val.to_list();
7456                            let mut map = IndexMap::new();
7457                            let mut i = 0;
7458                            while i + 1 < items.len() {
7459                                map.insert(items[i].to_string(), items[i + 1].clone());
7460                                i += 2;
7461                            }
7462                            self.scope.declare_atomic_hash(&decl.name, map);
7463                        }
7464                    }
7465                }
7466                Ok(PerlValue::UNDEF)
7467            }
7468            StmtKind::Package { name } => {
7469                // Minimal package support — just set a variable
7470                let _ = self
7471                    .scope
7472                    .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
7473                Ok(PerlValue::UNDEF)
7474            }
7475            StmtKind::UsePerlVersion { .. } => Ok(PerlValue::UNDEF),
7476            StmtKind::Use { .. } => {
7477                // Handled in `prepare_program_top_level` before BEGIN / main.
7478                Ok(PerlValue::UNDEF)
7479            }
7480            StmtKind::UseOverload { pairs } => {
7481                self.install_use_overload_pairs(pairs);
7482                Ok(PerlValue::UNDEF)
7483            }
7484            StmtKind::No { .. } => {
7485                // Handled in `prepare_program_top_level` (same phase as `use`).
7486                Ok(PerlValue::UNDEF)
7487            }
7488            StmtKind::Return(val) => {
7489                let v = if let Some(e) = val {
7490                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
7491                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
7492                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
7493                    self.eval_expr_ctx(e, self.wantarray_kind)?
7494                } else {
7495                    PerlValue::UNDEF
7496                };
7497                Err(Flow::Return(v).into())
7498            }
7499            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
7500            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
7501            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
7502            StmtKind::Block(block) => self.exec_block(block),
7503            StmtKind::Begin(_)
7504            | StmtKind::UnitCheck(_)
7505            | StmtKind::Check(_)
7506            | StmtKind::Init(_)
7507            | StmtKind::End(_) => Ok(PerlValue::UNDEF),
7508            StmtKind::Empty => Ok(PerlValue::UNDEF),
7509            StmtKind::Goto { target } => {
7510                // goto &sub — tail call
7511                if let ExprKind::SubroutineRef(name) = &target.kind {
7512                    return Err(Flow::GotoSub(name.clone()).into());
7513                }
7514                Err(PerlError::runtime("goto reached outside goto-aware block", stmt.line).into())
7515            }
7516            StmtKind::EvalTimeout { timeout, body } => {
7517                let secs = self.eval_expr(timeout)?.to_number();
7518                self.eval_timeout_block(body, secs, stmt.line)
7519            }
7520            StmtKind::Tie {
7521                target,
7522                class,
7523                args,
7524            } => {
7525                let kind = match &target {
7526                    TieTarget::Scalar(_) => 0u8,
7527                    TieTarget::Array(_) => 1u8,
7528                    TieTarget::Hash(_) => 2u8,
7529                };
7530                let name = match &target {
7531                    TieTarget::Scalar(s) => s.as_str(),
7532                    TieTarget::Array(a) => a.as_str(),
7533                    TieTarget::Hash(h) => h.as_str(),
7534                };
7535                let mut vals = vec![self.eval_expr(class)?];
7536                for a in args {
7537                    vals.push(self.eval_expr(a)?);
7538                }
7539                self.tie_execute(kind, name, vals, stmt.line)
7540                    .map_err(Into::into)
7541            }
7542            StmtKind::TryCatch {
7543                try_block,
7544                catch_var,
7545                catch_block,
7546                finally_block,
7547            } => match self.exec_block(try_block) {
7548                Ok(v) => {
7549                    if let Some(fb) = finally_block {
7550                        self.exec_block(fb)?;
7551                    }
7552                    Ok(v)
7553                }
7554                Err(FlowOrError::Error(e)) => {
7555                    if matches!(e.kind, ErrorKind::Exit(_)) {
7556                        return Err(FlowOrError::Error(e));
7557                    }
7558                    self.scope_push_hook();
7559                    self.scope
7560                        .declare_scalar(catch_var, PerlValue::string(e.to_string()));
7561                    self.english_note_lexical_scalar(catch_var);
7562                    let r = self.exec_block(catch_block);
7563                    self.scope_pop_hook();
7564                    if let Some(fb) = finally_block {
7565                        self.exec_block(fb)?;
7566                    }
7567                    r
7568                }
7569                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
7570            },
7571            StmtKind::Given { topic, body } => self.exec_given(topic, body),
7572            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(PerlError::runtime(
7573                "when/default may only appear inside a given block",
7574                stmt.line,
7575            )
7576            .into()),
7577            StmtKind::FormatDecl { .. } => {
7578                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
7579                Ok(PerlValue::UNDEF)
7580            }
7581            StmtKind::Continue(block) => self.exec_block_smart(block),
7582        }
7583    }
7584
7585    #[inline]
7586    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
7587        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
7588    }
7589
7590    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
7591    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
7592    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
7593    pub(crate) fn scalar_compound_assign_scalar_target(
7594        &mut self,
7595        name: &str,
7596        op: BinOp,
7597        rhs: PerlValue,
7598    ) -> Result<PerlValue, PerlError> {
7599        if op == BinOp::Concat {
7600            return self.scope.scalar_concat_inplace(name, &rhs);
7601        }
7602        Ok(self
7603            .scope
7604            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs)))
7605    }
7606
7607    fn compound_scalar_binop(old: &PerlValue, op: BinOp, rhs: &PerlValue) -> PerlValue {
7608        match op {
7609            BinOp::Add => {
7610                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7611                    PerlValue::integer(a.wrapping_add(b))
7612                } else {
7613                    PerlValue::float(old.to_number() + rhs.to_number())
7614                }
7615            }
7616            BinOp::Sub => {
7617                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7618                    PerlValue::integer(a.wrapping_sub(b))
7619                } else {
7620                    PerlValue::float(old.to_number() - rhs.to_number())
7621                }
7622            }
7623            BinOp::Mul => {
7624                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7625                    PerlValue::integer(a.wrapping_mul(b))
7626                } else {
7627                    PerlValue::float(old.to_number() * rhs.to_number())
7628                }
7629            }
7630            BinOp::BitAnd => {
7631                if let Some(s) = crate::value::set_intersection(old, rhs) {
7632                    s
7633                } else {
7634                    PerlValue::integer(old.to_int() & rhs.to_int())
7635                }
7636            }
7637            BinOp::BitOr => {
7638                if let Some(s) = crate::value::set_union(old, rhs) {
7639                    s
7640                } else {
7641                    PerlValue::integer(old.to_int() | rhs.to_int())
7642                }
7643            }
7644            BinOp::BitXor => PerlValue::integer(old.to_int() ^ rhs.to_int()),
7645            BinOp::ShiftLeft => PerlValue::integer(old.to_int() << rhs.to_int()),
7646            BinOp::ShiftRight => PerlValue::integer(old.to_int() >> rhs.to_int()),
7647            BinOp::Div => PerlValue::float(old.to_number() / rhs.to_number()),
7648            BinOp::Mod => PerlValue::float(old.to_number() % rhs.to_number()),
7649            BinOp::Pow => PerlValue::float(old.to_number().powf(rhs.to_number())),
7650            BinOp::LogOr => {
7651                if old.is_true() {
7652                    old.clone()
7653                } else {
7654                    rhs.clone()
7655                }
7656            }
7657            BinOp::DefinedOr => {
7658                if !old.is_undef() {
7659                    old.clone()
7660                } else {
7661                    rhs.clone()
7662                }
7663            }
7664            BinOp::LogAnd => {
7665                if old.is_true() {
7666                    rhs.clone()
7667                } else {
7668                    old.clone()
7669                }
7670            }
7671            _ => PerlValue::float(old.to_number() + rhs.to_number()),
7672        }
7673    }
7674
7675    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
7676    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
7677    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
7678    fn eval_hash_slice_key_components(
7679        &mut self,
7680        key_expr: &Expr,
7681    ) -> Result<Vec<String>, FlowOrError> {
7682        let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
7683            self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
7684        } else {
7685            self.eval_expr(key_expr)?
7686        };
7687        if let Some(vv) = v.as_array_vec() {
7688            Ok(vv.iter().map(|x| x.to_string()).collect())
7689        } else {
7690            Ok(vec![v.to_string()])
7691        }
7692    }
7693
7694    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
7695    pub(crate) fn symbolic_deref(
7696        &mut self,
7697        val: PerlValue,
7698        kind: Sigil,
7699        line: usize,
7700    ) -> ExecResult {
7701        match kind {
7702            Sigil::Scalar => {
7703                if let Some(name) = val.as_scalar_binding_name() {
7704                    return Ok(self.get_special_var(&name));
7705                }
7706                if let Some(r) = val.as_scalar_ref() {
7707                    return Ok(r.read().clone());
7708                }
7709                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
7710                if let Some(r) = val.as_array_ref() {
7711                    return Ok(PerlValue::array(r.read().clone()));
7712                }
7713                if let Some(name) = val.as_array_binding_name() {
7714                    return Ok(PerlValue::array(self.scope.get_array(&name)));
7715                }
7716                if let Some(r) = val.as_hash_ref() {
7717                    return Ok(PerlValue::hash(r.read().clone()));
7718                }
7719                if let Some(name) = val.as_hash_binding_name() {
7720                    self.touch_env_hash(&name);
7721                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
7722                }
7723                if let Some(s) = val.as_str() {
7724                    if self.strict_refs {
7725                        return Err(PerlError::runtime(
7726                            format!(
7727                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
7728                                s
7729                            ),
7730                            line,
7731                        )
7732                        .into());
7733                    }
7734                    return Ok(self.get_special_var(&s));
7735                }
7736                Err(PerlError::runtime("Can't dereference non-reference as scalar", line).into())
7737            }
7738            Sigil::Array => {
7739                if let Some(r) = val.as_array_ref() {
7740                    return Ok(PerlValue::array(r.read().clone()));
7741                }
7742                if let Some(name) = val.as_array_binding_name() {
7743                    return Ok(PerlValue::array(self.scope.get_array(&name)));
7744                }
7745                if let Some(s) = val.as_str() {
7746                    if self.strict_refs {
7747                        return Err(PerlError::runtime(
7748                            format!(
7749                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
7750                                s
7751                            ),
7752                            line,
7753                        )
7754                        .into());
7755                    }
7756                    return Ok(PerlValue::array(self.scope.get_array(&s)));
7757                }
7758                Err(PerlError::runtime("Can't dereference non-reference as array", line).into())
7759            }
7760            Sigil::Hash => {
7761                if let Some(r) = val.as_hash_ref() {
7762                    return Ok(PerlValue::hash(r.read().clone()));
7763                }
7764                if let Some(name) = val.as_hash_binding_name() {
7765                    self.touch_env_hash(&name);
7766                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
7767                }
7768                if let Some(s) = val.as_str() {
7769                    if self.strict_refs {
7770                        return Err(PerlError::runtime(
7771                            format!(
7772                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7773                                s
7774                            ),
7775                            line,
7776                        )
7777                        .into());
7778                    }
7779                    self.touch_env_hash(&s);
7780                    return Ok(PerlValue::hash(self.scope.get_hash(&s)));
7781                }
7782                Err(PerlError::runtime("Can't dereference non-reference as hash", line).into())
7783            }
7784            Sigil::Typeglob => {
7785                if let Some(s) = val.as_str() {
7786                    return Ok(PerlValue::string(self.resolve_io_handle_name(&s)));
7787                }
7788                Err(PerlError::runtime("Can't dereference non-reference as typeglob", line).into())
7789            }
7790        }
7791    }
7792
7793    /// `qq` list join expects a plain array; if a bare [`PerlValue::array_ref`] reaches join, peel
7794    /// one level so elements stringify like Perl (`"@$r"`).
7795    #[inline]
7796    pub(crate) fn peel_array_ref_for_list_join(&self, v: PerlValue) -> PerlValue {
7797        if let Some(r) = v.as_array_ref() {
7798            return PerlValue::array(r.read().clone());
7799        }
7800        v
7801    }
7802
7803    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
7804    pub(crate) fn make_array_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
7805        if let Some(a) = val.as_array_ref() {
7806            return Ok(PerlValue::array_ref(Arc::clone(&a)));
7807        }
7808        if let Some(name) = val.as_array_binding_name() {
7809            return Ok(PerlValue::array_binding_ref(name));
7810        }
7811        if let Some(s) = val.as_str() {
7812            if self.strict_refs {
7813                return Err(PerlError::runtime(
7814                    format!(
7815                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
7816                        s
7817                    ),
7818                    line,
7819                )
7820                .into());
7821            }
7822            return Ok(PerlValue::array_binding_ref(s.to_string()));
7823        }
7824        if let Some(r) = val.as_scalar_ref() {
7825            let inner = r.read().clone();
7826            return self.make_array_ref_alias(inner, line);
7827        }
7828        Err(PerlError::runtime("Can't make array reference from value", line).into())
7829    }
7830
7831    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
7832    pub(crate) fn make_hash_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
7833        if let Some(h) = val.as_hash_ref() {
7834            return Ok(PerlValue::hash_ref(Arc::clone(&h)));
7835        }
7836        if let Some(name) = val.as_hash_binding_name() {
7837            return Ok(PerlValue::hash_binding_ref(name));
7838        }
7839        if let Some(s) = val.as_str() {
7840            if self.strict_refs {
7841                return Err(PerlError::runtime(
7842                    format!(
7843                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7844                        s
7845                    ),
7846                    line,
7847                )
7848                .into());
7849            }
7850            return Ok(PerlValue::hash_binding_ref(s.to_string()));
7851        }
7852        if let Some(r) = val.as_scalar_ref() {
7853            let inner = r.read().clone();
7854            return self.make_hash_ref_alias(inner, line);
7855        }
7856        Err(PerlError::runtime("Can't make hash reference from value", line).into())
7857    }
7858
7859    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
7860    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
7861    pub(crate) fn process_case_escapes(s: &str) -> String {
7862        // Quick check: if no backslash, nothing to do
7863        if !s.contains('\\') {
7864            return s.to_string();
7865        }
7866        let mut result = String::with_capacity(s.len());
7867        let mut chars = s.chars().peekable();
7868        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
7869        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
7870
7871        while let Some(c) = chars.next() {
7872            if c == '\\' {
7873                match chars.peek() {
7874                    Some(&'U') => {
7875                        chars.next();
7876                        mode = Some('U');
7877                        continue;
7878                    }
7879                    Some(&'L') => {
7880                        chars.next();
7881                        mode = Some('L');
7882                        continue;
7883                    }
7884                    Some(&'Q') => {
7885                        chars.next();
7886                        mode = Some('Q');
7887                        continue;
7888                    }
7889                    Some(&'E') => {
7890                        chars.next();
7891                        mode = None;
7892                        next_char_mod = None;
7893                        continue;
7894                    }
7895                    Some(&'u') => {
7896                        chars.next();
7897                        next_char_mod = Some('u');
7898                        continue;
7899                    }
7900                    Some(&'l') => {
7901                        chars.next();
7902                        next_char_mod = Some('l');
7903                        continue;
7904                    }
7905                    _ => {}
7906                }
7907            }
7908
7909            let ch = c;
7910
7911            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
7912            if let Some(m) = next_char_mod.take() {
7913                let transformed = match m {
7914                    'u' => ch.to_uppercase().next().unwrap_or(ch),
7915                    'l' => ch.to_lowercase().next().unwrap_or(ch),
7916                    _ => ch,
7917                };
7918                result.push(transformed);
7919            } else {
7920                // Apply ongoing mode
7921                match mode {
7922                    Some('U') => {
7923                        for uc in ch.to_uppercase() {
7924                            result.push(uc);
7925                        }
7926                    }
7927                    Some('L') => {
7928                        for lc in ch.to_lowercase() {
7929                            result.push(lc);
7930                        }
7931                    }
7932                    Some('Q') => {
7933                        if !ch.is_ascii_alphanumeric() && ch != '_' {
7934                            result.push('\\');
7935                        }
7936                        result.push(ch);
7937                    }
7938                    None | Some(_) => {
7939                        result.push(ch);
7940                    }
7941                }
7942            }
7943        }
7944        result
7945    }
7946
7947    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
7948        let line = expr.line;
7949        match &expr.kind {
7950            ExprKind::Integer(n) => Ok(PerlValue::integer(*n)),
7951            ExprKind::Float(f) => Ok(PerlValue::float(*f)),
7952            ExprKind::String(s) => {
7953                let processed = Self::process_case_escapes(s);
7954                Ok(PerlValue::string(processed))
7955            }
7956            ExprKind::Bareword(s) => {
7957                if s == "__PACKAGE__" {
7958                    return Ok(PerlValue::string(self.current_package()));
7959                }
7960                if let Some(sub) = self.resolve_sub_by_name(s) {
7961                    return self.call_sub(&sub, vec![], ctx, line);
7962                }
7963                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
7964                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
7965                    return r.map_err(Into::into);
7966                }
7967                Ok(PerlValue::string(s.clone()))
7968            }
7969            ExprKind::Undef => Ok(PerlValue::UNDEF),
7970            ExprKind::MagicConst(MagicConstKind::File) => Ok(PerlValue::string(self.file.clone())),
7971            ExprKind::MagicConst(MagicConstKind::Line) => Ok(PerlValue::integer(expr.line as i64)),
7972            ExprKind::MagicConst(MagicConstKind::Sub) => {
7973                if let Some(sub) = self.current_sub_stack.last().cloned() {
7974                    Ok(PerlValue::code_ref(sub))
7975                } else {
7976                    Ok(PerlValue::UNDEF)
7977                }
7978            }
7979            ExprKind::Regex(pattern, flags) => {
7980                if ctx == WantarrayCtx::Void {
7981                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
7982                    let topic = self.scope.get_scalar("_");
7983                    let s = topic.to_string();
7984                    self.regex_match_execute(s, pattern, flags, false, "_", line)
7985                } else {
7986                    let re = self.compile_regex(pattern, flags, line)?;
7987                    Ok(PerlValue::regex(re, pattern.clone(), flags.clone()))
7988                }
7989            }
7990            ExprKind::QW(words) => Ok(PerlValue::array(
7991                words.iter().map(|w| PerlValue::string(w.clone())).collect(),
7992            )),
7993
7994            // Interpolated strings
7995            ExprKind::InterpolatedString(parts) => {
7996                let mut raw_result = String::new();
7997                for part in parts {
7998                    match part {
7999                        StringPart::Literal(s) => raw_result.push_str(s),
8000                        StringPart::ScalarVar(name) => {
8001                            self.check_strict_scalar_var(name, line)?;
8002                            let val = self.get_special_var(name);
8003                            let s = self.stringify_value(val, line)?;
8004                            raw_result.push_str(&s);
8005                        }
8006                        StringPart::ArrayVar(name) => {
8007                            self.check_strict_array_var(name, line)?;
8008                            let aname = self.stash_array_name_for_package(name);
8009                            let arr = self.scope.get_array(&aname);
8010                            let mut parts = Vec::with_capacity(arr.len());
8011                            for v in &arr {
8012                                parts.push(self.stringify_value(v.clone(), line)?);
8013                            }
8014                            let sep = self.list_separator.clone();
8015                            raw_result.push_str(&parts.join(&sep));
8016                        }
8017                        StringPart::Expr(e) => {
8018                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
8019                                self.check_strict_array_var(array, line)?;
8020                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8021                                let val = self.peel_array_ref_for_list_join(val);
8022                                let list = val.to_list();
8023                                let sep = self.list_separator.clone();
8024                                let mut parts = Vec::with_capacity(list.len());
8025                                for v in list {
8026                                    parts.push(self.stringify_value(v, line)?);
8027                                }
8028                                raw_result.push_str(&parts.join(&sep));
8029                            } else if let ExprKind::Deref {
8030                                kind: Sigil::Array, ..
8031                            } = &e.kind
8032                            {
8033                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8034                                let val = self.peel_array_ref_for_list_join(val);
8035                                let list = val.to_list();
8036                                let sep = self.list_separator.clone();
8037                                let mut parts = Vec::with_capacity(list.len());
8038                                for v in list {
8039                                    parts.push(self.stringify_value(v, line)?);
8040                                }
8041                                raw_result.push_str(&parts.join(&sep));
8042                            } else {
8043                                let val = self.eval_expr(e)?;
8044                                let s = self.stringify_value(val, line)?;
8045                                raw_result.push_str(&s);
8046                            }
8047                        }
8048                    }
8049                }
8050                let result = Self::process_case_escapes(&raw_result);
8051                Ok(PerlValue::string(result))
8052            }
8053
8054            // Variables
8055            ExprKind::ScalarVar(name) => {
8056                self.check_strict_scalar_var(name, line)?;
8057                let stor = self.tree_scalar_storage_name(name);
8058                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
8059                    let class = obj
8060                        .as_blessed_ref()
8061                        .map(|b| b.class.clone())
8062                        .unwrap_or_default();
8063                    let full = format!("{}::FETCH", class);
8064                    if let Some(sub) = self.subs.get(&full).cloned() {
8065                        return self.call_sub(&sub, vec![obj], ctx, line);
8066                    }
8067                }
8068                Ok(self.get_special_var(&stor))
8069            }
8070            ExprKind::ArrayVar(name) => {
8071                self.check_strict_array_var(name, line)?;
8072                let aname = self.stash_array_name_for_package(name);
8073                let arr = self.scope.get_array(&aname);
8074                if ctx == WantarrayCtx::List {
8075                    Ok(PerlValue::array(arr))
8076                } else {
8077                    Ok(PerlValue::integer(arr.len() as i64))
8078                }
8079            }
8080            ExprKind::HashVar(name) => {
8081                self.check_strict_hash_var(name, line)?;
8082                self.touch_env_hash(name);
8083                let h = self.scope.get_hash(name);
8084                let pv = PerlValue::hash(h);
8085                if ctx == WantarrayCtx::List {
8086                    Ok(pv)
8087                } else {
8088                    Ok(pv.scalar_context())
8089                }
8090            }
8091            ExprKind::Typeglob(name) => {
8092                let n = self.resolve_io_handle_name(name);
8093                Ok(PerlValue::string(n))
8094            }
8095            ExprKind::TypeglobExpr(e) => {
8096                let name = self.eval_expr(e)?.to_string();
8097                let n = self.resolve_io_handle_name(&name);
8098                Ok(PerlValue::string(n))
8099            }
8100            ExprKind::ArrayElement { array, index } => {
8101                self.check_strict_array_var(array, line)?;
8102                let idx = self.eval_expr(index)?.to_int();
8103                let aname = self.stash_array_name_for_package(array);
8104                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
8105                    let class = obj
8106                        .as_blessed_ref()
8107                        .map(|b| b.class.clone())
8108                        .unwrap_or_default();
8109                    let full = format!("{}::FETCH", class);
8110                    if let Some(sub) = self.subs.get(&full).cloned() {
8111                        let arg_vals = vec![obj, PerlValue::integer(idx)];
8112                        return self.call_sub(&sub, arg_vals, ctx, line);
8113                    }
8114                }
8115                Ok(self.scope.get_array_element(&aname, idx))
8116            }
8117            ExprKind::HashElement { hash, key } => {
8118                self.check_strict_hash_var(hash, line)?;
8119                let k = self.eval_expr(key)?.to_string();
8120                self.touch_env_hash(hash);
8121                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
8122                    let class = obj
8123                        .as_blessed_ref()
8124                        .map(|b| b.class.clone())
8125                        .unwrap_or_default();
8126                    let full = format!("{}::FETCH", class);
8127                    if let Some(sub) = self.subs.get(&full).cloned() {
8128                        let arg_vals = vec![obj, PerlValue::string(k)];
8129                        return self.call_sub(&sub, arg_vals, ctx, line);
8130                    }
8131                }
8132                Ok(self.scope.get_hash_element(hash, &k))
8133            }
8134            ExprKind::ArraySlice { array, indices } => {
8135                self.check_strict_array_var(array, line)?;
8136                let aname = self.stash_array_name_for_package(array);
8137                let flat = self.flatten_array_slice_index_specs(indices)?;
8138                let mut result = Vec::with_capacity(flat.len());
8139                for idx in flat {
8140                    result.push(self.scope.get_array_element(&aname, idx));
8141                }
8142                Ok(PerlValue::array(result))
8143            }
8144            ExprKind::HashSlice { hash, keys } => {
8145                self.check_strict_hash_var(hash, line)?;
8146                self.touch_env_hash(hash);
8147                let mut result = Vec::new();
8148                for key_expr in keys {
8149                    for k in self.eval_hash_slice_key_components(key_expr)? {
8150                        result.push(self.scope.get_hash_element(hash, &k));
8151                    }
8152                }
8153                Ok(PerlValue::array(result))
8154            }
8155            ExprKind::HashSliceDeref { container, keys } => {
8156                let hv = self.eval_expr(container)?;
8157                let mut key_vals = Vec::with_capacity(keys.len());
8158                for key_expr in keys {
8159                    let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
8160                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
8161                    } else {
8162                        self.eval_expr(key_expr)?
8163                    };
8164                    key_vals.push(v);
8165                }
8166                self.hash_slice_deref_values(&hv, &key_vals, line)
8167            }
8168            ExprKind::AnonymousListSlice { source, indices } => {
8169                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
8170                let items = list_val.to_list();
8171                let flat = self.flatten_array_slice_index_specs(indices)?;
8172                let mut out = Vec::with_capacity(flat.len());
8173                for idx in flat {
8174                    let i = if idx < 0 {
8175                        (items.len() as i64 + idx) as usize
8176                    } else {
8177                        idx as usize
8178                    };
8179                    out.push(items.get(i).cloned().unwrap_or(PerlValue::UNDEF));
8180                }
8181                let arr = PerlValue::array(out);
8182                if ctx != WantarrayCtx::List {
8183                    let v = arr.to_list();
8184                    Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF))
8185                } else {
8186                    Ok(arr)
8187                }
8188            }
8189
8190            // References
8191            ExprKind::ScalarRef(inner) => match &inner.kind {
8192                ExprKind::ScalarVar(name) => Ok(PerlValue::scalar_binding_ref(name.clone())),
8193                ExprKind::ArrayVar(name) => {
8194                    self.check_strict_array_var(name, line)?;
8195                    let aname = self.stash_array_name_for_package(name);
8196                    Ok(PerlValue::array_binding_ref(aname))
8197                }
8198                ExprKind::HashVar(name) => {
8199                    self.check_strict_hash_var(name, line)?;
8200                    Ok(PerlValue::hash_binding_ref(name.clone()))
8201                }
8202                ExprKind::Deref {
8203                    expr: e,
8204                    kind: Sigil::Array,
8205                } => {
8206                    let v = self.eval_expr(e)?;
8207                    self.make_array_ref_alias(v, line)
8208                }
8209                ExprKind::Deref {
8210                    expr: e,
8211                    kind: Sigil::Hash,
8212                } => {
8213                    let v = self.eval_expr(e)?;
8214                    self.make_hash_ref_alias(v, line)
8215                }
8216                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
8217                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8218                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8219                }
8220                ExprKind::HashSliceDeref { .. } => {
8221                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8222                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8223                }
8224                _ => {
8225                    let val = self.eval_expr(inner)?;
8226                    Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8227                }
8228            },
8229            ExprKind::ArrayRef(elems) => {
8230                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
8231                // variables flatten into the ref rather than collapsing to a scalar count /
8232                // flip-flop value.
8233                let mut arr = Vec::with_capacity(elems.len());
8234                for e in elems {
8235                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8236                    if let Some(vec) = v.as_array_vec() {
8237                        arr.extend(vec);
8238                    } else {
8239                        arr.push(v);
8240                    }
8241                }
8242                Ok(PerlValue::array_ref(Arc::new(RwLock::new(arr))))
8243            }
8244            ExprKind::HashRef(pairs) => {
8245                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
8246                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
8247                let mut map = IndexMap::new();
8248                for (k, v) in pairs {
8249                    let key_str = self.eval_expr(k)?.to_string();
8250                    if key_str == "__HASH_SPREAD__" {
8251                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
8252                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8253                        let items = spread.to_list();
8254                        let mut i = 0;
8255                        while i + 1 < items.len() {
8256                            map.insert(items[i].to_string(), items[i + 1].clone());
8257                            i += 2;
8258                        }
8259                    } else {
8260                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8261                        map.insert(key_str, val);
8262                    }
8263                }
8264                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map))))
8265            }
8266            ExprKind::CodeRef { params, body } => {
8267                let captured = self.scope.capture();
8268                Ok(PerlValue::code_ref(Arc::new(PerlSub {
8269                    name: "__ANON__".to_string(),
8270                    params: params.clone(),
8271                    body: body.clone(),
8272                    closure_env: Some(captured),
8273                    prototype: None,
8274                    fib_like: None,
8275                })))
8276            }
8277            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
8278            ExprKind::SubroutineCodeRef(name) => {
8279                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
8280                    PerlError::runtime(self.undefined_subroutine_resolve_message(name), line)
8281                })?;
8282                Ok(PerlValue::code_ref(sub))
8283            }
8284            ExprKind::DynamicSubCodeRef(expr) => {
8285                let name = self.eval_expr(expr)?.to_string();
8286                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
8287                    PerlError::runtime(self.undefined_subroutine_resolve_message(&name), line)
8288                })?;
8289                Ok(PerlValue::code_ref(sub))
8290            }
8291            ExprKind::Deref { expr, kind } => {
8292                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
8293                    let val = self.eval_expr(expr)?;
8294                    let n = self.array_deref_len(val, line)?;
8295                    return Ok(PerlValue::integer(n));
8296                }
8297                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
8298                    let val = self.eval_expr(expr)?;
8299                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
8300                    return Ok(h.scalar_context());
8301                }
8302                let val = self.eval_expr(expr)?;
8303                self.symbolic_deref(val, *kind, line)
8304            }
8305            ExprKind::ArrowDeref { expr, index, kind } => {
8306                match kind {
8307                    DerefKind::Array => {
8308                        let container = self.eval_arrow_array_base(expr, line)?;
8309                        if let ExprKind::List(indices) = &index.kind {
8310                            let mut out = Vec::with_capacity(indices.len());
8311                            for ix in indices {
8312                                let idx = self.eval_expr(ix)?.to_int();
8313                                out.push(self.read_arrow_array_element(
8314                                    container.clone(),
8315                                    idx,
8316                                    line,
8317                                )?);
8318                            }
8319                            let arr = PerlValue::array(out);
8320                            if ctx != WantarrayCtx::List {
8321                                let v = arr.to_list();
8322                                return Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF));
8323                            }
8324                            return Ok(arr);
8325                        }
8326                        let idx = self.eval_expr(index)?.to_int();
8327                        self.read_arrow_array_element(container, idx, line)
8328                    }
8329                    DerefKind::Hash => {
8330                        let val = self.eval_arrow_hash_base(expr, line)?;
8331                        let key = self.eval_expr(index)?.to_string();
8332                        self.read_arrow_hash_element(val, key.as_str(), line)
8333                    }
8334                    DerefKind::Call => {
8335                        // $coderef->(args)
8336                        let val = self.eval_expr(expr)?;
8337                        if let ExprKind::List(ref arg_exprs) = index.kind {
8338                            let mut args = Vec::new();
8339                            for a in arg_exprs {
8340                                args.push(self.eval_expr(a)?);
8341                            }
8342                            if let Some(sub) = val.as_code_ref() {
8343                                return self.call_sub(&sub, args, ctx, line);
8344                            }
8345                            Err(PerlError::runtime("Not a code reference", line).into())
8346                        } else {
8347                            Err(PerlError::runtime("Invalid call deref", line).into())
8348                        }
8349                    }
8350                }
8351            }
8352
8353            // Binary operators
8354            ExprKind::BinOp { left, op, right } => {
8355                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
8356                match op {
8357                    BinOp::BindMatch => {
8358                        let lv = self.eval_expr(left)?;
8359                        let rv = self.eval_expr(right)?;
8360                        let s = lv.to_string();
8361                        let pat = rv.to_string();
8362                        return self.regex_match_execute(s, &pat, "", false, "_", line);
8363                    }
8364                    BinOp::BindNotMatch => {
8365                        let lv = self.eval_expr(left)?;
8366                        let rv = self.eval_expr(right)?;
8367                        let s = lv.to_string();
8368                        let pat = rv.to_string();
8369                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
8370                        return Ok(PerlValue::integer(if m.is_true() { 0 } else { 1 }));
8371                    }
8372                    BinOp::LogAnd | BinOp::LogAndWord => {
8373                        match &left.kind {
8374                            ExprKind::Regex(_, _) => {
8375                                if !self.eval_boolean_rvalue_condition(left)? {
8376                                    return Ok(PerlValue::string(String::new()));
8377                                }
8378                            }
8379                            _ => {
8380                                let lv = self.eval_expr(left)?;
8381                                if !lv.is_true() {
8382                                    return Ok(lv);
8383                                }
8384                            }
8385                        }
8386                        return match &right.kind {
8387                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8388                                if self.eval_boolean_rvalue_condition(right)? {
8389                                    1
8390                                } else {
8391                                    0
8392                                },
8393                            )),
8394                            _ => self.eval_expr(right),
8395                        };
8396                    }
8397                    BinOp::LogOr | BinOp::LogOrWord => {
8398                        match &left.kind {
8399                            ExprKind::Regex(_, _) => {
8400                                if self.eval_boolean_rvalue_condition(left)? {
8401                                    return Ok(PerlValue::integer(1));
8402                                }
8403                            }
8404                            _ => {
8405                                let lv = self.eval_expr(left)?;
8406                                if lv.is_true() {
8407                                    return Ok(lv);
8408                                }
8409                            }
8410                        }
8411                        return match &right.kind {
8412                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8413                                if self.eval_boolean_rvalue_condition(right)? {
8414                                    1
8415                                } else {
8416                                    0
8417                                },
8418                            )),
8419                            _ => self.eval_expr(right),
8420                        };
8421                    }
8422                    BinOp::DefinedOr => {
8423                        let lv = self.eval_expr(left)?;
8424                        if !lv.is_undef() {
8425                            return Ok(lv);
8426                        }
8427                        return self.eval_expr(right);
8428                    }
8429                    _ => {}
8430                }
8431                let lv = self.eval_expr(left)?;
8432                let rv = self.eval_expr(right)?;
8433                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
8434                    return r;
8435                }
8436                self.eval_binop(*op, &lv, &rv, line)
8437            }
8438
8439            // Unary
8440            ExprKind::UnaryOp { op, expr } => match op {
8441                UnaryOp::PreIncrement => {
8442                    if let ExprKind::ScalarVar(name) = &expr.kind {
8443                        self.check_strict_scalar_var(name, line)?;
8444                        let n = self.english_scalar_name(name);
8445                        return Ok(self
8446                            .scope
8447                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() + 1)));
8448                    }
8449                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8450                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8451                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8452                                *kind, true, true, line,
8453                            ));
8454                        }
8455                    }
8456                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8457                        let href = self.eval_expr(container)?;
8458                        let mut key_vals = Vec::with_capacity(keys.len());
8459                        for key_expr in keys {
8460                            key_vals.push(self.eval_expr(key_expr)?);
8461                        }
8462                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
8463                    }
8464                    if let ExprKind::ArrowDeref {
8465                        expr: arr_expr,
8466                        index,
8467                        kind: DerefKind::Array,
8468                    } = &expr.kind
8469                    {
8470                        if let ExprKind::List(indices) = &index.kind {
8471                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8472                            let mut idxs = Vec::with_capacity(indices.len());
8473                            for ix in indices {
8474                                idxs.push(self.eval_expr(ix)?.to_int());
8475                            }
8476                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
8477                        }
8478                    }
8479                    let val = self.eval_expr(expr)?;
8480                    let new_val = PerlValue::integer(val.to_int() + 1);
8481                    self.assign_value(expr, new_val.clone())?;
8482                    Ok(new_val)
8483                }
8484                UnaryOp::PreDecrement => {
8485                    if let ExprKind::ScalarVar(name) = &expr.kind {
8486                        self.check_strict_scalar_var(name, line)?;
8487                        let n = self.english_scalar_name(name);
8488                        return Ok(self
8489                            .scope
8490                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() - 1)));
8491                    }
8492                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8493                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8494                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8495                                *kind, true, false, line,
8496                            ));
8497                        }
8498                    }
8499                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8500                        let href = self.eval_expr(container)?;
8501                        let mut key_vals = Vec::with_capacity(keys.len());
8502                        for key_expr in keys {
8503                            key_vals.push(self.eval_expr(key_expr)?);
8504                        }
8505                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
8506                    }
8507                    if let ExprKind::ArrowDeref {
8508                        expr: arr_expr,
8509                        index,
8510                        kind: DerefKind::Array,
8511                    } = &expr.kind
8512                    {
8513                        if let ExprKind::List(indices) = &index.kind {
8514                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8515                            let mut idxs = Vec::with_capacity(indices.len());
8516                            for ix in indices {
8517                                idxs.push(self.eval_expr(ix)?.to_int());
8518                            }
8519                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
8520                        }
8521                    }
8522                    let val = self.eval_expr(expr)?;
8523                    let new_val = PerlValue::integer(val.to_int() - 1);
8524                    self.assign_value(expr, new_val.clone())?;
8525                    Ok(new_val)
8526                }
8527                _ => {
8528                    match op {
8529                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
8530                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
8531                                let topic = self.scope.get_scalar("_");
8532                                let rl = expr.line;
8533                                let s = topic.to_string();
8534                                let v =
8535                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
8536                                return Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }));
8537                            }
8538                        }
8539                        _ => {}
8540                    }
8541                    let val = self.eval_expr(expr)?;
8542                    match op {
8543                        UnaryOp::Negate => {
8544                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
8545                                return r;
8546                            }
8547                            if let Some(n) = val.as_integer() {
8548                                Ok(PerlValue::integer(-n))
8549                            } else {
8550                                Ok(PerlValue::float(-val.to_number()))
8551                            }
8552                        }
8553                        UnaryOp::LogNot => {
8554                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8555                                let pv = r?;
8556                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8557                            }
8558                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8559                        }
8560                        UnaryOp::BitNot => Ok(PerlValue::integer(!val.to_int())),
8561                        UnaryOp::LogNotWord => {
8562                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8563                                let pv = r?;
8564                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8565                            }
8566                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8567                        }
8568                        UnaryOp::Ref => {
8569                            if let ExprKind::ScalarVar(name) = &expr.kind {
8570                                return Ok(PerlValue::scalar_binding_ref(name.clone()));
8571                            }
8572                            Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8573                        }
8574                        _ => unreachable!(),
8575                    }
8576                }
8577            },
8578
8579            ExprKind::PostfixOp { expr, op } => {
8580                // For scalar variables, use atomic_mutate_post to hold the lock
8581                // for the entire read-modify-write (critical for mysync).
8582                if let ExprKind::ScalarVar(name) = &expr.kind {
8583                    self.check_strict_scalar_var(name, line)?;
8584                    let n = self.english_scalar_name(name);
8585                    let f: fn(&PerlValue) -> PerlValue = match op {
8586                        PostfixOp::Increment => |v| PerlValue::integer(v.to_int() + 1),
8587                        PostfixOp::Decrement => |v| PerlValue::integer(v.to_int() - 1),
8588                    };
8589                    return Ok(self.scope.atomic_mutate_post(n, f));
8590                }
8591                if let ExprKind::Deref { kind, .. } = &expr.kind {
8592                    if matches!(kind, Sigil::Array | Sigil::Hash) {
8593                        let is_inc = matches!(op, PostfixOp::Increment);
8594                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8595                            *kind, false, is_inc, line,
8596                        ));
8597                    }
8598                }
8599                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8600                    let href = self.eval_expr(container)?;
8601                    let mut key_vals = Vec::with_capacity(keys.len());
8602                    for key_expr in keys {
8603                        key_vals.push(self.eval_expr(key_expr)?);
8604                    }
8605                    let kind_byte = match op {
8606                        PostfixOp::Increment => 2u8,
8607                        PostfixOp::Decrement => 3u8,
8608                    };
8609                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
8610                }
8611                if let ExprKind::ArrowDeref {
8612                    expr: arr_expr,
8613                    index,
8614                    kind: DerefKind::Array,
8615                } = &expr.kind
8616                {
8617                    if let ExprKind::List(indices) = &index.kind {
8618                        let container = self.eval_arrow_array_base(arr_expr, line)?;
8619                        let mut idxs = Vec::with_capacity(indices.len());
8620                        for ix in indices {
8621                            idxs.push(self.eval_expr(ix)?.to_int());
8622                        }
8623                        let kind_byte = match op {
8624                            PostfixOp::Increment => 2u8,
8625                            PostfixOp::Decrement => 3u8,
8626                        };
8627                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
8628                    }
8629                }
8630                let val = self.eval_expr(expr)?;
8631                let old = val.clone();
8632                let new_val = match op {
8633                    PostfixOp::Increment => PerlValue::integer(val.to_int() + 1),
8634                    PostfixOp::Decrement => PerlValue::integer(val.to_int() - 1),
8635                };
8636                self.assign_value(expr, new_val)?;
8637                Ok(old)
8638            }
8639
8640            // Assignment
8641            ExprKind::Assign { target, value } => {
8642                if let ExprKind::Typeglob(lhs) = &target.kind {
8643                    if let ExprKind::Typeglob(rhs) = &value.kind {
8644                        self.copy_typeglob_slots(lhs, rhs, line)?;
8645                        return self.eval_expr(value);
8646                    }
8647                }
8648                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
8649                self.assign_value(target, val.clone())?;
8650                Ok(val)
8651            }
8652            ExprKind::CompoundAssign { target, op, value } => {
8653                // For scalar targets, use atomic_mutate to hold the lock.
8654                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
8655                if let ExprKind::ScalarVar(name) = &target.kind {
8656                    self.check_strict_scalar_var(name, line)?;
8657                    let n = self.english_scalar_name(name);
8658                    let op = *op;
8659                    let rhs = match op {
8660                        BinOp::LogOr => {
8661                            let old = self.scope.get_scalar(n);
8662                            if old.is_true() {
8663                                return Ok(old);
8664                            }
8665                            self.eval_expr(value)?
8666                        }
8667                        BinOp::DefinedOr => {
8668                            let old = self.scope.get_scalar(n);
8669                            if !old.is_undef() {
8670                                return Ok(old);
8671                            }
8672                            self.eval_expr(value)?
8673                        }
8674                        BinOp::LogAnd => {
8675                            let old = self.scope.get_scalar(n);
8676                            if !old.is_true() {
8677                                return Ok(old);
8678                            }
8679                            self.eval_expr(value)?
8680                        }
8681                        _ => self.eval_expr(value)?,
8682                    };
8683                    return Ok(self.scalar_compound_assign_scalar_target(n, op, rhs)?);
8684                }
8685                let rhs = self.eval_expr(value)?;
8686                // For hash element targets: $h{key} += 1
8687                if let ExprKind::HashElement { hash, key } = &target.kind {
8688                    self.check_strict_hash_var(hash, line)?;
8689                    let k = self.eval_expr(key)?.to_string();
8690                    let op = *op;
8691                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
8692                        BinOp::Add => {
8693                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8694                                PerlValue::integer(a.wrapping_add(b))
8695                            } else {
8696                                PerlValue::float(old.to_number() + rhs.to_number())
8697                            }
8698                        }
8699                        BinOp::Sub => {
8700                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8701                                PerlValue::integer(a.wrapping_sub(b))
8702                            } else {
8703                                PerlValue::float(old.to_number() - rhs.to_number())
8704                            }
8705                        }
8706                        BinOp::Concat => {
8707                            let mut s = old.to_string();
8708                            rhs.append_to(&mut s);
8709                            PerlValue::string(s)
8710                        }
8711                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
8712                    })?);
8713                }
8714                // For array element targets: $a[i] += 1
8715                if let ExprKind::ArrayElement { array, index } = &target.kind {
8716                    self.check_strict_array_var(array, line)?;
8717                    let idx = self.eval_expr(index)?.to_int();
8718                    let op = *op;
8719                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
8720                        BinOp::Add => {
8721                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8722                                PerlValue::integer(a.wrapping_add(b))
8723                            } else {
8724                                PerlValue::float(old.to_number() + rhs.to_number())
8725                            }
8726                        }
8727                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
8728                    })?);
8729                }
8730                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
8731                    let href = self.eval_expr(container)?;
8732                    let mut key_vals = Vec::with_capacity(keys.len());
8733                    for key_expr in keys {
8734                        key_vals.push(self.eval_expr(key_expr)?);
8735                    }
8736                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
8737                }
8738                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
8739                    if let ExprKind::Deref {
8740                        expr: inner,
8741                        kind: Sigil::Array,
8742                    } = &source.kind
8743                    {
8744                        let container = self.eval_arrow_array_base(inner, line)?;
8745                        let idxs = self.flatten_array_slice_index_specs(indices)?;
8746                        return self
8747                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
8748                    }
8749                }
8750                if let ExprKind::ArrowDeref {
8751                    expr: arr_expr,
8752                    index,
8753                    kind: DerefKind::Array,
8754                } = &target.kind
8755                {
8756                    if let ExprKind::List(indices) = &index.kind {
8757                        let container = self.eval_arrow_array_base(arr_expr, line)?;
8758                        let mut idxs = Vec::with_capacity(indices.len());
8759                        for ix in indices {
8760                            idxs.push(self.eval_expr(ix)?.to_int());
8761                        }
8762                        return self
8763                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
8764                    }
8765                }
8766                let old = self.eval_expr(target)?;
8767                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
8768                self.assign_value(target, new_val.clone())?;
8769                Ok(new_val)
8770            }
8771
8772            // Ternary
8773            ExprKind::Ternary {
8774                condition,
8775                then_expr,
8776                else_expr,
8777            } => {
8778                if self.eval_boolean_rvalue_condition(condition)? {
8779                    self.eval_expr(then_expr)
8780                } else {
8781                    self.eval_expr(else_expr)
8782                }
8783            }
8784
8785            // Range
8786            ExprKind::Range {
8787                from,
8788                to,
8789                exclusive,
8790            } => {
8791                if ctx == WantarrayCtx::List {
8792                    let f = self.eval_expr(from)?;
8793                    let t = self.eval_expr(to)?;
8794                    let list = perl_list_range_expand(f, t);
8795                    Ok(PerlValue::array(list))
8796                } else {
8797                    let key = std::ptr::from_ref(expr) as usize;
8798                    match (&from.kind, &to.kind) {
8799                        (
8800                            ExprKind::Regex(left_pat, left_flags),
8801                            ExprKind::Regex(right_pat, right_flags),
8802                        ) => {
8803                            let dot = self.scalar_flipflop_dot_line();
8804                            let subject = self.scope.get_scalar("_").to_string();
8805                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
8806                                |e| match e {
8807                                    FlowOrError::Error(err) => err,
8808                                    FlowOrError::Flow(_) => PerlError::runtime(
8809                                        "unexpected flow in regex flip-flop",
8810                                        line,
8811                                    ),
8812                                },
8813                            )?;
8814                            let right_re = self
8815                                .compile_regex(right_pat, right_flags, line)
8816                                .map_err(|e| match e {
8817                                    FlowOrError::Error(err) => err,
8818                                    FlowOrError::Flow(_) => PerlError::runtime(
8819                                        "unexpected flow in regex flip-flop",
8820                                        line,
8821                                    ),
8822                                })?;
8823                            let left_m = left_re.is_match(&subject);
8824                            let right_m = right_re.is_match(&subject);
8825                            let st = self.flip_flop_tree.entry(key).or_default();
8826                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
8827                                &mut st.active,
8828                                &mut st.exclusive_left_line,
8829                                *exclusive,
8830                                dot,
8831                                left_m,
8832                                right_m,
8833                            )))
8834                        }
8835                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
8836                            let dot = self.scalar_flipflop_dot_line();
8837                            let subject = self.scope.get_scalar("_").to_string();
8838                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
8839                                |e| match e {
8840                                    FlowOrError::Error(err) => err,
8841                                    FlowOrError::Flow(_) => PerlError::runtime(
8842                                        "unexpected flow in regex/eof flip-flop",
8843                                        line,
8844                                    ),
8845                                },
8846                            )?;
8847                            let left_m = left_re.is_match(&subject);
8848                            let right_m = self.eof_without_arg_is_true();
8849                            let st = self.flip_flop_tree.entry(key).or_default();
8850                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
8851                                &mut st.active,
8852                                &mut st.exclusive_left_line,
8853                                *exclusive,
8854                                dot,
8855                                left_m,
8856                                right_m,
8857                            )))
8858                        }
8859                        (
8860                            ExprKind::Regex(left_pat, left_flags),
8861                            ExprKind::Integer(_) | ExprKind::Float(_),
8862                        ) => {
8863                            let dot = self.scalar_flipflop_dot_line();
8864                            let right = self.eval_expr(to)?.to_int();
8865                            let subject = self.scope.get_scalar("_").to_string();
8866                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
8867                                |e| match e {
8868                                    FlowOrError::Error(err) => err,
8869                                    FlowOrError::Flow(_) => PerlError::runtime(
8870                                        "unexpected flow in regex flip-flop",
8871                                        line,
8872                                    ),
8873                                },
8874                            )?;
8875                            let left_m = left_re.is_match(&subject);
8876                            let right_m = dot == right;
8877                            let st = self.flip_flop_tree.entry(key).or_default();
8878                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
8879                                &mut st.active,
8880                                &mut st.exclusive_left_line,
8881                                *exclusive,
8882                                dot,
8883                                left_m,
8884                                right_m,
8885                            )))
8886                        }
8887                        (ExprKind::Regex(left_pat, left_flags), _) => {
8888                            if let ExprKind::Eof(Some(_)) = &to.kind {
8889                                return Err(FlowOrError::Error(PerlError::runtime(
8890                                    "regex flip-flop with eof(HANDLE) is not supported",
8891                                    line,
8892                                )));
8893                            }
8894                            let dot = self.scalar_flipflop_dot_line();
8895                            let subject = self.scope.get_scalar("_").to_string();
8896                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
8897                                |e| match e {
8898                                    FlowOrError::Error(err) => err,
8899                                    FlowOrError::Flow(_) => PerlError::runtime(
8900                                        "unexpected flow in regex flip-flop",
8901                                        line,
8902                                    ),
8903                                },
8904                            )?;
8905                            let left_m = left_re.is_match(&subject);
8906                            let right_m = self.eval_boolean_rvalue_condition(to)?;
8907                            let st = self.flip_flop_tree.entry(key).or_default();
8908                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
8909                                &mut st.active,
8910                                &mut st.exclusive_left_line,
8911                                *exclusive,
8912                                dot,
8913                                left_m,
8914                                right_m,
8915                            )))
8916                        }
8917                        _ => {
8918                            let left = self.eval_expr(from)?.to_int();
8919                            let right = self.eval_expr(to)?.to_int();
8920                            let dot = self.scalar_flipflop_dot_line();
8921                            let st = self.flip_flop_tree.entry(key).or_default();
8922                            if !st.active {
8923                                if dot == left {
8924                                    st.active = true;
8925                                    if *exclusive {
8926                                        st.exclusive_left_line = Some(dot);
8927                                    } else {
8928                                        st.exclusive_left_line = None;
8929                                        if dot == right {
8930                                            st.active = false;
8931                                        }
8932                                    }
8933                                    return Ok(PerlValue::integer(1));
8934                                }
8935                                return Ok(PerlValue::integer(0));
8936                            }
8937                            if let Some(ll) = st.exclusive_left_line {
8938                                if dot == right && dot > ll {
8939                                    st.active = false;
8940                                    st.exclusive_left_line = None;
8941                                }
8942                            } else if dot == right {
8943                                st.active = false;
8944                            }
8945                            Ok(PerlValue::integer(1))
8946                        }
8947                    }
8948                }
8949            }
8950
8951            // Repeat
8952            ExprKind::Repeat { expr, count } => {
8953                let val = self.eval_expr(expr)?;
8954                let n = self.eval_expr(count)?.to_int().max(0) as usize;
8955                if let Some(s) = val.as_str() {
8956                    Ok(PerlValue::string(s.repeat(n)))
8957                } else if let Some(a) = val.as_array_vec() {
8958                    let mut result = Vec::with_capacity(a.len() * n);
8959                    for _ in 0..n {
8960                        result.extend(a.iter().cloned());
8961                    }
8962                    Ok(PerlValue::array(result))
8963                } else {
8964                    Ok(PerlValue::string(val.to_string().repeat(n)))
8965                }
8966            }
8967
8968            // `my $x = …` / `our` / `state` / `local` used as an expression
8969            // (e.g. `if (my $line = readline)`).  Declare each variable in the
8970            // current scope, evaluate the initializer (if any), and return the
8971            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
8972            ExprKind::MyExpr { keyword, decls } => {
8973                // Build a temporary statement and dispatch to the canonical
8974                // statement handler so behavior matches `my $x = …;` exactly.
8975                let stmt_kind = match keyword.as_str() {
8976                    "my" => StmtKind::My(decls.clone()),
8977                    "our" => StmtKind::Our(decls.clone()),
8978                    "state" => StmtKind::State(decls.clone()),
8979                    "local" => StmtKind::Local(decls.clone()),
8980                    _ => StmtKind::My(decls.clone()),
8981                };
8982                let stmt = Statement {
8983                    label: None,
8984                    kind: stmt_kind,
8985                    line,
8986                };
8987                self.exec_statement(&stmt)?;
8988                // Return the value of the (first) declared variable so the
8989                // surrounding expression sees the assigned value, matching
8990                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
8991                let first = decls.first().ok_or_else(|| {
8992                    FlowOrError::Error(PerlError::runtime("MyExpr: empty decl list", line))
8993                })?;
8994                Ok(match first.sigil {
8995                    Sigil::Scalar => self.scope.get_scalar(&first.name),
8996                    Sigil::Array => PerlValue::array(self.scope.get_array(&first.name)),
8997                    Sigil::Hash => {
8998                        let h = self.scope.get_hash(&first.name);
8999                        let mut flat: Vec<PerlValue> = Vec::with_capacity(h.len() * 2);
9000                        for (k, v) in h {
9001                            flat.push(PerlValue::string(k));
9002                            flat.push(v);
9003                        }
9004                        PerlValue::array(flat)
9005                    }
9006                    Sigil::Typeglob => PerlValue::UNDEF,
9007                })
9008            }
9009
9010            // Function calls
9011            ExprKind::FuncCall { name, args } => {
9012                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
9013                if matches!(name.as_str(), "read" | "CORE::read") && args.len() >= 3 {
9014                    let fh_val = self.eval_expr(&args[0])?;
9015                    let fh = fh_val
9016                        .as_io_handle_name()
9017                        .unwrap_or_else(|| fh_val.to_string());
9018                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
9019                    let offset = if args.len() > 3 {
9020                        self.eval_expr(&args[3])?.to_int().max(0) as usize
9021                    } else {
9022                        0
9023                    };
9024                    // Extract the variable name from the AST
9025                    let var_name = match &args[1].kind {
9026                        ExprKind::ScalarVar(n) => n.clone(),
9027                        _ => self.eval_expr(&args[1])?.to_string(),
9028                    };
9029                    let mut buf = vec![0u8; len];
9030                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
9031                        slot.lock().read(&mut buf).unwrap_or(0)
9032                    } else if fh == "STDIN" {
9033                        std::io::stdin().read(&mut buf).unwrap_or(0)
9034                    } else {
9035                        return Err(PerlError::runtime(
9036                            format!("read: unopened handle {}", fh),
9037                            line,
9038                        )
9039                        .into());
9040                    };
9041                    buf.truncate(n);
9042                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
9043                    if offset > 0 {
9044                        let mut existing = self.scope.get_scalar(&var_name).to_string();
9045                        while existing.len() < offset {
9046                            existing.push('\0');
9047                        }
9048                        existing.push_str(&read_str);
9049                        let _ = self
9050                            .scope
9051                            .set_scalar(&var_name, PerlValue::string(existing));
9052                    } else {
9053                        let _ = self
9054                            .scope
9055                            .set_scalar(&var_name, PerlValue::string(read_str));
9056                    }
9057                    return Ok(PerlValue::integer(n as i64));
9058                }
9059                if matches!(name.as_str(), "group_by" | "chunk_by") {
9060                    if args.len() != 2 {
9061                        return Err(PerlError::runtime(
9062                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
9063                            line,
9064                        )
9065                        .into());
9066                    }
9067                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
9068                }
9069                if matches!(name.as_str(), "puniq" | "pfirst" | "pany") {
9070                    let mut arg_vals = Vec::with_capacity(args.len());
9071                    for a in args {
9072                        arg_vals.push(self.eval_expr(a)?);
9073                    }
9074                    let saved_wa = self.wantarray_kind;
9075                    self.wantarray_kind = ctx;
9076                    let r = self.eval_par_list_call(name.as_str(), &arg_vals, ctx, line);
9077                    self.wantarray_kind = saved_wa;
9078                    return r.map_err(Into::into);
9079                }
9080                let arg_vals = if matches!(name.as_str(), "any" | "all" | "none" | "first")
9081                    || matches!(
9082                        name.as_str(),
9083                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9084                    )
9085                    || matches!(
9086                        name.as_str(),
9087                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
9088                    ) {
9089                    if args.len() != 2 {
9090                        return Err(PerlError::runtime(
9091                            format!("{}: expected BLOCK, LIST", name),
9092                            line,
9093                        )
9094                        .into());
9095                    }
9096                    let cr = self.eval_expr(&args[0])?;
9097                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
9098                    let mut v = vec![cr];
9099                    v.extend(list_src.to_list());
9100                    v
9101                } else if matches!(
9102                    name.as_str(),
9103                    "zip" | "List::Util::zip" | "List::Util::zip_longest"
9104                ) {
9105                    let mut v = Vec::with_capacity(args.len());
9106                    for a in args {
9107                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9108                    }
9109                    v
9110                } else if matches!(
9111                    name.as_str(),
9112                    "uniq"
9113                        | "distinct"
9114                        | "uniqstr"
9115                        | "uniqint"
9116                        | "uniqnum"
9117                        | "flatten"
9118                        | "set"
9119                        | "list_count"
9120                        | "list_size"
9121                        | "count"
9122                        | "size"
9123                        | "cnt"
9124                        | "with_index"
9125                        | "List::Util::uniq"
9126                        | "List::Util::uniqstr"
9127                        | "List::Util::uniqint"
9128                        | "List::Util::uniqnum"
9129                        | "shuffle"
9130                        | "List::Util::shuffle"
9131                        | "sum"
9132                        | "sum0"
9133                        | "product"
9134                        | "min"
9135                        | "max"
9136                        | "minstr"
9137                        | "maxstr"
9138                        | "mean"
9139                        | "median"
9140                        | "mode"
9141                        | "stddev"
9142                        | "variance"
9143                        | "List::Util::sum"
9144                        | "List::Util::sum0"
9145                        | "List::Util::product"
9146                        | "List::Util::min"
9147                        | "List::Util::max"
9148                        | "List::Util::minstr"
9149                        | "List::Util::maxstr"
9150                        | "List::Util::mean"
9151                        | "List::Util::median"
9152                        | "List::Util::mode"
9153                        | "List::Util::stddev"
9154                        | "List::Util::variance"
9155                        | "pairs"
9156                        | "unpairs"
9157                        | "pairkeys"
9158                        | "pairvalues"
9159                        | "List::Util::pairs"
9160                        | "List::Util::unpairs"
9161                        | "List::Util::pairkeys"
9162                        | "List::Util::pairvalues"
9163                ) {
9164                    // Perl prototype `(@)`: one slurpy list — either one list expr (`uniq @x`) or
9165                    // multiple actuals (`List::Util::uniq(1, 1, 2)`). Each actual is evaluated in
9166                    // list context so `@a, @b` flattens like Perl.
9167                    let mut list_out = Vec::new();
9168                    if args.len() == 1 {
9169                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
9170                    } else {
9171                        for a in args {
9172                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
9173                        }
9174                    }
9175                    list_out
9176                } else if matches!(
9177                    name.as_str(),
9178                    "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail"
9179                ) {
9180                    if args.is_empty() {
9181                        return Err(PerlError::runtime(
9182                            "take/head/tail/drop/List::Util::head|tail: need LIST..., N or unary N",
9183                            line,
9184                        )
9185                        .into());
9186                    }
9187                    let mut arg_vals = Vec::with_capacity(args.len());
9188                    if args.len() == 1 {
9189                        // head @l == head @l, 1 — evaluate in list context
9190                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9191                    } else {
9192                        for a in &args[..args.len() - 1] {
9193                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9194                        }
9195                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
9196                    }
9197                    arg_vals
9198                } else if matches!(
9199                    name.as_str(),
9200                    "chunked" | "List::Util::chunked" | "windowed" | "List::Util::windowed"
9201                ) {
9202                    let mut list_out = Vec::new();
9203                    match args.len() {
9204                        0 => {
9205                            return Err(PerlError::runtime(
9206                                format!("{name}: expected (LIST, N) or unary N after |>"),
9207                                line,
9208                            )
9209                            .into());
9210                        }
9211                        1 => {
9212                            // chunked @l / windowed @l — evaluate in list context, default size
9213                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9214                        }
9215                        2 => {
9216                            list_out.extend(
9217                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
9218                            );
9219                            list_out.push(self.eval_expr(&args[1])?);
9220                        }
9221                        _ => {
9222                            return Err(PerlError::runtime(
9223                                format!(
9224                                    "{name}: expected exactly (LIST, N); use one list expression then size"
9225                                ),
9226                                line,
9227                            )
9228                            .into());
9229                        }
9230                    }
9231                    list_out
9232                } else {
9233                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
9234                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
9235                    let mut arg_vals = Vec::with_capacity(args.len());
9236                    for a in args {
9237                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
9238                        if let Some(items) = v.as_array_vec() {
9239                            arg_vals.extend(items);
9240                        } else {
9241                            arg_vals.push(v);
9242                        }
9243                    }
9244                    arg_vals
9245                };
9246                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
9247                let saved_wa = self.wantarray_kind;
9248                self.wantarray_kind = ctx;
9249                // User-defined subs shadow builtins (correct Perl semantics).
9250                if let Some(sub) = self.resolve_sub_by_name(name) {
9251                    self.wantarray_kind = saved_wa;
9252                    let args = self.with_topic_default_args(arg_vals);
9253                    return self.call_sub(&sub, args, ctx, line);
9254                }
9255                if matches!(
9256                    name.as_str(),
9257                    "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9258                ) {
9259                    let r = self.list_higher_order_block_builtin(name.as_str(), &arg_vals, line);
9260                    self.wantarray_kind = saved_wa;
9261                    return r.map_err(Into::into);
9262                }
9263                if let Some(r) = crate::builtins::try_builtin(self, name.as_str(), &arg_vals, line)
9264                {
9265                    self.wantarray_kind = saved_wa;
9266                    return r.map_err(Into::into);
9267                }
9268                self.wantarray_kind = saved_wa;
9269                self.call_named_sub(name, arg_vals, line, ctx)
9270            }
9271            ExprKind::IndirectCall {
9272                target,
9273                args,
9274                ampersand: _,
9275                pass_caller_arglist,
9276            } => {
9277                let tval = self.eval_expr(target)?;
9278                let arg_vals = if *pass_caller_arglist {
9279                    self.scope.get_array("_")
9280                } else {
9281                    let mut v = Vec::with_capacity(args.len());
9282                    for a in args {
9283                        v.push(self.eval_expr(a)?);
9284                    }
9285                    v
9286                };
9287                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
9288            }
9289            ExprKind::MethodCall {
9290                object,
9291                method,
9292                args,
9293                super_call,
9294            } => {
9295                let obj = self.eval_expr(object)?;
9296                let mut arg_vals = vec![obj.clone()];
9297                for a in args {
9298                    arg_vals.push(self.eval_expr(a)?);
9299                }
9300                if let Some(r) =
9301                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
9302                {
9303                    return r.map_err(Into::into);
9304                }
9305                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
9306                    return r.map_err(Into::into);
9307                }
9308                // Get class name
9309                let class = if let Some(b) = obj.as_blessed_ref() {
9310                    b.class.clone()
9311                } else if let Some(s) = obj.as_str() {
9312                    s // Class->method()
9313                } else {
9314                    return Err(PerlError::runtime("Can't call method on non-object", line).into());
9315                };
9316                if method == "VERSION" && !*super_call {
9317                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
9318                        return Ok(ver);
9319                    }
9320                }
9321                // UNIVERSAL methods: isa, can, DOES
9322                if !*super_call {
9323                    match method.as_str() {
9324                        "isa" => {
9325                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9326                            let mro = self.mro_linearize(&class);
9327                            let result = mro.iter().any(|c| c == &target);
9328                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9329                        }
9330                        "can" => {
9331                            let target_method =
9332                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9333                            let found = self
9334                                .resolve_method_full_name(&class, &target_method, false)
9335                                .and_then(|fq| self.subs.get(&fq))
9336                                .is_some();
9337                            if found {
9338                                return Ok(PerlValue::code_ref(Arc::new(PerlSub {
9339                                    name: target_method,
9340                                    params: vec![],
9341                                    body: vec![],
9342                                    closure_env: None,
9343                                    prototype: None,
9344                                    fib_like: None,
9345                                })));
9346                            } else {
9347                                return Ok(PerlValue::UNDEF);
9348                            }
9349                        }
9350                        "DOES" => {
9351                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9352                            let mro = self.mro_linearize(&class);
9353                            let result = mro.iter().any(|c| c == &target);
9354                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9355                        }
9356                        _ => {}
9357                    }
9358                }
9359                let full_name = self
9360                    .resolve_method_full_name(&class, method, *super_call)
9361                    .ok_or_else(|| {
9362                        PerlError::runtime(
9363                            format!(
9364                                "Can't locate method \"{}\" for invocant \"{}\"",
9365                                method, class
9366                            ),
9367                            line,
9368                        )
9369                    })?;
9370                if let Some(sub) = self.subs.get(&full_name).cloned() {
9371                    self.call_sub(&sub, arg_vals, ctx, line)
9372                } else if method == "new" && !*super_call {
9373                    // Default constructor
9374                    self.builtin_new(&class, arg_vals, line)
9375                } else if let Some(r) =
9376                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
9377                {
9378                    r
9379                } else {
9380                    Err(PerlError::runtime(
9381                        format!(
9382                            "Can't locate method \"{}\" in package \"{}\"",
9383                            method, class
9384                        ),
9385                        line,
9386                    )
9387                    .into())
9388                }
9389            }
9390
9391            // Print/Say/Printf
9392            ExprKind::Print { handle, args } => {
9393                self.exec_print(handle.as_deref(), args, false, line)
9394            }
9395            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
9396            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
9397            ExprKind::Die(args) => {
9398                if args.is_empty() {
9399                    // `die` with no args: re-die with current $@ or "Died"
9400                    let current = self.scope.get_scalar("@");
9401                    let msg = if current.is_undef() || current.to_string().is_empty() {
9402                        let mut m = "Died".to_string();
9403                        m.push_str(&self.die_warn_at_suffix(line));
9404                        m.push('\n');
9405                        m
9406                    } else {
9407                        current.to_string()
9408                    };
9409                    return Err(PerlError::die(msg, line).into());
9410                }
9411                // Single ref argument: store the ref value in $@
9412                if args.len() == 1 {
9413                    let v = self.eval_expr(&args[0])?;
9414                    if v.as_hash_ref().is_some()
9415                        || v.as_blessed_ref().is_some()
9416                        || v.as_array_ref().is_some()
9417                        || v.as_code_ref().is_some()
9418                    {
9419                        let msg = v.to_string();
9420                        return Err(PerlError::die_with_value(v, msg, line).into());
9421                    }
9422                }
9423                let mut msg = String::new();
9424                for a in args {
9425                    let v = self.eval_expr(a)?;
9426                    msg.push_str(&v.to_string());
9427                }
9428                if msg.is_empty() {
9429                    msg = "Died".to_string();
9430                }
9431                if !msg.ends_with('\n') {
9432                    msg.push_str(&self.die_warn_at_suffix(line));
9433                    msg.push('\n');
9434                }
9435                Err(PerlError::die(msg, line).into())
9436            }
9437            ExprKind::Warn(args) => {
9438                let mut msg = String::new();
9439                for a in args {
9440                    let v = self.eval_expr(a)?;
9441                    msg.push_str(&v.to_string());
9442                }
9443                if msg.is_empty() {
9444                    msg = "Warning: something's wrong".to_string();
9445                }
9446                if !msg.ends_with('\n') {
9447                    msg.push_str(&self.die_warn_at_suffix(line));
9448                    msg.push('\n');
9449                }
9450                eprint!("{}", msg);
9451                Ok(PerlValue::integer(1))
9452            }
9453
9454            // Regex
9455            ExprKind::Match {
9456                expr,
9457                pattern,
9458                flags,
9459                scalar_g,
9460                delim: _,
9461            } => {
9462                let val = self.eval_expr(expr)?;
9463                if val.is_iterator() {
9464                    let source = crate::map_stream::into_pull_iter(val);
9465                    let re = self.compile_regex(pattern, flags, line)?;
9466                    let global = flags.contains('g');
9467                    if global {
9468                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9469                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
9470                        )));
9471                    } else {
9472                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9473                            crate::map_stream::MatchStreamIterator::new(source, re),
9474                        )));
9475                    }
9476                }
9477                let s = val.to_string();
9478                let pos_key = match &expr.kind {
9479                    ExprKind::ScalarVar(n) => n.as_str(),
9480                    _ => "_",
9481                };
9482                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
9483            }
9484            ExprKind::Substitution {
9485                expr,
9486                pattern,
9487                replacement,
9488                flags,
9489                delim: _,
9490            } => {
9491                let val = self.eval_expr(expr)?;
9492                if val.is_iterator() {
9493                    let source = crate::map_stream::into_pull_iter(val);
9494                    let re = self.compile_regex(pattern, flags, line)?;
9495                    let global = flags.contains('g');
9496                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9497                        crate::map_stream::SubstStreamIterator::new(
9498                            source,
9499                            re,
9500                            normalize_replacement_backrefs(replacement),
9501                            global,
9502                        ),
9503                    )));
9504                }
9505                let s = val.to_string();
9506                self.regex_subst_execute(
9507                    s,
9508                    pattern,
9509                    replacement.as_str(),
9510                    flags.as_str(),
9511                    expr,
9512                    line,
9513                )
9514            }
9515            ExprKind::Transliterate {
9516                expr,
9517                from,
9518                to,
9519                flags,
9520                delim: _,
9521            } => {
9522                let val = self.eval_expr(expr)?;
9523                if val.is_iterator() {
9524                    let source = crate::map_stream::into_pull_iter(val);
9525                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9526                        crate::map_stream::TransliterateStreamIterator::new(
9527                            source, from, to, flags,
9528                        ),
9529                    )));
9530                }
9531                let s = val.to_string();
9532                self.regex_transliterate_execute(
9533                    s,
9534                    from.as_str(),
9535                    to.as_str(),
9536                    flags.as_str(),
9537                    expr,
9538                    line,
9539                )
9540            }
9541
9542            // List operations
9543            ExprKind::MapExpr {
9544                block,
9545                list,
9546                flatten_array_refs,
9547                stream,
9548            } => {
9549                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9550                if *stream {
9551                    let out =
9552                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
9553                    if ctx == WantarrayCtx::List {
9554                        return Ok(out);
9555                    }
9556                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9557                }
9558                let items = list_val.to_list();
9559                if items.len() == 1 {
9560                    if let Some(p) = items[0].as_pipeline() {
9561                        if *flatten_array_refs {
9562                            return Err(PerlError::runtime(
9563                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
9564                                line,
9565                            )
9566                            .into());
9567                        }
9568                        let sub = self.anon_coderef_from_block(block);
9569                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
9570                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
9571                    }
9572                }
9573                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
9574                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
9575                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
9576                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
9577                let mut result = Vec::new();
9578                for item in items {
9579                    self.scope.set_topic(item);
9580                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
9581                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
9582                }
9583                if ctx == WantarrayCtx::List {
9584                    Ok(PerlValue::array(result))
9585                } else {
9586                    Ok(PerlValue::integer(result.len() as i64))
9587                }
9588            }
9589            ExprKind::ForEachExpr { block, list } => {
9590                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9591                // Lazy: consume iterator one-at-a-time without materializing.
9592                if list_val.is_iterator() {
9593                    let iter = list_val.into_iterator();
9594                    let mut count = 0i64;
9595                    while let Some(item) = iter.next_item() {
9596                        count += 1;
9597                        self.scope.set_topic(item);
9598                        self.exec_block(block)?;
9599                    }
9600                    return Ok(PerlValue::integer(count));
9601                }
9602                let items = list_val.to_list();
9603                let count = items.len();
9604                for item in items {
9605                    self.scope.set_topic(item);
9606                    self.exec_block(block)?;
9607                }
9608                Ok(PerlValue::integer(count as i64))
9609            }
9610            ExprKind::MapExprComma {
9611                expr,
9612                list,
9613                flatten_array_refs,
9614                stream,
9615            } => {
9616                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9617                if *stream {
9618                    let out =
9619                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
9620                    if ctx == WantarrayCtx::List {
9621                        return Ok(out);
9622                    }
9623                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9624                }
9625                let items = list_val.to_list();
9626                let mut result = Vec::new();
9627                for item in items {
9628                    self.scope.set_topic(item.clone());
9629                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
9630                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
9631                }
9632                if ctx == WantarrayCtx::List {
9633                    Ok(PerlValue::array(result))
9634                } else {
9635                    Ok(PerlValue::integer(result.len() as i64))
9636                }
9637            }
9638            ExprKind::GrepExpr {
9639                block,
9640                list,
9641                keyword,
9642            } => {
9643                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9644                if keyword.is_stream() {
9645                    let out = self.filter_stream_block_output(list_val, block, line)?;
9646                    if ctx == WantarrayCtx::List {
9647                        return Ok(out);
9648                    }
9649                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9650                }
9651                let items = list_val.to_list();
9652                if items.len() == 1 {
9653                    if let Some(p) = items[0].as_pipeline() {
9654                        let sub = self.anon_coderef_from_block(block);
9655                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
9656                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
9657                    }
9658                }
9659                let mut result = Vec::new();
9660                for item in items {
9661                    self.scope.set_topic(item.clone());
9662                    let val = self.exec_block(block)?;
9663                    // Bare regex in block → match against $_ (Perl: /pat/ in
9664                    // grep is `$_ =~ /pat/`, not a truthy regex object).
9665                    let keep = if let Some(re) = val.as_regex() {
9666                        re.is_match(&item.to_string())
9667                    } else {
9668                        val.is_true()
9669                    };
9670                    if keep {
9671                        result.push(item);
9672                    }
9673                }
9674                if ctx == WantarrayCtx::List {
9675                    Ok(PerlValue::array(result))
9676                } else {
9677                    Ok(PerlValue::integer(result.len() as i64))
9678                }
9679            }
9680            ExprKind::GrepExprComma {
9681                expr,
9682                list,
9683                keyword,
9684            } => {
9685                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9686                if keyword.is_stream() {
9687                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
9688                    if ctx == WantarrayCtx::List {
9689                        return Ok(out);
9690                    }
9691                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9692                }
9693                let items = list_val.to_list();
9694                let mut result = Vec::new();
9695                for item in items {
9696                    self.scope.set_topic(item.clone());
9697                    let val = self.eval_expr(expr)?;
9698                    let keep = if let Some(re) = val.as_regex() {
9699                        re.is_match(&item.to_string())
9700                    } else {
9701                        val.is_true()
9702                    };
9703                    if keep {
9704                        result.push(item);
9705                    }
9706                }
9707                if ctx == WantarrayCtx::List {
9708                    Ok(PerlValue::array(result))
9709                } else {
9710                    Ok(PerlValue::integer(result.len() as i64))
9711                }
9712            }
9713            ExprKind::SortExpr { cmp, list } => {
9714                let list_val = self.eval_expr(list)?;
9715                let mut items = list_val.to_list();
9716                match cmp {
9717                    Some(SortComparator::Code(code_expr)) => {
9718                        let sub = self.eval_expr(code_expr)?;
9719                        let Some(sub) = sub.as_code_ref() else {
9720                            return Err(PerlError::runtime(
9721                                "sort: comparator must be a code reference",
9722                                line,
9723                            )
9724                            .into());
9725                        };
9726                        let sub = sub.clone();
9727                        items.sort_by(|a, b| {
9728                            let _ = self.scope.set_scalar("a", a.clone());
9729                            let _ = self.scope.set_scalar("b", b.clone());
9730                            let _ = self.scope.set_scalar("_0", a.clone());
9731                            let _ = self.scope.set_scalar("_1", b.clone());
9732                            match self.call_sub(&sub, vec![], ctx, line) {
9733                                Ok(v) => {
9734                                    let n = v.to_int();
9735                                    if n < 0 {
9736                                        Ordering::Less
9737                                    } else if n > 0 {
9738                                        Ordering::Greater
9739                                    } else {
9740                                        Ordering::Equal
9741                                    }
9742                                }
9743                                Err(_) => Ordering::Equal,
9744                            }
9745                        });
9746                    }
9747                    Some(SortComparator::Block(cmp_block)) => {
9748                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
9749                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
9750                        } else {
9751                            let cmp_block = cmp_block.clone();
9752                            items.sort_by(|a, b| {
9753                                let _ = self.scope.set_scalar("a", a.clone());
9754                                let _ = self.scope.set_scalar("b", b.clone());
9755                                let _ = self.scope.set_scalar("_0", a.clone());
9756                                let _ = self.scope.set_scalar("_1", b.clone());
9757                                match self.exec_block(&cmp_block) {
9758                                    Ok(v) => {
9759                                        let n = v.to_int();
9760                                        if n < 0 {
9761                                            Ordering::Less
9762                                        } else if n > 0 {
9763                                            Ordering::Greater
9764                                        } else {
9765                                            Ordering::Equal
9766                                        }
9767                                    }
9768                                    Err(_) => Ordering::Equal,
9769                                }
9770                            });
9771                        }
9772                    }
9773                    None => {
9774                        items.sort_by_key(|a| a.to_string());
9775                    }
9776                }
9777                Ok(PerlValue::array(items))
9778            }
9779            ExprKind::ScalarReverse(expr) => {
9780                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
9781                // Lazy: wrap iterator without materializing
9782                if val.is_iterator() {
9783                    return Ok(PerlValue::iterator(Arc::new(
9784                        crate::value::ScalarReverseIterator::new(val.into_iterator()),
9785                    )));
9786                }
9787                let items = val.to_list();
9788                if items.len() <= 1 {
9789                    let s = if items.is_empty() {
9790                        String::new()
9791                    } else {
9792                        items[0].to_string()
9793                    };
9794                    Ok(PerlValue::string(s.chars().rev().collect()))
9795                } else {
9796                    let mut items = items;
9797                    items.reverse();
9798                    Ok(PerlValue::array(items))
9799                }
9800            }
9801            ExprKind::ReverseExpr(list) => {
9802                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9803                match ctx {
9804                    WantarrayCtx::List => {
9805                        let mut items = val.to_list();
9806                        items.reverse();
9807                        Ok(PerlValue::array(items))
9808                    }
9809                    _ => {
9810                        let items = val.to_list();
9811                        let s: String = items.iter().map(|v| v.to_string()).collect();
9812                        Ok(PerlValue::string(s.chars().rev().collect()))
9813                    }
9814                }
9815            }
9816
9817            // ── Parallel operations (rayon-powered) ──
9818            ExprKind::ParLinesExpr {
9819                path,
9820                callback,
9821                progress,
9822            } => self.eval_par_lines_expr(
9823                path.as_ref(),
9824                callback.as_ref(),
9825                progress.as_deref(),
9826                line,
9827            ),
9828            ExprKind::ParWalkExpr {
9829                path,
9830                callback,
9831                progress,
9832            } => {
9833                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
9834            }
9835            ExprKind::PwatchExpr { path, callback } => {
9836                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
9837            }
9838            ExprKind::PMapExpr {
9839                block,
9840                list,
9841                progress,
9842                flat_outputs,
9843                on_cluster,
9844            } => {
9845                let show_progress = progress
9846                    .as_ref()
9847                    .map(|p| self.eval_expr(p))
9848                    .transpose()?
9849                    .map(|v| v.is_true())
9850                    .unwrap_or(false);
9851                let list_val = self.eval_expr(list)?;
9852                if let Some(cluster_e) = on_cluster {
9853                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
9854                    return self.eval_pmap_remote(
9855                        cluster_val,
9856                        list_val,
9857                        show_progress,
9858                        block,
9859                        *flat_outputs,
9860                        line,
9861                    );
9862                }
9863                let items = list_val.to_list();
9864                let block = block.clone();
9865                let subs = self.subs.clone();
9866                let (scope_capture, atomic_arrays, atomic_hashes) =
9867                    self.scope.capture_with_atomics();
9868                let pmap_progress = PmapProgress::new(show_progress, items.len());
9869
9870                if *flat_outputs {
9871                    let mut indexed: Vec<(usize, Vec<PerlValue>)> = items
9872                        .into_par_iter()
9873                        .enumerate()
9874                        .map(|(i, item)| {
9875                            let mut local_interp = Interpreter::new();
9876                            local_interp.subs = subs.clone();
9877                            local_interp.scope.restore_capture(&scope_capture);
9878                            local_interp
9879                                .scope
9880                                .restore_atomics(&atomic_arrays, &atomic_hashes);
9881                            local_interp.enable_parallel_guard();
9882                            local_interp.scope.set_topic(item);
9883                            let val = match local_interp.exec_block(&block) {
9884                                Ok(val) => val,
9885                                Err(_) => PerlValue::UNDEF,
9886                            };
9887                            let chunk = val.map_flatten_outputs(true);
9888                            pmap_progress.tick();
9889                            (i, chunk)
9890                        })
9891                        .collect();
9892                    pmap_progress.finish();
9893                    indexed.sort_by_key(|(i, _)| *i);
9894                    let results: Vec<PerlValue> =
9895                        indexed.into_iter().flat_map(|(_, v)| v).collect();
9896                    Ok(PerlValue::array(results))
9897                } else {
9898                    let results: Vec<PerlValue> = items
9899                        .into_par_iter()
9900                        .map(|item| {
9901                            let mut local_interp = Interpreter::new();
9902                            local_interp.subs = subs.clone();
9903                            local_interp.scope.restore_capture(&scope_capture);
9904                            local_interp
9905                                .scope
9906                                .restore_atomics(&atomic_arrays, &atomic_hashes);
9907                            local_interp.enable_parallel_guard();
9908                            local_interp.scope.set_topic(item);
9909                            let val = match local_interp.exec_block(&block) {
9910                                Ok(val) => val,
9911                                Err(_) => PerlValue::UNDEF,
9912                            };
9913                            pmap_progress.tick();
9914                            val
9915                        })
9916                        .collect();
9917                    pmap_progress.finish();
9918                    Ok(PerlValue::array(results))
9919                }
9920            }
9921            ExprKind::PMapChunkedExpr {
9922                chunk_size,
9923                block,
9924                list,
9925                progress,
9926            } => {
9927                let show_progress = progress
9928                    .as_ref()
9929                    .map(|p| self.eval_expr(p))
9930                    .transpose()?
9931                    .map(|v| v.is_true())
9932                    .unwrap_or(false);
9933                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
9934                let list_val = self.eval_expr(list)?;
9935                let items = list_val.to_list();
9936                let block = block.clone();
9937                let subs = self.subs.clone();
9938                let (scope_capture, atomic_arrays, atomic_hashes) =
9939                    self.scope.capture_with_atomics();
9940
9941                let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = items
9942                    .chunks(chunk_n)
9943                    .enumerate()
9944                    .map(|(i, c)| (i, c.to_vec()))
9945                    .collect();
9946
9947                let n_chunks = indexed_chunks.len();
9948                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
9949
9950                let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
9951                    .into_par_iter()
9952                    .map(|(chunk_idx, chunk)| {
9953                        let mut local_interp = Interpreter::new();
9954                        local_interp.subs = subs.clone();
9955                        local_interp.scope.restore_capture(&scope_capture);
9956                        local_interp
9957                            .scope
9958                            .restore_atomics(&atomic_arrays, &atomic_hashes);
9959                        local_interp.enable_parallel_guard();
9960                        let mut out = Vec::with_capacity(chunk.len());
9961                        for item in chunk {
9962                            local_interp.scope.set_topic(item);
9963                            match local_interp.exec_block(&block) {
9964                                Ok(val) => out.push(val),
9965                                Err(_) => out.push(PerlValue::UNDEF),
9966                            }
9967                        }
9968                        pmap_progress.tick();
9969                        (chunk_idx, out)
9970                    })
9971                    .collect();
9972
9973                pmap_progress.finish();
9974                chunk_results.sort_by_key(|(i, _)| *i);
9975                let results: Vec<PerlValue> =
9976                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
9977                Ok(PerlValue::array(results))
9978            }
9979            ExprKind::PGrepExpr {
9980                block,
9981                list,
9982                progress,
9983            } => {
9984                let show_progress = progress
9985                    .as_ref()
9986                    .map(|p| self.eval_expr(p))
9987                    .transpose()?
9988                    .map(|v| v.is_true())
9989                    .unwrap_or(false);
9990                let list_val = self.eval_expr(list)?;
9991                let items = list_val.to_list();
9992                let block = block.clone();
9993                let subs = self.subs.clone();
9994                let (scope_capture, atomic_arrays, atomic_hashes) =
9995                    self.scope.capture_with_atomics();
9996                let pmap_progress = PmapProgress::new(show_progress, items.len());
9997
9998                let results: Vec<PerlValue> = items
9999                    .into_par_iter()
10000                    .filter_map(|item| {
10001                        let mut local_interp = Interpreter::new();
10002                        local_interp.subs = subs.clone();
10003                        local_interp.scope.restore_capture(&scope_capture);
10004                        local_interp
10005                            .scope
10006                            .restore_atomics(&atomic_arrays, &atomic_hashes);
10007                        local_interp.enable_parallel_guard();
10008                        local_interp.scope.set_topic(item.clone());
10009                        let keep = match local_interp.exec_block(&block) {
10010                            Ok(val) => val.is_true(),
10011                            Err(_) => false,
10012                        };
10013                        pmap_progress.tick();
10014                        if keep {
10015                            Some(item)
10016                        } else {
10017                            None
10018                        }
10019                    })
10020                    .collect();
10021                pmap_progress.finish();
10022                Ok(PerlValue::array(results))
10023            }
10024            ExprKind::PForExpr {
10025                block,
10026                list,
10027                progress,
10028            } => {
10029                let show_progress = progress
10030                    .as_ref()
10031                    .map(|p| self.eval_expr(p))
10032                    .transpose()?
10033                    .map(|v| v.is_true())
10034                    .unwrap_or(false);
10035                let list_val = self.eval_expr(list)?;
10036                let items = list_val.to_list();
10037                let block = block.clone();
10038                let subs = self.subs.clone();
10039                let (scope_capture, atomic_arrays, atomic_hashes) =
10040                    self.scope.capture_with_atomics();
10041
10042                let pmap_progress = PmapProgress::new(show_progress, items.len());
10043                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10044                items.into_par_iter().for_each(|item| {
10045                    if first_err.lock().is_some() {
10046                        return;
10047                    }
10048                    let mut local_interp = Interpreter::new();
10049                    local_interp.subs = subs.clone();
10050                    local_interp.scope.restore_capture(&scope_capture);
10051                    local_interp
10052                        .scope
10053                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10054                    local_interp.enable_parallel_guard();
10055                    local_interp.scope.set_topic(item);
10056                    match local_interp.exec_block(&block) {
10057                        Ok(_) => {}
10058                        Err(e) => {
10059                            let stryke = match e {
10060                                FlowOrError::Error(stryke) => stryke,
10061                                FlowOrError::Flow(_) => PerlError::runtime(
10062                                    "return/last/next/redo not supported inside pfor block",
10063                                    line,
10064                                ),
10065                            };
10066                            let mut g = first_err.lock();
10067                            if g.is_none() {
10068                                *g = Some(stryke);
10069                            }
10070                        }
10071                    }
10072                    pmap_progress.tick();
10073                });
10074                pmap_progress.finish();
10075                if let Some(e) = first_err.lock().take() {
10076                    return Err(FlowOrError::Error(e));
10077                }
10078                Ok(PerlValue::UNDEF)
10079            }
10080            ExprKind::FanExpr {
10081                count,
10082                block,
10083                progress,
10084                capture,
10085            } => {
10086                let show_progress = progress
10087                    .as_ref()
10088                    .map(|p| self.eval_expr(p))
10089                    .transpose()?
10090                    .map(|v| v.is_true())
10091                    .unwrap_or(false);
10092                let n = match count {
10093                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
10094                    None => self.parallel_thread_count(),
10095                };
10096                let block = block.clone();
10097                let subs = self.subs.clone();
10098                let (scope_capture, atomic_arrays, atomic_hashes) =
10099                    self.scope.capture_with_atomics();
10100
10101                let fan_progress = FanProgress::new(show_progress, n);
10102                if *capture {
10103                    if n == 0 {
10104                        return Ok(PerlValue::array(Vec::new()));
10105                    }
10106                    let pairs: Vec<(usize, ExecResult)> = (0..n)
10107                        .into_par_iter()
10108                        .map(|i| {
10109                            fan_progress.start_worker(i);
10110                            let mut local_interp = Interpreter::new();
10111                            local_interp.subs = subs.clone();
10112                            local_interp.suppress_stdout = show_progress;
10113                            local_interp.scope.restore_capture(&scope_capture);
10114                            local_interp
10115                                .scope
10116                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10117                            local_interp.enable_parallel_guard();
10118                            local_interp.scope.set_topic(PerlValue::integer(i as i64));
10119                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10120                            let res = local_interp.exec_block(&block);
10121                            crate::parallel_trace::fan_worker_set_index(None);
10122                            fan_progress.finish_worker(i);
10123                            (i, res)
10124                        })
10125                        .collect();
10126                    fan_progress.finish();
10127                    let mut pairs = pairs;
10128                    pairs.sort_by_key(|(i, _)| *i);
10129                    let mut out = Vec::with_capacity(n);
10130                    for (_, r) in pairs {
10131                        match r {
10132                            Ok(v) => out.push(v),
10133                            Err(e) => return Err(e),
10134                        }
10135                    }
10136                    return Ok(PerlValue::array(out));
10137                }
10138                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10139                (0..n).into_par_iter().for_each(|i| {
10140                    if first_err.lock().is_some() {
10141                        return;
10142                    }
10143                    fan_progress.start_worker(i);
10144                    let mut local_interp = Interpreter::new();
10145                    local_interp.subs = subs.clone();
10146                    local_interp.suppress_stdout = show_progress;
10147                    local_interp.scope.restore_capture(&scope_capture);
10148                    local_interp
10149                        .scope
10150                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10151                    local_interp.enable_parallel_guard();
10152                    local_interp.scope.set_topic(PerlValue::integer(i as i64));
10153                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10154                    match local_interp.exec_block(&block) {
10155                        Ok(_) => {}
10156                        Err(e) => {
10157                            let stryke = match e {
10158                                FlowOrError::Error(stryke) => stryke,
10159                                FlowOrError::Flow(_) => PerlError::runtime(
10160                                    "return/last/next/redo not supported inside fan block",
10161                                    line,
10162                                ),
10163                            };
10164                            let mut g = first_err.lock();
10165                            if g.is_none() {
10166                                *g = Some(stryke);
10167                            }
10168                        }
10169                    }
10170                    crate::parallel_trace::fan_worker_set_index(None);
10171                    fan_progress.finish_worker(i);
10172                });
10173                fan_progress.finish();
10174                if let Some(e) = first_err.lock().take() {
10175                    return Err(FlowOrError::Error(e));
10176                }
10177                Ok(PerlValue::UNDEF)
10178            }
10179            ExprKind::RetryBlock {
10180                body,
10181                times,
10182                backoff,
10183            } => self.eval_retry_block(body, times, *backoff, line),
10184            ExprKind::RateLimitBlock {
10185                slot,
10186                max,
10187                window,
10188                body,
10189            } => self.eval_rate_limit_block(*slot, max, window, body, line),
10190            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
10191            ExprKind::GenBlock { body } => {
10192                let g = Arc::new(PerlGenerator {
10193                    block: body.clone(),
10194                    pc: Mutex::new(0),
10195                    scope_started: Mutex::new(false),
10196                    exhausted: Mutex::new(false),
10197                });
10198                Ok(PerlValue::generator(g))
10199            }
10200            ExprKind::Yield(e) => {
10201                if !self.in_generator {
10202                    return Err(PerlError::runtime("yield outside gen block", line).into());
10203                }
10204                let v = self.eval_expr(e)?;
10205                Err(FlowOrError::Flow(Flow::Yield(v)))
10206            }
10207            ExprKind::AlgebraicMatch { subject, arms } => {
10208                self.eval_algebraic_match(subject, arms, line)
10209            }
10210            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
10211                Ok(self.spawn_async_block(body))
10212            }
10213            ExprKind::Trace { body } => {
10214                crate::parallel_trace::trace_enter();
10215                let out = self.exec_block(body);
10216                crate::parallel_trace::trace_leave();
10217                out
10218            }
10219            ExprKind::Spinner { message, body } => {
10220                use std::io::Write as _;
10221                let msg = self.eval_expr(message)?.to_string();
10222                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
10223                let done2 = done.clone();
10224                let handle = std::thread::spawn(move || {
10225                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
10226                    let mut i = 0;
10227                    let stderr = std::io::stderr();
10228                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
10229                        {
10230                            let stdout = std::io::stdout();
10231                            let _stdout_lock = stdout.lock();
10232                            let mut err = stderr.lock();
10233                            let _ = write!(
10234                                err,
10235                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
10236                                frames[i % frames.len()],
10237                                msg
10238                            );
10239                            let _ = err.flush();
10240                        }
10241                        std::thread::sleep(std::time::Duration::from_millis(80));
10242                        i += 1;
10243                    }
10244                    let mut err = stderr.lock();
10245                    let _ = write!(err, "\r\x1b[2K");
10246                    let _ = err.flush();
10247                });
10248                let result = self.exec_block(body);
10249                done.store(true, std::sync::atomic::Ordering::Relaxed);
10250                let _ = handle.join();
10251                result
10252            }
10253            ExprKind::Timer { body } => {
10254                let start = std::time::Instant::now();
10255                self.exec_block(body)?;
10256                let ms = start.elapsed().as_secs_f64() * 1000.0;
10257                Ok(PerlValue::float(ms))
10258            }
10259            ExprKind::Bench { body, times } => {
10260                let n = self.eval_expr(times)?.to_int();
10261                if n < 0 {
10262                    return Err(PerlError::runtime(
10263                        "bench: iteration count must be non-negative",
10264                        line,
10265                    )
10266                    .into());
10267                }
10268                self.run_bench_block(body, n as usize, line)
10269            }
10270            ExprKind::Await(expr) => {
10271                let v = self.eval_expr(expr)?;
10272                if let Some(t) = v.as_async_task() {
10273                    t.await_result().map_err(FlowOrError::from)
10274                } else {
10275                    Ok(v)
10276                }
10277            }
10278            ExprKind::Slurp(e) => {
10279                let path = self.eval_expr(e)?.to_string();
10280                read_file_text_perl_compat(&path)
10281                    .map(PerlValue::string)
10282                    .map_err(|e| {
10283                        FlowOrError::Error(PerlError::runtime(format!("slurp: {}", e), line))
10284                    })
10285            }
10286            ExprKind::Capture(e) => {
10287                let cmd = self.eval_expr(e)?.to_string();
10288                let output = Command::new("sh")
10289                    .arg("-c")
10290                    .arg(&cmd)
10291                    .output()
10292                    .map_err(|e| {
10293                        FlowOrError::Error(PerlError::runtime(format!("capture: {}", e), line))
10294                    })?;
10295                self.record_child_exit_status(output.status);
10296                let exitcode = output.status.code().unwrap_or(-1) as i64;
10297                let stdout = decode_utf8_or_latin1(&output.stdout);
10298                let stderr = decode_utf8_or_latin1(&output.stderr);
10299                Ok(PerlValue::capture(Arc::new(CaptureResult {
10300                    stdout,
10301                    stderr,
10302                    exitcode,
10303                })))
10304            }
10305            ExprKind::Qx(e) => {
10306                let cmd = self.eval_expr(e)?.to_string();
10307                crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)
10308            }
10309            ExprKind::FetchUrl(e) => {
10310                let url = self.eval_expr(e)?.to_string();
10311                ureq::get(&url)
10312                    .call()
10313                    .map_err(|e| {
10314                        FlowOrError::Error(PerlError::runtime(format!("fetch_url: {}", e), line))
10315                    })
10316                    .and_then(|r| {
10317                        r.into_string().map(PerlValue::string).map_err(|e| {
10318                            FlowOrError::Error(PerlError::runtime(
10319                                format!("fetch_url: {}", e),
10320                                line,
10321                            ))
10322                        })
10323                    })
10324            }
10325            ExprKind::Pchannel { capacity } => {
10326                if let Some(c) = capacity {
10327                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
10328                    Ok(crate::pchannel::create_bounded_pair(n))
10329                } else {
10330                    Ok(crate::pchannel::create_pair())
10331                }
10332            }
10333            ExprKind::PSortExpr {
10334                cmp,
10335                list,
10336                progress,
10337            } => {
10338                let show_progress = progress
10339                    .as_ref()
10340                    .map(|p| self.eval_expr(p))
10341                    .transpose()?
10342                    .map(|v| v.is_true())
10343                    .unwrap_or(false);
10344                let list_val = self.eval_expr(list)?;
10345                let mut items = list_val.to_list();
10346                let pmap_progress = PmapProgress::new(show_progress, 2);
10347                pmap_progress.tick();
10348                if let Some(cmp_block) = cmp {
10349                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
10350                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
10351                    } else {
10352                        let cmp_block = cmp_block.clone();
10353                        let subs = self.subs.clone();
10354                        let scope_capture = self.scope.capture();
10355                        items.par_sort_by(|a, b| {
10356                            let mut local_interp = Interpreter::new();
10357                            local_interp.subs = subs.clone();
10358                            local_interp.scope.restore_capture(&scope_capture);
10359                            let _ = local_interp.scope.set_scalar("a", a.clone());
10360                            let _ = local_interp.scope.set_scalar("b", b.clone());
10361                            let _ = local_interp.scope.set_scalar("_0", a.clone());
10362                            let _ = local_interp.scope.set_scalar("_1", b.clone());
10363                            match local_interp.exec_block(&cmp_block) {
10364                                Ok(v) => {
10365                                    let n = v.to_int();
10366                                    if n < 0 {
10367                                        std::cmp::Ordering::Less
10368                                    } else if n > 0 {
10369                                        std::cmp::Ordering::Greater
10370                                    } else {
10371                                        std::cmp::Ordering::Equal
10372                                    }
10373                                }
10374                                Err(_) => std::cmp::Ordering::Equal,
10375                            }
10376                        });
10377                    }
10378                } else {
10379                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
10380                }
10381                pmap_progress.tick();
10382                pmap_progress.finish();
10383                Ok(PerlValue::array(items))
10384            }
10385
10386            ExprKind::ReduceExpr { block, list } => {
10387                let list_val = self.eval_expr(list)?;
10388                let items = list_val.to_list();
10389                if items.is_empty() {
10390                    return Ok(PerlValue::UNDEF);
10391                }
10392                if items.len() == 1 {
10393                    return Ok(items.into_iter().next().unwrap());
10394                }
10395                let block = block.clone();
10396                let subs = self.subs.clone();
10397                let scope_capture = self.scope.capture();
10398                let mut acc = items[0].clone();
10399                for b in items.into_iter().skip(1) {
10400                    let mut local_interp = Interpreter::new();
10401                    local_interp.subs = subs.clone();
10402                    local_interp.scope.restore_capture(&scope_capture);
10403                    let _ = local_interp.scope.set_scalar("a", acc.clone());
10404                    let _ = local_interp.scope.set_scalar("b", b.clone());
10405                    let _ = local_interp.scope.set_scalar("_0", acc);
10406                    let _ = local_interp.scope.set_scalar("_1", b);
10407                    acc = match local_interp.exec_block(&block) {
10408                        Ok(val) => val,
10409                        Err(_) => PerlValue::UNDEF,
10410                    };
10411                }
10412                Ok(acc)
10413            }
10414
10415            ExprKind::PReduceExpr {
10416                block,
10417                list,
10418                progress,
10419            } => {
10420                let show_progress = progress
10421                    .as_ref()
10422                    .map(|p| self.eval_expr(p))
10423                    .transpose()?
10424                    .map(|v| v.is_true())
10425                    .unwrap_or(false);
10426                let list_val = self.eval_expr(list)?;
10427                let items = list_val.to_list();
10428                if items.is_empty() {
10429                    return Ok(PerlValue::UNDEF);
10430                }
10431                if items.len() == 1 {
10432                    return Ok(items.into_iter().next().unwrap());
10433                }
10434                let block = block.clone();
10435                let subs = self.subs.clone();
10436                let scope_capture = self.scope.capture();
10437                let pmap_progress = PmapProgress::new(show_progress, items.len());
10438
10439                let result = items
10440                    .into_par_iter()
10441                    .map(|x| {
10442                        pmap_progress.tick();
10443                        x
10444                    })
10445                    .reduce_with(|a, b| {
10446                        let mut local_interp = Interpreter::new();
10447                        local_interp.subs = subs.clone();
10448                        local_interp.scope.restore_capture(&scope_capture);
10449                        let _ = local_interp.scope.set_scalar("a", a.clone());
10450                        let _ = local_interp.scope.set_scalar("b", b.clone());
10451                        let _ = local_interp.scope.set_scalar("_0", a);
10452                        let _ = local_interp.scope.set_scalar("_1", b);
10453                        match local_interp.exec_block(&block) {
10454                            Ok(val) => val,
10455                            Err(_) => PerlValue::UNDEF,
10456                        }
10457                    });
10458                pmap_progress.finish();
10459                Ok(result.unwrap_or(PerlValue::UNDEF))
10460            }
10461
10462            ExprKind::PReduceInitExpr {
10463                init,
10464                block,
10465                list,
10466                progress,
10467            } => {
10468                let show_progress = progress
10469                    .as_ref()
10470                    .map(|p| self.eval_expr(p))
10471                    .transpose()?
10472                    .map(|v| v.is_true())
10473                    .unwrap_or(false);
10474                let init_val = self.eval_expr(init)?;
10475                let list_val = self.eval_expr(list)?;
10476                let items = list_val.to_list();
10477                if items.is_empty() {
10478                    return Ok(init_val);
10479                }
10480                let block = block.clone();
10481                let subs = self.subs.clone();
10482                let scope_capture = self.scope.capture();
10483                let cap: &[(String, PerlValue)] = scope_capture.as_slice();
10484                if items.len() == 1 {
10485                    return Ok(fold_preduce_init_step(
10486                        &subs,
10487                        cap,
10488                        &block,
10489                        preduce_init_fold_identity(&init_val),
10490                        items.into_iter().next().unwrap(),
10491                    ));
10492                }
10493                let pmap_progress = PmapProgress::new(show_progress, items.len());
10494                let result = items
10495                    .into_par_iter()
10496                    .fold(
10497                        || preduce_init_fold_identity(&init_val),
10498                        |acc, item| {
10499                            pmap_progress.tick();
10500                            fold_preduce_init_step(&subs, cap, &block, acc, item)
10501                        },
10502                    )
10503                    .reduce(
10504                        || preduce_init_fold_identity(&init_val),
10505                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
10506                    );
10507                pmap_progress.finish();
10508                Ok(result)
10509            }
10510
10511            ExprKind::PMapReduceExpr {
10512                map_block,
10513                reduce_block,
10514                list,
10515                progress,
10516            } => {
10517                let show_progress = progress
10518                    .as_ref()
10519                    .map(|p| self.eval_expr(p))
10520                    .transpose()?
10521                    .map(|v| v.is_true())
10522                    .unwrap_or(false);
10523                let list_val = self.eval_expr(list)?;
10524                let items = list_val.to_list();
10525                if items.is_empty() {
10526                    return Ok(PerlValue::UNDEF);
10527                }
10528                let map_block = map_block.clone();
10529                let reduce_block = reduce_block.clone();
10530                let subs = self.subs.clone();
10531                let scope_capture = self.scope.capture();
10532                if items.len() == 1 {
10533                    let mut local_interp = Interpreter::new();
10534                    local_interp.subs = subs.clone();
10535                    local_interp.scope.restore_capture(&scope_capture);
10536                    local_interp.scope.set_topic(items[0].clone());
10537                    return match local_interp.exec_block_no_scope(&map_block) {
10538                        Ok(v) => Ok(v),
10539                        Err(_) => Ok(PerlValue::UNDEF),
10540                    };
10541                }
10542                let pmap_progress = PmapProgress::new(show_progress, items.len());
10543                let result = items
10544                    .into_par_iter()
10545                    .map(|item| {
10546                        let mut local_interp = Interpreter::new();
10547                        local_interp.subs = subs.clone();
10548                        local_interp.scope.restore_capture(&scope_capture);
10549                        local_interp.scope.set_topic(item);
10550                        let val = match local_interp.exec_block_no_scope(&map_block) {
10551                            Ok(val) => val,
10552                            Err(_) => PerlValue::UNDEF,
10553                        };
10554                        pmap_progress.tick();
10555                        val
10556                    })
10557                    .reduce_with(|a, b| {
10558                        let mut local_interp = Interpreter::new();
10559                        local_interp.subs = subs.clone();
10560                        local_interp.scope.restore_capture(&scope_capture);
10561                        let _ = local_interp.scope.set_scalar("a", a.clone());
10562                        let _ = local_interp.scope.set_scalar("b", b.clone());
10563                        let _ = local_interp.scope.set_scalar("_0", a);
10564                        let _ = local_interp.scope.set_scalar("_1", b);
10565                        match local_interp.exec_block_no_scope(&reduce_block) {
10566                            Ok(val) => val,
10567                            Err(_) => PerlValue::UNDEF,
10568                        }
10569                    });
10570                pmap_progress.finish();
10571                Ok(result.unwrap_or(PerlValue::UNDEF))
10572            }
10573
10574            ExprKind::PcacheExpr {
10575                block,
10576                list,
10577                progress,
10578            } => {
10579                let show_progress = progress
10580                    .as_ref()
10581                    .map(|p| self.eval_expr(p))
10582                    .transpose()?
10583                    .map(|v| v.is_true())
10584                    .unwrap_or(false);
10585                let list_val = self.eval_expr(list)?;
10586                let items = list_val.to_list();
10587                let block = block.clone();
10588                let subs = self.subs.clone();
10589                let scope_capture = self.scope.capture();
10590                let cache = &*crate::pcache::GLOBAL_PCACHE;
10591                let pmap_progress = PmapProgress::new(show_progress, items.len());
10592                let results: Vec<PerlValue> = items
10593                    .into_par_iter()
10594                    .map(|item| {
10595                        let k = crate::pcache::cache_key(&item);
10596                        if let Some(v) = cache.get(&k) {
10597                            pmap_progress.tick();
10598                            return v.clone();
10599                        }
10600                        let mut local_interp = Interpreter::new();
10601                        local_interp.subs = subs.clone();
10602                        local_interp.scope.restore_capture(&scope_capture);
10603                        local_interp.scope.set_topic(item.clone());
10604                        let val = match local_interp.exec_block_no_scope(&block) {
10605                            Ok(v) => v,
10606                            Err(_) => PerlValue::UNDEF,
10607                        };
10608                        cache.insert(k, val.clone());
10609                        pmap_progress.tick();
10610                        val
10611                    })
10612                    .collect();
10613                pmap_progress.finish();
10614                Ok(PerlValue::array(results))
10615            }
10616
10617            ExprKind::PselectExpr { receivers, timeout } => {
10618                let mut rx_vals = Vec::with_capacity(receivers.len());
10619                for r in receivers {
10620                    rx_vals.push(self.eval_expr(r)?);
10621                }
10622                let dur = if let Some(t) = timeout.as_ref() {
10623                    Some(std::time::Duration::from_secs_f64(
10624                        self.eval_expr(t)?.to_number().max(0.0),
10625                    ))
10626                } else {
10627                    None
10628                };
10629                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
10630                    &rx_vals, dur, line,
10631                )?)
10632            }
10633
10634            // Array ops
10635            ExprKind::Push { array, values } => {
10636                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
10637            }
10638            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
10639            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
10640            ExprKind::Unshift { array, values } => {
10641                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
10642            }
10643            ExprKind::Splice {
10644                array,
10645                offset,
10646                length,
10647                replacement,
10648            } => self.eval_splice_expr(
10649                array.as_ref(),
10650                offset.as_deref(),
10651                length.as_deref(),
10652                replacement.as_slice(),
10653                ctx,
10654                line,
10655            ),
10656            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
10657            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
10658            ExprKind::Keys(expr) => {
10659                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10660                let keys = Self::keys_from_value(val, line)?;
10661                if ctx == WantarrayCtx::List {
10662                    Ok(keys)
10663                } else {
10664                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
10665                    Ok(PerlValue::integer(n as i64))
10666                }
10667            }
10668            ExprKind::Values(expr) => {
10669                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10670                let vals = Self::values_from_value(val, line)?;
10671                if ctx == WantarrayCtx::List {
10672                    Ok(vals)
10673                } else {
10674                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
10675                    Ok(PerlValue::integer(n as i64))
10676                }
10677            }
10678            ExprKind::Each(_) => {
10679                // Simplified: returns empty list (full iterator state would need more work)
10680                Ok(PerlValue::array(vec![]))
10681            }
10682
10683            // String ops
10684            ExprKind::Chomp(expr) => {
10685                let val = self.eval_expr(expr)?;
10686                self.chomp_inplace_execute(val, expr)
10687            }
10688            ExprKind::Chop(expr) => {
10689                let val = self.eval_expr(expr)?;
10690                self.chop_inplace_execute(val, expr)
10691            }
10692            ExprKind::Length(expr) => {
10693                let val = self.eval_expr(expr)?;
10694                Ok(if let Some(a) = val.as_array_vec() {
10695                    PerlValue::integer(a.len() as i64)
10696                } else if let Some(h) = val.as_hash_map() {
10697                    PerlValue::integer(h.len() as i64)
10698                } else if let Some(b) = val.as_bytes_arc() {
10699                    PerlValue::integer(b.len() as i64)
10700                } else {
10701                    PerlValue::integer(val.to_string().len() as i64)
10702                })
10703            }
10704            ExprKind::Substr {
10705                string,
10706                offset,
10707                length,
10708                replacement,
10709            } => self.eval_substr_expr(
10710                string.as_ref(),
10711                offset.as_ref(),
10712                length.as_deref(),
10713                replacement.as_deref(),
10714                line,
10715            ),
10716            ExprKind::Index {
10717                string,
10718                substr,
10719                position,
10720            } => {
10721                let s = self.eval_expr(string)?.to_string();
10722                let sub = self.eval_expr(substr)?.to_string();
10723                let pos = if let Some(p) = position {
10724                    self.eval_expr(p)?.to_int() as usize
10725                } else {
10726                    0
10727                };
10728                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
10729                Ok(PerlValue::integer(result))
10730            }
10731            ExprKind::Rindex {
10732                string,
10733                substr,
10734                position,
10735            } => {
10736                let s = self.eval_expr(string)?.to_string();
10737                let sub = self.eval_expr(substr)?.to_string();
10738                let end = if let Some(p) = position {
10739                    self.eval_expr(p)?.to_int() as usize + sub.len()
10740                } else {
10741                    s.len()
10742                };
10743                let search = &s[..end.min(s.len())];
10744                let result = search.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
10745                Ok(PerlValue::integer(result))
10746            }
10747            ExprKind::Sprintf { format, args } => {
10748                let fmt = self.eval_expr(format)?.to_string();
10749                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
10750                // builtins into individual format arguments.
10751                let mut arg_vals = Vec::new();
10752                for a in args {
10753                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
10754                    if let Some(items) = v.as_array_vec() {
10755                        arg_vals.extend(items);
10756                    } else {
10757                        arg_vals.push(v);
10758                    }
10759                }
10760                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
10761                Ok(PerlValue::string(s))
10762            }
10763            ExprKind::JoinExpr { separator, list } => {
10764                let sep = self.eval_expr(separator)?.to_string();
10765                // Like Perl 5, arguments after the separator are evaluated in list context so
10766                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
10767                // expands `localtime` to nine fields.
10768                let items = if let ExprKind::List(exprs) = &list.kind {
10769                    let saved = self.wantarray_kind;
10770                    self.wantarray_kind = WantarrayCtx::List;
10771                    let mut vals = Vec::new();
10772                    for e in exprs {
10773                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
10774                        if let Some(items) = v.as_array_vec() {
10775                            vals.extend(items);
10776                        } else {
10777                            vals.push(v);
10778                        }
10779                    }
10780                    self.wantarray_kind = saved;
10781                    vals
10782                } else {
10783                    let saved = self.wantarray_kind;
10784                    self.wantarray_kind = WantarrayCtx::List;
10785                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10786                    self.wantarray_kind = saved;
10787                    if let Some(items) = v.as_array_vec() {
10788                        items
10789                    } else {
10790                        vec![v]
10791                    }
10792                };
10793                let mut strs = Vec::with_capacity(items.len());
10794                for v in &items {
10795                    strs.push(self.stringify_value(v.clone(), line)?);
10796                }
10797                Ok(PerlValue::string(strs.join(&sep)))
10798            }
10799            ExprKind::SplitExpr {
10800                pattern,
10801                string,
10802                limit,
10803            } => {
10804                let pat = self.eval_expr(pattern)?.to_string();
10805                let s = self.eval_expr(string)?.to_string();
10806                let lim = if let Some(l) = limit {
10807                    self.eval_expr(l)?.to_int() as usize
10808                } else {
10809                    0
10810                };
10811                let re = self.compile_regex(&pat, "", line)?;
10812                let parts: Vec<PerlValue> = if lim > 0 {
10813                    re.splitn_strings(&s, lim)
10814                        .into_iter()
10815                        .map(PerlValue::string)
10816                        .collect()
10817                } else {
10818                    re.split_strings(&s)
10819                        .into_iter()
10820                        .map(PerlValue::string)
10821                        .collect()
10822                };
10823                Ok(PerlValue::array(parts))
10824            }
10825
10826            // Numeric
10827            ExprKind::Abs(expr) => {
10828                let val = self.eval_expr(expr)?;
10829                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
10830                    return r;
10831                }
10832                Ok(PerlValue::float(val.to_number().abs()))
10833            }
10834            ExprKind::Int(expr) => {
10835                let val = self.eval_expr(expr)?;
10836                Ok(PerlValue::integer(val.to_number() as i64))
10837            }
10838            ExprKind::Sqrt(expr) => {
10839                let val = self.eval_expr(expr)?;
10840                Ok(PerlValue::float(val.to_number().sqrt()))
10841            }
10842            ExprKind::Sin(expr) => {
10843                let val = self.eval_expr(expr)?;
10844                Ok(PerlValue::float(val.to_number().sin()))
10845            }
10846            ExprKind::Cos(expr) => {
10847                let val = self.eval_expr(expr)?;
10848                Ok(PerlValue::float(val.to_number().cos()))
10849            }
10850            ExprKind::Atan2 { y, x } => {
10851                let yv = self.eval_expr(y)?.to_number();
10852                let xv = self.eval_expr(x)?.to_number();
10853                Ok(PerlValue::float(yv.atan2(xv)))
10854            }
10855            ExprKind::Exp(expr) => {
10856                let val = self.eval_expr(expr)?;
10857                Ok(PerlValue::float(val.to_number().exp()))
10858            }
10859            ExprKind::Log(expr) => {
10860                let val = self.eval_expr(expr)?;
10861                Ok(PerlValue::float(val.to_number().ln()))
10862            }
10863            ExprKind::Rand(upper) => {
10864                let u = match upper {
10865                    Some(e) => self.eval_expr(e)?.to_number(),
10866                    None => 1.0,
10867                };
10868                Ok(PerlValue::float(self.perl_rand(u)))
10869            }
10870            ExprKind::Srand(seed) => {
10871                let s = match seed {
10872                    Some(e) => Some(self.eval_expr(e)?.to_number()),
10873                    None => None,
10874                };
10875                Ok(PerlValue::integer(self.perl_srand(s)))
10876            }
10877            ExprKind::Hex(expr) => {
10878                let val = self.eval_expr(expr)?.to_string();
10879                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
10880                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
10881                Ok(PerlValue::integer(n))
10882            }
10883            ExprKind::Oct(expr) => {
10884                let val = self.eval_expr(expr)?.to_string();
10885                let s = val.trim();
10886                let n = if s.starts_with("0x") || s.starts_with("0X") {
10887                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
10888                } else if s.starts_with("0b") || s.starts_with("0B") {
10889                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
10890                } else {
10891                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
10892                };
10893                Ok(PerlValue::integer(n))
10894            }
10895
10896            // Case
10897            ExprKind::Lc(expr) => Ok(PerlValue::string(
10898                self.eval_expr(expr)?.to_string().to_lowercase(),
10899            )),
10900            ExprKind::Uc(expr) => Ok(PerlValue::string(
10901                self.eval_expr(expr)?.to_string().to_uppercase(),
10902            )),
10903            ExprKind::Lcfirst(expr) => {
10904                let s = self.eval_expr(expr)?.to_string();
10905                let mut chars = s.chars();
10906                let result = match chars.next() {
10907                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
10908                    None => String::new(),
10909                };
10910                Ok(PerlValue::string(result))
10911            }
10912            ExprKind::Ucfirst(expr) => {
10913                let s = self.eval_expr(expr)?.to_string();
10914                let mut chars = s.chars();
10915                let result = match chars.next() {
10916                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
10917                    None => String::new(),
10918                };
10919                Ok(PerlValue::string(result))
10920            }
10921            ExprKind::Fc(expr) => Ok(PerlValue::string(default_case_fold_str(
10922                &self.eval_expr(expr)?.to_string(),
10923            ))),
10924            ExprKind::Crypt { plaintext, salt } => {
10925                let p = self.eval_expr(plaintext)?.to_string();
10926                let sl = self.eval_expr(salt)?.to_string();
10927                Ok(PerlValue::string(perl_crypt(&p, &sl)))
10928            }
10929            ExprKind::Pos(e) => {
10930                let key = match e {
10931                    None => "_".to_string(),
10932                    Some(expr) => match &expr.kind {
10933                        ExprKind::ScalarVar(n) => n.clone(),
10934                        _ => self.eval_expr(expr)?.to_string(),
10935                    },
10936                };
10937                Ok(self
10938                    .regex_pos
10939                    .get(&key)
10940                    .copied()
10941                    .flatten()
10942                    .map(|p| PerlValue::integer(p as i64))
10943                    .unwrap_or(PerlValue::UNDEF))
10944            }
10945            ExprKind::Study(expr) => {
10946                let s = self.eval_expr(expr)?.to_string();
10947                Ok(Self::study_return_value(&s))
10948            }
10949
10950            // Type
10951            ExprKind::Defined(expr) => {
10952                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
10953                if let ExprKind::SubroutineRef(name) = &expr.kind {
10954                    let exists = self.resolve_sub_by_name(name).is_some();
10955                    return Ok(PerlValue::integer(if exists { 1 } else { 0 }));
10956                }
10957                let val = self.eval_expr(expr)?;
10958                Ok(PerlValue::integer(if val.is_undef() { 0 } else { 1 }))
10959            }
10960            ExprKind::Ref(expr) => {
10961                let val = self.eval_expr(expr)?;
10962                Ok(val.ref_type())
10963            }
10964            ExprKind::ScalarContext(expr) => {
10965                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
10966                Ok(v.scalar_context())
10967            }
10968
10969            // Char
10970            ExprKind::Chr(expr) => {
10971                let n = self.eval_expr(expr)?.to_int() as u32;
10972                Ok(PerlValue::string(
10973                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
10974                ))
10975            }
10976            ExprKind::Ord(expr) => {
10977                let s = self.eval_expr(expr)?.to_string();
10978                Ok(PerlValue::integer(
10979                    s.chars().next().map(|c| c as i64).unwrap_or(0),
10980                ))
10981            }
10982
10983            // I/O
10984            ExprKind::OpenMyHandle { .. } => Err(PerlError::runtime(
10985                "internal: `open my $fh` handle used outside open()",
10986                line,
10987            )
10988            .into()),
10989            ExprKind::Open { handle, mode, file } => {
10990                if let ExprKind::OpenMyHandle { name } = &handle.kind {
10991                    self.scope
10992                        .declare_scalar_frozen(name, PerlValue::UNDEF, false, None)?;
10993                    self.english_note_lexical_scalar(name);
10994                    let mode_s = self.eval_expr(mode)?.to_string();
10995                    let file_opt = if let Some(f) = file {
10996                        Some(self.eval_expr(f)?.to_string())
10997                    } else {
10998                        None
10999                    };
11000                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
11001                    self.scope.set_scalar(name, ret.clone())?;
11002                    return Ok(ret);
11003                }
11004                let handle_s = self.eval_expr(handle)?.to_string();
11005                let handle_name = self.resolve_io_handle_name(&handle_s);
11006                let mode_s = self.eval_expr(mode)?.to_string();
11007                let file_opt = if let Some(f) = file {
11008                    Some(self.eval_expr(f)?.to_string())
11009                } else {
11010                    None
11011                };
11012                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
11013                    .map_err(Into::into)
11014            }
11015            ExprKind::Close(expr) => {
11016                let s = self.eval_expr(expr)?.to_string();
11017                let name = self.resolve_io_handle_name(&s);
11018                self.close_builtin_execute(name).map_err(Into::into)
11019            }
11020            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
11021                self.readline_builtin_execute_list(handle.as_deref())
11022            } else {
11023                self.readline_builtin_execute(handle.as_deref())
11024            }
11025            .map_err(Into::into),
11026            ExprKind::Eof(expr) => match expr {
11027                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
11028                Some(e) => {
11029                    let name = self.eval_expr(e)?;
11030                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
11031                }
11032            },
11033
11034            ExprKind::Opendir { handle, path } => {
11035                let h = self.eval_expr(handle)?.to_string();
11036                let p = self.eval_expr(path)?.to_string();
11037                Ok(self.opendir_handle(&h, &p))
11038            }
11039            ExprKind::Readdir(e) => {
11040                let h = self.eval_expr(e)?.to_string();
11041                Ok(if ctx == WantarrayCtx::List {
11042                    self.readdir_handle_list(&h)
11043                } else {
11044                    self.readdir_handle(&h)
11045                })
11046            }
11047            ExprKind::Closedir(e) => {
11048                let h = self.eval_expr(e)?.to_string();
11049                Ok(self.closedir_handle(&h))
11050            }
11051            ExprKind::Rewinddir(e) => {
11052                let h = self.eval_expr(e)?.to_string();
11053                Ok(self.rewinddir_handle(&h))
11054            }
11055            ExprKind::Telldir(e) => {
11056                let h = self.eval_expr(e)?.to_string();
11057                Ok(self.telldir_handle(&h))
11058            }
11059            ExprKind::Seekdir { handle, position } => {
11060                let h = self.eval_expr(handle)?.to_string();
11061                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
11062                Ok(self.seekdir_handle(&h, pos))
11063            }
11064
11065            // File tests
11066            ExprKind::FileTest { op, expr } => {
11067                let path = self.eval_expr(expr)?.to_string();
11068                // -M, -A, -C return fractional days (float), not boolean
11069                if matches!(op, 'M' | 'A' | 'C') {
11070                    #[cfg(unix)]
11071                    {
11072                        return match crate::perl_fs::filetest_age_days(&path, *op) {
11073                            Some(days) => Ok(PerlValue::float(days)),
11074                            None => Ok(PerlValue::UNDEF),
11075                        };
11076                    }
11077                    #[cfg(not(unix))]
11078                    return Ok(PerlValue::UNDEF);
11079                }
11080                // -s returns file size (or undef on error)
11081                if *op == 's' {
11082                    return match std::fs::metadata(&path) {
11083                        Ok(m) => Ok(PerlValue::integer(m.len() as i64)),
11084                        Err(_) => Ok(PerlValue::UNDEF),
11085                    };
11086                }
11087                let result = match op {
11088                    'e' => std::path::Path::new(&path).exists(),
11089                    'f' => std::path::Path::new(&path).is_file(),
11090                    'd' => std::path::Path::new(&path).is_dir(),
11091                    'l' => std::path::Path::new(&path).is_symlink(),
11092                    #[cfg(unix)]
11093                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
11094                    #[cfg(not(unix))]
11095                    'r' => std::fs::metadata(&path).is_ok(),
11096                    #[cfg(unix)]
11097                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
11098                    #[cfg(not(unix))]
11099                    'w' => std::fs::metadata(&path).is_ok(),
11100                    #[cfg(unix)]
11101                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
11102                    #[cfg(not(unix))]
11103                    'x' => false,
11104                    #[cfg(unix)]
11105                    'o' => crate::perl_fs::filetest_owned_effective(&path),
11106                    #[cfg(not(unix))]
11107                    'o' => false,
11108                    #[cfg(unix)]
11109                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
11110                    #[cfg(not(unix))]
11111                    'R' => false,
11112                    #[cfg(unix)]
11113                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
11114                    #[cfg(not(unix))]
11115                    'W' => false,
11116                    #[cfg(unix)]
11117                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
11118                    #[cfg(not(unix))]
11119                    'X' => false,
11120                    #[cfg(unix)]
11121                    'O' => crate::perl_fs::filetest_owned_real(&path),
11122                    #[cfg(not(unix))]
11123                    'O' => false,
11124                    'z' => std::fs::metadata(&path)
11125                        .map(|m| m.len() == 0)
11126                        .unwrap_or(true),
11127                    't' => crate::perl_fs::filetest_is_tty(&path),
11128                    #[cfg(unix)]
11129                    'p' => crate::perl_fs::filetest_is_pipe(&path),
11130                    #[cfg(not(unix))]
11131                    'p' => false,
11132                    #[cfg(unix)]
11133                    'S' => crate::perl_fs::filetest_is_socket(&path),
11134                    #[cfg(not(unix))]
11135                    'S' => false,
11136                    #[cfg(unix)]
11137                    'b' => crate::perl_fs::filetest_is_block_device(&path),
11138                    #[cfg(not(unix))]
11139                    'b' => false,
11140                    #[cfg(unix)]
11141                    'c' => crate::perl_fs::filetest_is_char_device(&path),
11142                    #[cfg(not(unix))]
11143                    'c' => false,
11144                    #[cfg(unix)]
11145                    'u' => crate::perl_fs::filetest_is_setuid(&path),
11146                    #[cfg(not(unix))]
11147                    'u' => false,
11148                    #[cfg(unix)]
11149                    'g' => crate::perl_fs::filetest_is_setgid(&path),
11150                    #[cfg(not(unix))]
11151                    'g' => false,
11152                    #[cfg(unix)]
11153                    'k' => crate::perl_fs::filetest_is_sticky(&path),
11154                    #[cfg(not(unix))]
11155                    'k' => false,
11156                    'T' => crate::perl_fs::filetest_is_text(&path),
11157                    'B' => crate::perl_fs::filetest_is_binary(&path),
11158                    _ => false,
11159                };
11160                Ok(PerlValue::integer(if result { 1 } else { 0 }))
11161            }
11162
11163            // System
11164            ExprKind::System(args) => {
11165                let mut cmd_args = Vec::new();
11166                for a in args {
11167                    cmd_args.push(self.eval_expr(a)?.to_string());
11168                }
11169                if cmd_args.is_empty() {
11170                    return Ok(PerlValue::integer(-1));
11171                }
11172                let status = Command::new("sh")
11173                    .arg("-c")
11174                    .arg(cmd_args.join(" "))
11175                    .status();
11176                match status {
11177                    Ok(s) => {
11178                        self.record_child_exit_status(s);
11179                        Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
11180                    }
11181                    Err(e) => {
11182                        self.apply_io_error_to_errno(&e);
11183                        Ok(PerlValue::integer(-1))
11184                    }
11185                }
11186            }
11187            ExprKind::Exec(args) => {
11188                let mut cmd_args = Vec::new();
11189                for a in args {
11190                    cmd_args.push(self.eval_expr(a)?.to_string());
11191                }
11192                if cmd_args.is_empty() {
11193                    return Ok(PerlValue::integer(-1));
11194                }
11195                let status = Command::new("sh")
11196                    .arg("-c")
11197                    .arg(cmd_args.join(" "))
11198                    .status();
11199                match status {
11200                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
11201                    Err(e) => {
11202                        self.apply_io_error_to_errno(&e);
11203                        Ok(PerlValue::integer(-1))
11204                    }
11205                }
11206            }
11207            ExprKind::Eval(expr) => {
11208                self.eval_nesting += 1;
11209                let out = match &expr.kind {
11210                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
11211                        Ok(v) => {
11212                            self.clear_eval_error();
11213                            Ok(v)
11214                        }
11215                        Err(FlowOrError::Error(e)) => {
11216                            self.set_eval_error_from_perl_error(&e);
11217                            Ok(PerlValue::UNDEF)
11218                        }
11219                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
11220                    },
11221                    _ => {
11222                        let code = self.eval_expr(expr)?.to_string();
11223                        // Parse and execute the string as Perl code
11224                        match crate::parse_and_run_string(&code, self) {
11225                            Ok(v) => {
11226                                self.clear_eval_error();
11227                                Ok(v)
11228                            }
11229                            Err(e) => {
11230                                self.set_eval_error(e.to_string());
11231                                Ok(PerlValue::UNDEF)
11232                            }
11233                        }
11234                    }
11235                };
11236                self.eval_nesting -= 1;
11237                out
11238            }
11239            ExprKind::Do(expr) => match &expr.kind {
11240                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
11241                _ => {
11242                    let val = self.eval_expr(expr)?;
11243                    let filename = val.to_string();
11244                    match read_file_text_perl_compat(&filename) {
11245                        Ok(code) => {
11246                            let code = crate::data_section::strip_perl_end_marker(&code);
11247                            match crate::parse_and_run_string_in_file(code, self, &filename) {
11248                                Ok(v) => Ok(v),
11249                                Err(e) => {
11250                                    self.set_eval_error(e.to_string());
11251                                    Ok(PerlValue::UNDEF)
11252                                }
11253                            }
11254                        }
11255                        Err(e) => {
11256                            self.apply_io_error_to_errno(&e);
11257                            Ok(PerlValue::UNDEF)
11258                        }
11259                    }
11260                }
11261            },
11262            ExprKind::Require(expr) => {
11263                let spec = self.eval_expr(expr)?.to_string();
11264                self.require_execute(&spec, line)
11265                    .map_err(FlowOrError::Error)
11266            }
11267            ExprKind::Exit(code) => {
11268                let c = if let Some(e) = code {
11269                    self.eval_expr(e)?.to_int() as i32
11270                } else {
11271                    0
11272                };
11273                Err(PerlError::new(ErrorKind::Exit(c), "", line, &self.file).into())
11274            }
11275            ExprKind::Chdir(expr) => {
11276                let path = self.eval_expr(expr)?.to_string();
11277                match std::env::set_current_dir(&path) {
11278                    Ok(_) => Ok(PerlValue::integer(1)),
11279                    Err(e) => {
11280                        self.apply_io_error_to_errno(&e);
11281                        Ok(PerlValue::integer(0))
11282                    }
11283                }
11284            }
11285            ExprKind::Mkdir { path, mode: _ } => {
11286                let p = self.eval_expr(path)?.to_string();
11287                match std::fs::create_dir(&p) {
11288                    Ok(_) => Ok(PerlValue::integer(1)),
11289                    Err(e) => {
11290                        self.apply_io_error_to_errno(&e);
11291                        Ok(PerlValue::integer(0))
11292                    }
11293                }
11294            }
11295            ExprKind::Unlink(args) => {
11296                let mut count = 0i64;
11297                for a in args {
11298                    let path = self.eval_expr(a)?.to_string();
11299                    if std::fs::remove_file(&path).is_ok() {
11300                        count += 1;
11301                    }
11302                }
11303                Ok(PerlValue::integer(count))
11304            }
11305            ExprKind::Rename { old, new } => {
11306                let o = self.eval_expr(old)?.to_string();
11307                let n = self.eval_expr(new)?.to_string();
11308                Ok(crate::perl_fs::rename_paths(&o, &n))
11309            }
11310            ExprKind::Chmod(args) => {
11311                let mode = self.eval_expr(&args[0])?.to_int();
11312                let mut paths = Vec::new();
11313                for a in &args[1..] {
11314                    paths.push(self.eval_expr(a)?.to_string());
11315                }
11316                Ok(PerlValue::integer(crate::perl_fs::chmod_paths(
11317                    &paths, mode,
11318                )))
11319            }
11320            ExprKind::Chown(args) => {
11321                let uid = self.eval_expr(&args[0])?.to_int();
11322                let gid = self.eval_expr(&args[1])?.to_int();
11323                let mut paths = Vec::new();
11324                for a in &args[2..] {
11325                    paths.push(self.eval_expr(a)?.to_string());
11326                }
11327                Ok(PerlValue::integer(crate::perl_fs::chown_paths(
11328                    &paths, uid, gid,
11329                )))
11330            }
11331            ExprKind::Stat(e) => {
11332                let path = self.eval_expr(e)?.to_string();
11333                Ok(crate::perl_fs::stat_path(&path, false))
11334            }
11335            ExprKind::Lstat(e) => {
11336                let path = self.eval_expr(e)?.to_string();
11337                Ok(crate::perl_fs::stat_path(&path, true))
11338            }
11339            ExprKind::Link { old, new } => {
11340                let o = self.eval_expr(old)?.to_string();
11341                let n = self.eval_expr(new)?.to_string();
11342                Ok(crate::perl_fs::link_hard(&o, &n))
11343            }
11344            ExprKind::Symlink { old, new } => {
11345                let o = self.eval_expr(old)?.to_string();
11346                let n = self.eval_expr(new)?.to_string();
11347                Ok(crate::perl_fs::link_sym(&o, &n))
11348            }
11349            ExprKind::Readlink(e) => {
11350                let path = self.eval_expr(e)?.to_string();
11351                Ok(crate::perl_fs::read_link(&path))
11352            }
11353            ExprKind::Files(args) => {
11354                let dir = if args.is_empty() {
11355                    ".".to_string()
11356                } else {
11357                    self.eval_expr(&args[0])?.to_string()
11358                };
11359                Ok(crate::perl_fs::list_files(&dir))
11360            }
11361            ExprKind::Filesf(args) => {
11362                let dir = if args.is_empty() {
11363                    ".".to_string()
11364                } else {
11365                    self.eval_expr(&args[0])?.to_string()
11366                };
11367                Ok(crate::perl_fs::list_filesf(&dir))
11368            }
11369            ExprKind::FilesfRecursive(args) => {
11370                let dir = if args.is_empty() {
11371                    ".".to_string()
11372                } else {
11373                    self.eval_expr(&args[0])?.to_string()
11374                };
11375                Ok(PerlValue::iterator(Arc::new(
11376                    crate::value::FsWalkIterator::new(&dir, true),
11377                )))
11378            }
11379            ExprKind::Dirs(args) => {
11380                let dir = if args.is_empty() {
11381                    ".".to_string()
11382                } else {
11383                    self.eval_expr(&args[0])?.to_string()
11384                };
11385                Ok(crate::perl_fs::list_dirs(&dir))
11386            }
11387            ExprKind::DirsRecursive(args) => {
11388                let dir = if args.is_empty() {
11389                    ".".to_string()
11390                } else {
11391                    self.eval_expr(&args[0])?.to_string()
11392                };
11393                Ok(PerlValue::iterator(Arc::new(
11394                    crate::value::FsWalkIterator::new(&dir, false),
11395                )))
11396            }
11397            ExprKind::SymLinks(args) => {
11398                let dir = if args.is_empty() {
11399                    ".".to_string()
11400                } else {
11401                    self.eval_expr(&args[0])?.to_string()
11402                };
11403                Ok(crate::perl_fs::list_sym_links(&dir))
11404            }
11405            ExprKind::Sockets(args) => {
11406                let dir = if args.is_empty() {
11407                    ".".to_string()
11408                } else {
11409                    self.eval_expr(&args[0])?.to_string()
11410                };
11411                Ok(crate::perl_fs::list_sockets(&dir))
11412            }
11413            ExprKind::Pipes(args) => {
11414                let dir = if args.is_empty() {
11415                    ".".to_string()
11416                } else {
11417                    self.eval_expr(&args[0])?.to_string()
11418                };
11419                Ok(crate::perl_fs::list_pipes(&dir))
11420            }
11421            ExprKind::BlockDevices(args) => {
11422                let dir = if args.is_empty() {
11423                    ".".to_string()
11424                } else {
11425                    self.eval_expr(&args[0])?.to_string()
11426                };
11427                Ok(crate::perl_fs::list_block_devices(&dir))
11428            }
11429            ExprKind::CharDevices(args) => {
11430                let dir = if args.is_empty() {
11431                    ".".to_string()
11432                } else {
11433                    self.eval_expr(&args[0])?.to_string()
11434                };
11435                Ok(crate::perl_fs::list_char_devices(&dir))
11436            }
11437            ExprKind::Glob(args) => {
11438                let mut pats = Vec::new();
11439                for a in args {
11440                    pats.push(self.eval_expr(a)?.to_string());
11441                }
11442                Ok(crate::perl_fs::glob_patterns(&pats))
11443            }
11444            ExprKind::GlobPar { args, progress } => {
11445                let mut pats = Vec::new();
11446                for a in args {
11447                    pats.push(self.eval_expr(a)?.to_string());
11448                }
11449                let show_progress = progress
11450                    .as_ref()
11451                    .map(|p| self.eval_expr(p))
11452                    .transpose()?
11453                    .map(|v| v.is_true())
11454                    .unwrap_or(false);
11455                if show_progress {
11456                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
11457                } else {
11458                    Ok(crate::perl_fs::glob_par_patterns(&pats))
11459                }
11460            }
11461            ExprKind::ParSed { args, progress } => {
11462                let has_progress = progress.is_some();
11463                let mut vals: Vec<PerlValue> = Vec::new();
11464                for a in args {
11465                    vals.push(self.eval_expr(a)?);
11466                }
11467                if let Some(p) = progress {
11468                    vals.push(self.eval_expr(p.as_ref())?);
11469                }
11470                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
11471            }
11472            ExprKind::Bless { ref_expr, class } => {
11473                let val = self.eval_expr(ref_expr)?;
11474                let class_name = if let Some(c) = class {
11475                    self.eval_expr(c)?.to_string()
11476                } else {
11477                    self.scope.get_scalar("__PACKAGE__").to_string()
11478                };
11479                Ok(PerlValue::blessed(Arc::new(
11480                    crate::value::BlessedRef::new_blessed(class_name, val),
11481                )))
11482            }
11483            ExprKind::Caller(_) => {
11484                // Simplified: return package, file, line
11485                Ok(PerlValue::array(vec![
11486                    PerlValue::string("main".into()),
11487                    PerlValue::string(self.file.clone()),
11488                    PerlValue::integer(line as i64),
11489                ]))
11490            }
11491            ExprKind::Wantarray => Ok(match self.wantarray_kind {
11492                WantarrayCtx::Void => PerlValue::UNDEF,
11493                WantarrayCtx::Scalar => PerlValue::integer(0),
11494                WantarrayCtx::List => PerlValue::integer(1),
11495            }),
11496
11497            ExprKind::List(exprs) => {
11498                // In scalar context, the comma operator evaluates to the last element.
11499                if ctx == WantarrayCtx::Scalar {
11500                    if let Some(last) = exprs.last() {
11501                        // Evaluate earlier expressions for side effects
11502                        for e in &exprs[..exprs.len() - 1] {
11503                            self.eval_expr(e)?;
11504                        }
11505                        return self.eval_expr(last);
11506                    } else {
11507                        return Ok(PerlValue::UNDEF);
11508                    }
11509                }
11510                let mut vals = Vec::new();
11511                for e in exprs {
11512                    let v = self.eval_expr(e)?;
11513                    if let Some(items) = v.as_array_vec() {
11514                        vals.extend(items);
11515                    } else {
11516                        vals.push(v);
11517                    }
11518                }
11519                if vals.len() == 1 {
11520                    Ok(vals.pop().unwrap())
11521                } else {
11522                    Ok(PerlValue::array(vals))
11523                }
11524            }
11525
11526            // Postfix modifiers
11527            ExprKind::PostfixIf { expr, condition } => {
11528                if self.eval_postfix_condition(condition)? {
11529                    self.eval_expr(expr)
11530                } else {
11531                    Ok(PerlValue::UNDEF)
11532                }
11533            }
11534            ExprKind::PostfixUnless { expr, condition } => {
11535                if !self.eval_postfix_condition(condition)? {
11536                    self.eval_expr(expr)
11537                } else {
11538                    Ok(PerlValue::UNDEF)
11539                }
11540            }
11541            ExprKind::PostfixWhile { expr, condition } => {
11542                // `do { ... } while (COND)` — body runs before the first condition check.
11543                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
11544                let is_do_block = matches!(
11545                    &expr.kind,
11546                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
11547                );
11548                let mut last = PerlValue::UNDEF;
11549                if is_do_block {
11550                    loop {
11551                        last = self.eval_expr(expr)?;
11552                        if !self.eval_postfix_condition(condition)? {
11553                            break;
11554                        }
11555                    }
11556                } else {
11557                    loop {
11558                        if !self.eval_postfix_condition(condition)? {
11559                            break;
11560                        }
11561                        last = self.eval_expr(expr)?;
11562                    }
11563                }
11564                Ok(last)
11565            }
11566            ExprKind::PostfixUntil { expr, condition } => {
11567                let is_do_block = matches!(
11568                    &expr.kind,
11569                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
11570                );
11571                let mut last = PerlValue::UNDEF;
11572                if is_do_block {
11573                    loop {
11574                        last = self.eval_expr(expr)?;
11575                        if self.eval_postfix_condition(condition)? {
11576                            break;
11577                        }
11578                    }
11579                } else {
11580                    loop {
11581                        if self.eval_postfix_condition(condition)? {
11582                            break;
11583                        }
11584                        last = self.eval_expr(expr)?;
11585                    }
11586                }
11587                Ok(last)
11588            }
11589            ExprKind::PostfixForeach { expr, list } => {
11590                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
11591                let mut last = PerlValue::UNDEF;
11592                for item in items {
11593                    self.scope.set_topic(item);
11594                    last = self.eval_expr(expr)?;
11595                }
11596                Ok(last)
11597            }
11598        }
11599    }
11600
11601    // ── Helpers ──
11602
11603    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
11604        match op {
11605            BinOp::Add => Some("+"),
11606            BinOp::Sub => Some("-"),
11607            BinOp::Mul => Some("*"),
11608            BinOp::Div => Some("/"),
11609            BinOp::Mod => Some("%"),
11610            BinOp::Pow => Some("**"),
11611            BinOp::Concat => Some("."),
11612            BinOp::StrEq => Some("eq"),
11613            BinOp::NumEq => Some("=="),
11614            BinOp::StrNe => Some("ne"),
11615            BinOp::NumNe => Some("!="),
11616            BinOp::StrLt => Some("lt"),
11617            BinOp::StrGt => Some("gt"),
11618            BinOp::StrLe => Some("le"),
11619            BinOp::StrGe => Some("ge"),
11620            BinOp::NumLt => Some("<"),
11621            BinOp::NumGt => Some(">"),
11622            BinOp::NumLe => Some("<="),
11623            BinOp::NumGe => Some(">="),
11624            BinOp::Spaceship => Some("<=>"),
11625            BinOp::StrCmp => Some("cmp"),
11626            _ => None,
11627        }
11628    }
11629
11630    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
11631    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
11632        map.get("").or_else(|| map.get("\"\""))
11633    }
11634
11635    /// String context for blessed objects with `overload '""'`.
11636    pub(crate) fn stringify_value(
11637        &mut self,
11638        v: PerlValue,
11639        line: usize,
11640    ) -> Result<String, FlowOrError> {
11641        if let Some(r) = self.try_overload_stringify(&v, line) {
11642            let pv = r?;
11643            return Ok(pv.to_string());
11644        }
11645        Ok(v.to_string())
11646    }
11647
11648    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
11649    pub(crate) fn perl_sprintf_stringify(
11650        &mut self,
11651        fmt: &str,
11652        args: &[PerlValue],
11653        line: usize,
11654    ) -> Result<String, FlowOrError> {
11655        perl_sprintf_format_with(fmt, args, |v| self.stringify_value(v.clone(), line))
11656    }
11657
11658    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
11659    pub(crate) fn render_format_template(
11660        &mut self,
11661        tmpl: &crate::format::FormatTemplate,
11662        line: usize,
11663    ) -> Result<String, FlowOrError> {
11664        use crate::format::{FormatRecord, PictureSegment};
11665        let mut buf = String::new();
11666        for rec in &tmpl.records {
11667            match rec {
11668                FormatRecord::Literal(s) => {
11669                    buf.push_str(s);
11670                    buf.push('\n');
11671                }
11672                FormatRecord::Picture { segments, exprs } => {
11673                    let mut vals: Vec<String> = Vec::new();
11674                    for e in exprs {
11675                        let v = self.eval_expr(e)?;
11676                        vals.push(self.stringify_value(v, line)?);
11677                    }
11678                    let mut vi = 0usize;
11679                    let mut line_out = String::new();
11680                    for seg in segments {
11681                        match seg {
11682                            PictureSegment::Literal(t) => line_out.push_str(t),
11683                            PictureSegment::Field {
11684                                width,
11685                                align,
11686                                kind: _,
11687                            } => {
11688                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
11689                                vi += 1;
11690                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
11691                            }
11692                        }
11693                    }
11694                    buf.push_str(line_out.trim_end());
11695                    buf.push('\n');
11696                }
11697            }
11698        }
11699        Ok(buf)
11700    }
11701
11702    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
11703    pub(crate) fn resolve_write_output_handle(
11704        &self,
11705        v: &PerlValue,
11706        line: usize,
11707    ) -> PerlResult<String> {
11708        if let Some(n) = v.as_io_handle_name() {
11709            let n = self.resolve_io_handle_name(&n);
11710            if self.is_bound_handle(&n) {
11711                return Ok(n);
11712            }
11713        }
11714        if let Some(s) = v.as_str() {
11715            if self.is_bound_handle(&s) {
11716                return Ok(self.resolve_io_handle_name(&s));
11717            }
11718        }
11719        let s = v.to_string();
11720        if self.is_bound_handle(&s) {
11721            return Ok(self.resolve_io_handle_name(&s));
11722        }
11723        Err(PerlError::runtime(
11724            format!("write: invalid or unopened filehandle {}", s),
11725            line,
11726        ))
11727    }
11728
11729    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
11730    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
11731    /// that handle like `write FH`.
11732    pub(crate) fn write_format_execute(
11733        &mut self,
11734        args: &[PerlValue],
11735        line: usize,
11736    ) -> PerlResult<PerlValue> {
11737        let handle_name = match args.len() {
11738            0 => self.default_print_handle.clone(),
11739            1 => self.resolve_write_output_handle(&args[0], line)?,
11740            _ => {
11741                return Err(PerlError::runtime("write: too many arguments", line));
11742            }
11743        };
11744        let pkg = self.current_package();
11745        let mut fmt_name = self.scope.get_scalar("~").to_string();
11746        if fmt_name.is_empty() {
11747            fmt_name = "STDOUT".to_string();
11748        }
11749        let key = format!("{}::{}", pkg, fmt_name);
11750        let tmpl = self
11751            .format_templates
11752            .get(&key)
11753            .map(Arc::clone)
11754            .ok_or_else(|| {
11755                PerlError::runtime(
11756                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
11757                    line,
11758                )
11759            })?;
11760        let out = self
11761            .render_format_template(&tmpl, line)
11762            .map_err(|e| match e {
11763                FlowOrError::Error(e) => e,
11764                FlowOrError::Flow(_) => PerlError::runtime("write: unexpected control flow", line),
11765            })?;
11766        self.write_formatted_print(handle_name.as_str(), &out, line)?;
11767        Ok(PerlValue::integer(1))
11768    }
11769
11770    pub(crate) fn try_overload_stringify(
11771        &mut self,
11772        v: &PerlValue,
11773        line: usize,
11774    ) -> Option<ExecResult> {
11775        // Native class instance: look for method named '""' or 'stringify'
11776        if let Some(c) = v.as_class_inst() {
11777            let method_name = c
11778                .def
11779                .method("stringify")
11780                .or_else(|| c.def.method("\"\""))
11781                .filter(|m| m.body.is_some())?;
11782            let body = method_name.body.clone().unwrap();
11783            let params = method_name.params.clone();
11784            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
11785        }
11786        let br = v.as_blessed_ref()?;
11787        let class = br.class.clone();
11788        let map = self.overload_table.get(&class)?;
11789        let sub_short = Self::overload_stringify_method(map)?;
11790        let fq = format!("{}::{}", class, sub_short);
11791        let sub = self.subs.get(&fq)?.clone();
11792        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
11793    }
11794
11795    /// Map overload operator key to native class method name.
11796    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
11797        match key {
11798            "+" => Some("op_add"),
11799            "-" => Some("op_sub"),
11800            "*" => Some("op_mul"),
11801            "/" => Some("op_div"),
11802            "%" => Some("op_mod"),
11803            "**" => Some("op_pow"),
11804            "." => Some("op_concat"),
11805            "==" => Some("op_eq"),
11806            "!=" => Some("op_ne"),
11807            "<" => Some("op_lt"),
11808            ">" => Some("op_gt"),
11809            "<=" => Some("op_le"),
11810            ">=" => Some("op_ge"),
11811            "<=>" => Some("op_spaceship"),
11812            "eq" => Some("op_str_eq"),
11813            "ne" => Some("op_str_ne"),
11814            "lt" => Some("op_str_lt"),
11815            "gt" => Some("op_str_gt"),
11816            "le" => Some("op_str_le"),
11817            "ge" => Some("op_str_ge"),
11818            "cmp" => Some("op_cmp"),
11819            _ => None,
11820        }
11821    }
11822
11823    pub(crate) fn try_overload_binop(
11824        &mut self,
11825        op: BinOp,
11826        lv: &PerlValue,
11827        rv: &PerlValue,
11828        line: usize,
11829    ) -> Option<ExecResult> {
11830        let key = Self::overload_key_for_binop(op)?;
11831        // Native class instance overloading
11832        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
11833            (Some(c.def.clone()), lv.clone(), rv.clone())
11834        } else if let Some(c) = rv.as_class_inst() {
11835            (Some(c.def.clone()), rv.clone(), lv.clone())
11836        } else {
11837            (None, lv.clone(), rv.clone())
11838        };
11839        if let Some(ref def) = ci_def {
11840            if let Some(method_name) = Self::overload_method_name_for_key(key) {
11841                if let Some((m, _)) = self.find_class_method(def, method_name) {
11842                    if let Some(ref body) = m.body {
11843                        let params = m.params.clone();
11844                        return Some(self.call_class_method(
11845                            body,
11846                            &params,
11847                            vec![invocant, other],
11848                            line,
11849                        ));
11850                    }
11851                }
11852            }
11853        }
11854        // Blessed ref overloading (existing path)
11855        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
11856            (br.class.clone(), lv.clone(), rv.clone())
11857        } else if let Some(br) = rv.as_blessed_ref() {
11858            (br.class.clone(), rv.clone(), lv.clone())
11859        } else {
11860            return None;
11861        };
11862        let map = self.overload_table.get(&class)?;
11863        let sub_short = if let Some(s) = map.get(key) {
11864            s.clone()
11865        } else if let Some(nm) = map.get("nomethod") {
11866            let fq = format!("{}::{}", class, nm);
11867            let sub = self.subs.get(&fq)?.clone();
11868            return Some(self.call_sub(
11869                &sub,
11870                vec![invocant, other, PerlValue::string(key.to_string())],
11871                WantarrayCtx::Scalar,
11872                line,
11873            ));
11874        } else {
11875            return None;
11876        };
11877        let fq = format!("{}::{}", class, sub_short);
11878        let sub = self.subs.get(&fq)?.clone();
11879        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
11880    }
11881
11882    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
11883    pub(crate) fn try_overload_unary_dispatch(
11884        &mut self,
11885        op_key: &str,
11886        val: &PerlValue,
11887        line: usize,
11888    ) -> Option<ExecResult> {
11889        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
11890        if let Some(c) = val.as_class_inst() {
11891            let method_name = match op_key {
11892                "neg" => "op_neg",
11893                "bool" => "op_bool",
11894                "abs" => "op_abs",
11895                "0+" => "op_numify",
11896                _ => return None,
11897            };
11898            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
11899                if let Some(ref body) = m.body {
11900                    let params = m.params.clone();
11901                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
11902                }
11903            }
11904            return None;
11905        }
11906        // Blessed ref path
11907        let br = val.as_blessed_ref()?;
11908        let class = br.class.clone();
11909        let map = self.overload_table.get(&class)?;
11910        if let Some(s) = map.get(op_key) {
11911            let fq = format!("{}::{}", class, s);
11912            let sub = self.subs.get(&fq)?.clone();
11913            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
11914        }
11915        if let Some(nm) = map.get("nomethod") {
11916            let fq = format!("{}::{}", class, nm);
11917            let sub = self.subs.get(&fq)?.clone();
11918            return Some(self.call_sub(
11919                &sub,
11920                vec![val.clone(), PerlValue::string(op_key.to_string())],
11921                WantarrayCtx::Scalar,
11922                line,
11923            ));
11924        }
11925        None
11926    }
11927
11928    #[inline]
11929    fn eval_binop(
11930        &mut self,
11931        op: BinOp,
11932        lv: &PerlValue,
11933        rv: &PerlValue,
11934        _line: usize,
11935    ) -> ExecResult {
11936        Ok(match op {
11937            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
11938            // Perl `+` is numeric addition only; string concatenation is `.`.
11939            BinOp::Add => {
11940                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
11941                    PerlValue::integer(a.wrapping_add(b))
11942                } else {
11943                    PerlValue::float(lv.to_number() + rv.to_number())
11944                }
11945            }
11946            BinOp::Sub => {
11947                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
11948                    PerlValue::integer(a.wrapping_sub(b))
11949                } else {
11950                    PerlValue::float(lv.to_number() - rv.to_number())
11951                }
11952            }
11953            BinOp::Mul => {
11954                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
11955                    PerlValue::integer(a.wrapping_mul(b))
11956                } else {
11957                    PerlValue::float(lv.to_number() * rv.to_number())
11958                }
11959            }
11960            BinOp::Div => {
11961                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
11962                    if b == 0 {
11963                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
11964                    }
11965                    if a % b == 0 {
11966                        PerlValue::integer(a / b)
11967                    } else {
11968                        PerlValue::float(a as f64 / b as f64)
11969                    }
11970                } else {
11971                    let d = rv.to_number();
11972                    if d == 0.0 {
11973                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
11974                    }
11975                    PerlValue::float(lv.to_number() / d)
11976                }
11977            }
11978            BinOp::Mod => {
11979                let d = rv.to_int();
11980                if d == 0 {
11981                    return Err(PerlError::runtime("Illegal modulus zero", _line).into());
11982                }
11983                PerlValue::integer(lv.to_int() % d)
11984            }
11985            BinOp::Pow => {
11986                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
11987                    let int_pow = (b >= 0)
11988                        .then(|| u32::try_from(b).ok())
11989                        .flatten()
11990                        .and_then(|bu| a.checked_pow(bu))
11991                        .map(PerlValue::integer);
11992                    int_pow.unwrap_or_else(|| PerlValue::float(lv.to_number().powf(rv.to_number())))
11993                } else {
11994                    PerlValue::float(lv.to_number().powf(rv.to_number()))
11995                }
11996            }
11997            BinOp::Concat => {
11998                let mut s = String::new();
11999                lv.append_to(&mut s);
12000                rv.append_to(&mut s);
12001                PerlValue::string(s)
12002            }
12003            BinOp::NumEq => {
12004                // Struct equality: compare all fields
12005                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
12006                    if a.def.name != b.def.name {
12007                        PerlValue::integer(0)
12008                    } else {
12009                        let av = a.get_values();
12010                        let bv = b.get_values();
12011                        let eq = av.len() == bv.len()
12012                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
12013                        PerlValue::integer(if eq { 1 } else { 0 })
12014                    }
12015                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12016                    PerlValue::integer(if a == b { 1 } else { 0 })
12017                } else {
12018                    PerlValue::integer(if lv.to_number() == rv.to_number() {
12019                        1
12020                    } else {
12021                        0
12022                    })
12023                }
12024            }
12025            BinOp::NumNe => {
12026                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12027                    PerlValue::integer(if a != b { 1 } else { 0 })
12028                } else {
12029                    PerlValue::integer(if lv.to_number() != rv.to_number() {
12030                        1
12031                    } else {
12032                        0
12033                    })
12034                }
12035            }
12036            BinOp::NumLt => {
12037                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12038                    PerlValue::integer(if a < b { 1 } else { 0 })
12039                } else {
12040                    PerlValue::integer(if lv.to_number() < rv.to_number() {
12041                        1
12042                    } else {
12043                        0
12044                    })
12045                }
12046            }
12047            BinOp::NumGt => {
12048                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12049                    PerlValue::integer(if a > b { 1 } else { 0 })
12050                } else {
12051                    PerlValue::integer(if lv.to_number() > rv.to_number() {
12052                        1
12053                    } else {
12054                        0
12055                    })
12056                }
12057            }
12058            BinOp::NumLe => {
12059                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12060                    PerlValue::integer(if a <= b { 1 } else { 0 })
12061                } else {
12062                    PerlValue::integer(if lv.to_number() <= rv.to_number() {
12063                        1
12064                    } else {
12065                        0
12066                    })
12067                }
12068            }
12069            BinOp::NumGe => {
12070                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12071                    PerlValue::integer(if a >= b { 1 } else { 0 })
12072                } else {
12073                    PerlValue::integer(if lv.to_number() >= rv.to_number() {
12074                        1
12075                    } else {
12076                        0
12077                    })
12078                }
12079            }
12080            BinOp::Spaceship => {
12081                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12082                    PerlValue::integer(if a < b {
12083                        -1
12084                    } else if a > b {
12085                        1
12086                    } else {
12087                        0
12088                    })
12089                } else {
12090                    let a = lv.to_number();
12091                    let b = rv.to_number();
12092                    PerlValue::integer(if a < b {
12093                        -1
12094                    } else if a > b {
12095                        1
12096                    } else {
12097                        0
12098                    })
12099                }
12100            }
12101            BinOp::StrEq => PerlValue::integer(if lv.to_string() == rv.to_string() {
12102                1
12103            } else {
12104                0
12105            }),
12106            BinOp::StrNe => PerlValue::integer(if lv.to_string() != rv.to_string() {
12107                1
12108            } else {
12109                0
12110            }),
12111            BinOp::StrLt => PerlValue::integer(if lv.to_string() < rv.to_string() {
12112                1
12113            } else {
12114                0
12115            }),
12116            BinOp::StrGt => PerlValue::integer(if lv.to_string() > rv.to_string() {
12117                1
12118            } else {
12119                0
12120            }),
12121            BinOp::StrLe => PerlValue::integer(if lv.to_string() <= rv.to_string() {
12122                1
12123            } else {
12124                0
12125            }),
12126            BinOp::StrGe => PerlValue::integer(if lv.to_string() >= rv.to_string() {
12127                1
12128            } else {
12129                0
12130            }),
12131            BinOp::StrCmp => {
12132                let cmp = lv.to_string().cmp(&rv.to_string());
12133                PerlValue::integer(match cmp {
12134                    std::cmp::Ordering::Less => -1,
12135                    std::cmp::Ordering::Greater => 1,
12136                    std::cmp::Ordering::Equal => 0,
12137                })
12138            }
12139            BinOp::BitAnd => {
12140                if let Some(s) = crate::value::set_intersection(lv, rv) {
12141                    s
12142                } else {
12143                    PerlValue::integer(lv.to_int() & rv.to_int())
12144                }
12145            }
12146            BinOp::BitOr => {
12147                if let Some(s) = crate::value::set_union(lv, rv) {
12148                    s
12149                } else {
12150                    PerlValue::integer(lv.to_int() | rv.to_int())
12151                }
12152            }
12153            BinOp::BitXor => PerlValue::integer(lv.to_int() ^ rv.to_int()),
12154            BinOp::ShiftLeft => PerlValue::integer(lv.to_int() << rv.to_int()),
12155            BinOp::ShiftRight => PerlValue::integer(lv.to_int() >> rv.to_int()),
12156            // These should have been handled by short-circuit above
12157            BinOp::LogAnd
12158            | BinOp::LogOr
12159            | BinOp::DefinedOr
12160            | BinOp::LogAndWord
12161            | BinOp::LogOrWord => unreachable!(),
12162            BinOp::BindMatch | BinOp::BindNotMatch => {
12163                unreachable!("regex bind handled in eval_expr BinOp arm")
12164            }
12165        })
12166    }
12167
12168    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
12169    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
12170    /// length — that was silently wrong vs `perl`.
12171    fn err_modify_symbolic_aggregate_deref_inc_dec(
12172        kind: Sigil,
12173        is_pre: bool,
12174        is_inc: bool,
12175        line: usize,
12176    ) -> FlowOrError {
12177        let agg = match kind {
12178            Sigil::Array => "array",
12179            Sigil::Hash => "hash",
12180            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
12181        };
12182        let op = match (is_pre, is_inc) {
12183            (true, true) => "preincrement (++)",
12184            (true, false) => "predecrement (--)",
12185            (false, true) => "postincrement (++)",
12186            (false, false) => "postdecrement (--)",
12187        };
12188        FlowOrError::Error(PerlError::runtime(
12189            format!("Can't modify {agg} dereference in {op}"),
12190            line,
12191        ))
12192    }
12193
12194    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
12195    pub(crate) fn symbolic_scalar_ref_postfix(
12196        &mut self,
12197        ref_val: PerlValue,
12198        decrement: bool,
12199        line: usize,
12200    ) -> Result<PerlValue, FlowOrError> {
12201        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
12202        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12203        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
12204        Ok(old)
12205    }
12206
12207    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
12208    /// [`Self::assign_value`] and the VM.
12209    pub(crate) fn assign_scalar_ref_deref(
12210        &mut self,
12211        ref_val: PerlValue,
12212        val: PerlValue,
12213        line: usize,
12214    ) -> ExecResult {
12215        if let Some(name) = ref_val.as_scalar_binding_name() {
12216            self.set_special_var(&name, &val)
12217                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12218            return Ok(PerlValue::UNDEF);
12219        }
12220        if let Some(r) = ref_val.as_scalar_ref() {
12221            *r.write() = val;
12222            return Ok(PerlValue::UNDEF);
12223        }
12224        Err(PerlError::runtime("Can't assign to non-scalar reference", line).into())
12225    }
12226
12227    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
12228    pub(crate) fn assign_symbolic_array_ref_deref(
12229        &mut self,
12230        ref_val: PerlValue,
12231        val: PerlValue,
12232        line: usize,
12233    ) -> ExecResult {
12234        if let Some(a) = ref_val.as_array_ref() {
12235            *a.write() = val.to_list();
12236            return Ok(PerlValue::UNDEF);
12237        }
12238        if let Some(name) = ref_val.as_array_binding_name() {
12239            self.scope
12240                .set_array(&name, val.to_list())
12241                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12242            return Ok(PerlValue::UNDEF);
12243        }
12244        if let Some(s) = ref_val.as_str() {
12245            if self.strict_refs {
12246                return Err(PerlError::runtime(
12247                    format!(
12248                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
12249                        s
12250                    ),
12251                    line,
12252                )
12253                .into());
12254            }
12255            self.scope
12256                .set_array(&s, val.to_list())
12257                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12258            return Ok(PerlValue::UNDEF);
12259        }
12260        Err(PerlError::runtime("Can't assign to non-array reference", line).into())
12261    }
12262
12263    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
12264    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
12265    pub(crate) fn assign_symbolic_typeglob_ref_deref(
12266        &mut self,
12267        ref_val: PerlValue,
12268        val: PerlValue,
12269        line: usize,
12270    ) -> ExecResult {
12271        let lhs_name = if let Some(s) = ref_val.as_str() {
12272            if self.strict_refs {
12273                return Err(PerlError::runtime(
12274                    format!(
12275                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
12276                        s
12277                    ),
12278                    line,
12279                )
12280                .into());
12281            }
12282            s.to_string()
12283        } else {
12284            return Err(
12285                PerlError::runtime("Can't assign to non-glob symbolic reference", line).into(),
12286            );
12287        };
12288        let is_coderef = val.as_code_ref().is_some()
12289            || val
12290                .as_scalar_ref()
12291                .map(|r| r.read().as_code_ref().is_some())
12292                .unwrap_or(false);
12293        if is_coderef {
12294            return self.assign_typeglob_value(&lhs_name, val, line);
12295        }
12296        let rhs_key = val.to_string();
12297        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
12298            .map_err(FlowOrError::Error)?;
12299        Ok(PerlValue::UNDEF)
12300    }
12301
12302    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
12303    pub(crate) fn assign_symbolic_hash_ref_deref(
12304        &mut self,
12305        ref_val: PerlValue,
12306        val: PerlValue,
12307        line: usize,
12308    ) -> ExecResult {
12309        let items = val.to_list();
12310        let mut map = IndexMap::new();
12311        let mut i = 0;
12312        while i + 1 < items.len() {
12313            map.insert(items[i].to_string(), items[i + 1].clone());
12314            i += 2;
12315        }
12316        if let Some(h) = ref_val.as_hash_ref() {
12317            *h.write() = map;
12318            return Ok(PerlValue::UNDEF);
12319        }
12320        if let Some(name) = ref_val.as_hash_binding_name() {
12321            self.touch_env_hash(&name);
12322            self.scope
12323                .set_hash(&name, map)
12324                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12325            return Ok(PerlValue::UNDEF);
12326        }
12327        if let Some(s) = ref_val.as_str() {
12328            if self.strict_refs {
12329                return Err(PerlError::runtime(
12330                    format!(
12331                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
12332                        s
12333                    ),
12334                    line,
12335                )
12336                .into());
12337            }
12338            self.touch_env_hash(&s);
12339            self.scope
12340                .set_hash(&s, map)
12341                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12342            return Ok(PerlValue::UNDEF);
12343        }
12344        Err(PerlError::runtime("Can't assign to non-hash reference", line).into())
12345    }
12346
12347    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
12348    pub(crate) fn assign_arrow_hash_deref(
12349        &mut self,
12350        container: PerlValue,
12351        key: String,
12352        val: PerlValue,
12353        line: usize,
12354    ) -> ExecResult {
12355        if let Some(b) = container.as_blessed_ref() {
12356            let mut data = b.data.write();
12357            if let Some(r) = data.as_hash_ref() {
12358                r.write().insert(key, val);
12359                return Ok(PerlValue::UNDEF);
12360            }
12361            if let Some(mut map) = data.as_hash_map() {
12362                map.insert(key, val);
12363                *data = PerlValue::hash(map);
12364                return Ok(PerlValue::UNDEF);
12365            }
12366            return Err(PerlError::runtime("Can't assign into non-hash blessed ref", line).into());
12367        }
12368        if let Some(r) = container.as_hash_ref() {
12369            r.write().insert(key, val);
12370            return Ok(PerlValue::UNDEF);
12371        }
12372        if let Some(name) = container.as_hash_binding_name() {
12373            self.touch_env_hash(&name);
12374            self.scope
12375                .set_hash_element(&name, &key, val)
12376                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12377            return Ok(PerlValue::UNDEF);
12378        }
12379        Err(PerlError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
12380    }
12381
12382    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
12383    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
12384    pub(crate) fn eval_arrow_array_base(
12385        &mut self,
12386        expr: &Expr,
12387        _line: usize,
12388    ) -> Result<PerlValue, FlowOrError> {
12389        match &expr.kind {
12390            ExprKind::Deref {
12391                expr: inner,
12392                kind: Sigil::Array | Sigil::Scalar,
12393            } => self.eval_expr(inner),
12394            _ => self.eval_expr(expr),
12395        }
12396    }
12397
12398    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
12399    pub(crate) fn eval_arrow_hash_base(
12400        &mut self,
12401        expr: &Expr,
12402        _line: usize,
12403    ) -> Result<PerlValue, FlowOrError> {
12404        match &expr.kind {
12405            ExprKind::Deref {
12406                expr: inner,
12407                kind: Sigil::Scalar,
12408            } => self.eval_expr(inner),
12409            _ => self.eval_expr(expr),
12410        }
12411    }
12412
12413    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
12414    pub(crate) fn read_arrow_array_element(
12415        &self,
12416        container: PerlValue,
12417        idx: i64,
12418        line: usize,
12419    ) -> Result<PerlValue, FlowOrError> {
12420        if let Some(a) = container.as_array_ref() {
12421            let arr = a.read();
12422            let i = if idx < 0 {
12423                (arr.len() as i64 + idx) as usize
12424            } else {
12425                idx as usize
12426            };
12427            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12428        }
12429        if let Some(name) = container.as_array_binding_name() {
12430            return Ok(self.scope.get_array_element(&name, idx));
12431        }
12432        if let Some(arr) = container.as_array_vec() {
12433            let i = if idx < 0 {
12434                (arr.len() as i64 + idx) as usize
12435            } else {
12436                idx as usize
12437            };
12438            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12439        }
12440        // Blessed arrayref (e.g. `List::Util::_Pair`) — Perl allows `->[N]` on
12441        // blessed arrayrefs; `pairs` returns blessed `_Pair` objects that the
12442        // doc shows being indexed via `$_->[0]` / `$_->[1]`.
12443        if let Some(b) = container.as_blessed_ref() {
12444            let inner = b.data.read().clone();
12445            if let Some(a) = inner.as_array_ref() {
12446                let arr = a.read();
12447                let i = if idx < 0 {
12448                    (arr.len() as i64 + idx) as usize
12449                } else {
12450                    idx as usize
12451                };
12452                return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12453            }
12454        }
12455        Err(PerlError::runtime("Can't use arrow deref on non-array-ref", line).into())
12456    }
12457
12458    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
12459    pub(crate) fn read_arrow_hash_element(
12460        &mut self,
12461        container: PerlValue,
12462        key: &str,
12463        line: usize,
12464    ) -> Result<PerlValue, FlowOrError> {
12465        if let Some(r) = container.as_hash_ref() {
12466            let h = r.read();
12467            return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12468        }
12469        if let Some(name) = container.as_hash_binding_name() {
12470            self.touch_env_hash(&name);
12471            return Ok(self.scope.get_hash_element(&name, key));
12472        }
12473        if let Some(b) = container.as_blessed_ref() {
12474            let data = b.data.read();
12475            if let Some(v) = data.hash_get(key) {
12476                return Ok(v);
12477            }
12478            if let Some(r) = data.as_hash_ref() {
12479                let h = r.read();
12480                return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12481            }
12482            return Err(PerlError::runtime(
12483                "Can't access hash field on non-hash blessed ref",
12484                line,
12485            )
12486            .into());
12487        }
12488        Err(PerlError::runtime("Can't use arrow deref on non-hash-ref", line).into())
12489    }
12490
12491    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
12492    pub(crate) fn arrow_array_postfix(
12493        &mut self,
12494        container: PerlValue,
12495        idx: i64,
12496        decrement: bool,
12497        line: usize,
12498    ) -> Result<PerlValue, FlowOrError> {
12499        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
12500        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12501        self.assign_arrow_array_deref(container, idx, new_val, line)?;
12502        Ok(old)
12503    }
12504
12505    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
12506    pub(crate) fn arrow_hash_postfix(
12507        &mut self,
12508        container: PerlValue,
12509        key: String,
12510        decrement: bool,
12511        line: usize,
12512    ) -> Result<PerlValue, FlowOrError> {
12513        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
12514        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12515        self.assign_arrow_hash_deref(container, key, new_val, line)?;
12516        Ok(old)
12517    }
12518
12519    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` in the tree walker. If a nullary
12520    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
12521    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
12522    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
12523    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
12524    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
12525    /// subs" in use`).
12526    pub(crate) fn resolve_bareword_rvalue(
12527        &mut self,
12528        name: &str,
12529        want: WantarrayCtx,
12530        line: usize,
12531    ) -> Result<PerlValue, FlowOrError> {
12532        if name == "__PACKAGE__" {
12533            return Ok(PerlValue::string(self.current_package()));
12534        }
12535        if let Some(sub) = self.resolve_sub_by_name(name) {
12536            return self.call_sub(&sub, vec![], want, line);
12537        }
12538        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
12539        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
12540            return r.map_err(Into::into);
12541        }
12542        Ok(PerlValue::string(name.to_string()))
12543    }
12544
12545    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
12546    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
12547    /// compound / inc-dec / assign helpers below.
12548    pub(crate) fn arrow_array_slice_values(
12549        &mut self,
12550        container: PerlValue,
12551        indices: &[i64],
12552        line: usize,
12553    ) -> Result<PerlValue, FlowOrError> {
12554        let mut out = Vec::with_capacity(indices.len());
12555        for &idx in indices {
12556            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
12557            out.push(v);
12558        }
12559        Ok(PerlValue::array(out))
12560    }
12561
12562    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment matching the tree-walker
12563    /// `assign_value` path for multi-index `ArrowDeref { Array, List }`. Shared by the VM
12564    /// [`crate::bytecode::Op::SetArrowArraySlice`].
12565    pub(crate) fn assign_arrow_array_slice(
12566        &mut self,
12567        container: PerlValue,
12568        indices: Vec<i64>,
12569        val: PerlValue,
12570        line: usize,
12571    ) -> Result<PerlValue, FlowOrError> {
12572        if indices.is_empty() {
12573            return Err(PerlError::runtime("assign to empty array slice", line).into());
12574        }
12575        let vals = val.to_list();
12576        for (i, idx) in indices.iter().enumerate() {
12577            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
12578            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
12579        }
12580        Ok(PerlValue::UNDEF)
12581    }
12582
12583    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
12584    pub(crate) fn flatten_array_slice_index_specs(
12585        &mut self,
12586        indices: &[Expr],
12587    ) -> Result<Vec<i64>, FlowOrError> {
12588        let mut out = Vec::new();
12589        for idx_expr in indices {
12590            let v = if matches!(idx_expr.kind, ExprKind::Range { .. }) {
12591                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
12592            } else {
12593                self.eval_expr(idx_expr)?
12594            };
12595            if let Some(list) = v.as_array_vec() {
12596                for idx in list {
12597                    out.push(idx.to_int());
12598                }
12599            } else {
12600                out.push(v.to_int());
12601            }
12602        }
12603        Ok(out)
12604    }
12605
12606    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
12607    pub(crate) fn assign_named_array_slice(
12608        &mut self,
12609        stash_array_name: &str,
12610        indices: Vec<i64>,
12611        val: PerlValue,
12612        line: usize,
12613    ) -> Result<PerlValue, FlowOrError> {
12614        if indices.is_empty() {
12615            return Err(PerlError::runtime("assign to empty array slice", line).into());
12616        }
12617        let vals = val.to_list();
12618        for (i, idx) in indices.iter().enumerate() {
12619            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
12620            self.scope
12621                .set_array_element(stash_array_name, *idx, v)
12622                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12623        }
12624        Ok(PerlValue::UNDEF)
12625    }
12626
12627    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
12628    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
12629    pub(crate) fn compound_assign_arrow_array_slice(
12630        &mut self,
12631        container: PerlValue,
12632        indices: Vec<i64>,
12633        op: BinOp,
12634        rhs: PerlValue,
12635        line: usize,
12636    ) -> Result<PerlValue, FlowOrError> {
12637        if indices.is_empty() {
12638            return Err(PerlError::runtime("assign to empty array slice", line).into());
12639        }
12640        let last_idx = *indices.last().expect("non-empty indices");
12641        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
12642        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
12643        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
12644        Ok(new_val)
12645    }
12646
12647    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
12648    /// pre forms return the new value, post forms return the old **last** element.
12649    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
12650    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
12651    pub(crate) fn arrow_array_slice_inc_dec(
12652        &mut self,
12653        container: PerlValue,
12654        indices: Vec<i64>,
12655        kind: u8,
12656        line: usize,
12657    ) -> Result<PerlValue, FlowOrError> {
12658        if indices.is_empty() {
12659            return Err(
12660                PerlError::runtime("array slice increment needs at least one index", line).into(),
12661            );
12662        }
12663        let last_idx = *indices.last().expect("non-empty indices");
12664        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
12665        let new_val = if kind & 1 == 0 {
12666            PerlValue::integer(last_old.to_int() + 1)
12667        } else {
12668            PerlValue::integer(last_old.to_int() - 1)
12669        };
12670        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
12671        Ok(if kind < 2 { new_val } else { last_old })
12672    }
12673
12674    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
12675    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
12676    pub(crate) fn named_array_slice_inc_dec(
12677        &mut self,
12678        stash_array_name: &str,
12679        indices: Vec<i64>,
12680        kind: u8,
12681        line: usize,
12682    ) -> Result<PerlValue, FlowOrError> {
12683        let last_idx = *indices.last().ok_or_else(|| {
12684            PerlError::runtime("array slice increment needs at least one index", line)
12685        })?;
12686        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
12687        let new_val = if kind & 1 == 0 {
12688            PerlValue::integer(last_old.to_int() + 1)
12689        } else {
12690            PerlValue::integer(last_old.to_int() - 1)
12691        };
12692        self.scope
12693            .set_array_element(stash_array_name, last_idx, new_val.clone())
12694            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12695        Ok(if kind < 2 { new_val } else { last_old })
12696    }
12697
12698    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
12699    pub(crate) fn compound_assign_named_array_slice(
12700        &mut self,
12701        stash_array_name: &str,
12702        indices: Vec<i64>,
12703        op: BinOp,
12704        rhs: PerlValue,
12705        line: usize,
12706    ) -> Result<PerlValue, FlowOrError> {
12707        if indices.is_empty() {
12708            return Err(PerlError::runtime("assign to empty array slice", line).into());
12709        }
12710        let last_idx = *indices.last().expect("non-empty indices");
12711        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
12712        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
12713        self.scope
12714            .set_array_element(stash_array_name, last_idx, new_val.clone())
12715            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12716        Ok(new_val)
12717    }
12718
12719    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
12720    pub(crate) fn assign_arrow_array_deref(
12721        &mut self,
12722        container: PerlValue,
12723        idx: i64,
12724        val: PerlValue,
12725        line: usize,
12726    ) -> ExecResult {
12727        if let Some(a) = container.as_array_ref() {
12728            let mut arr = a.write();
12729            let i = if idx < 0 {
12730                (arr.len() as i64 + idx) as usize
12731            } else {
12732                idx as usize
12733            };
12734            if i >= arr.len() {
12735                arr.resize(i + 1, PerlValue::UNDEF);
12736            }
12737            arr[i] = val;
12738            return Ok(PerlValue::UNDEF);
12739        }
12740        if let Some(name) = container.as_array_binding_name() {
12741            self.scope
12742                .set_array_element(&name, idx, val)
12743                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12744            return Ok(PerlValue::UNDEF);
12745        }
12746        Err(PerlError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
12747    }
12748
12749    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
12750    pub(crate) fn assign_typeglob_value(
12751        &mut self,
12752        name: &str,
12753        val: PerlValue,
12754        line: usize,
12755    ) -> ExecResult {
12756        let sub = if let Some(c) = val.as_code_ref() {
12757            Some(c)
12758        } else if let Some(r) = val.as_scalar_ref() {
12759            r.read().as_code_ref().map(|c| Arc::clone(&c))
12760        } else {
12761            None
12762        };
12763        if let Some(sub) = sub {
12764            let lhs_sub = self.qualify_typeglob_sub_key(name);
12765            self.subs.insert(lhs_sub, sub);
12766            return Ok(PerlValue::UNDEF);
12767        }
12768        Err(PerlError::runtime(
12769            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
12770            line,
12771        )
12772        .into())
12773    }
12774
12775    fn assign_value(&mut self, target: &Expr, val: PerlValue) -> ExecResult {
12776        match &target.kind {
12777            ExprKind::ScalarVar(name) => {
12778                let stor = self.tree_scalar_storage_name(name);
12779                if self.scope.is_scalar_frozen(&stor) {
12780                    return Err(FlowOrError::Error(PerlError::runtime(
12781                        format!("Modification of a frozen value: ${}", name),
12782                        target.line,
12783                    )));
12784                }
12785                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
12786                    let class = obj
12787                        .as_blessed_ref()
12788                        .map(|b| b.class.clone())
12789                        .unwrap_or_default();
12790                    let full = format!("{}::STORE", class);
12791                    if let Some(sub) = self.subs.get(&full).cloned() {
12792                        let arg_vals = vec![obj, val];
12793                        return match self.call_sub(
12794                            &sub,
12795                            arg_vals,
12796                            WantarrayCtx::Scalar,
12797                            target.line,
12798                        ) {
12799                            Ok(_) => Ok(PerlValue::UNDEF),
12800                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
12801                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
12802                        };
12803                    }
12804                }
12805                self.set_special_var(&stor, &val)
12806                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
12807                Ok(PerlValue::UNDEF)
12808            }
12809            ExprKind::ArrayVar(name) => {
12810                if self.scope.is_array_frozen(name) {
12811                    return Err(PerlError::runtime(
12812                        format!("Modification of a frozen value: @{}", name),
12813                        target.line,
12814                    )
12815                    .into());
12816                }
12817                if self.strict_vars
12818                    && !name.contains("::")
12819                    && !self.scope.array_binding_exists(name)
12820                {
12821                    return Err(PerlError::runtime(
12822                        format!(
12823                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
12824                            name, name
12825                        ),
12826                        target.line,
12827                    )
12828                    .into());
12829                }
12830                self.scope.set_array(name, val.to_list())?;
12831                Ok(PerlValue::UNDEF)
12832            }
12833            ExprKind::HashVar(name) => {
12834                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
12835                {
12836                    return Err(PerlError::runtime(
12837                        format!(
12838                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
12839                            name, name
12840                        ),
12841                        target.line,
12842                    )
12843                    .into());
12844                }
12845                let items = val.to_list();
12846                let mut map = IndexMap::new();
12847                let mut i = 0;
12848                while i + 1 < items.len() {
12849                    map.insert(items[i].to_string(), items[i + 1].clone());
12850                    i += 2;
12851                }
12852                self.scope.set_hash(name, map)?;
12853                Ok(PerlValue::UNDEF)
12854            }
12855            ExprKind::ArrayElement { array, index } => {
12856                if self.strict_vars
12857                    && !array.contains("::")
12858                    && !self.scope.array_binding_exists(array)
12859                {
12860                    return Err(PerlError::runtime(
12861                        format!(
12862                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
12863                            array, array
12864                        ),
12865                        target.line,
12866                    )
12867                    .into());
12868                }
12869                if self.scope.is_array_frozen(array) {
12870                    return Err(PerlError::runtime(
12871                        format!("Modification of a frozen value: @{}", array),
12872                        target.line,
12873                    )
12874                    .into());
12875                }
12876                let idx = self.eval_expr(index)?.to_int();
12877                let aname = self.stash_array_name_for_package(array);
12878                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
12879                    let class = obj
12880                        .as_blessed_ref()
12881                        .map(|b| b.class.clone())
12882                        .unwrap_or_default();
12883                    let full = format!("{}::STORE", class);
12884                    if let Some(sub) = self.subs.get(&full).cloned() {
12885                        let arg_vals = vec![obj, PerlValue::integer(idx), val];
12886                        return match self.call_sub(
12887                            &sub,
12888                            arg_vals,
12889                            WantarrayCtx::Scalar,
12890                            target.line,
12891                        ) {
12892                            Ok(_) => Ok(PerlValue::UNDEF),
12893                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
12894                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
12895                        };
12896                    }
12897                }
12898                self.scope.set_array_element(&aname, idx, val)?;
12899                Ok(PerlValue::UNDEF)
12900            }
12901            ExprKind::ArraySlice { array, indices } => {
12902                if indices.is_empty() {
12903                    return Err(
12904                        PerlError::runtime("assign to empty array slice", target.line).into(),
12905                    );
12906                }
12907                self.check_strict_array_var(array, target.line)?;
12908                if self.scope.is_array_frozen(array) {
12909                    return Err(PerlError::runtime(
12910                        format!("Modification of a frozen value: @{}", array),
12911                        target.line,
12912                    )
12913                    .into());
12914                }
12915                let aname = self.stash_array_name_for_package(array);
12916                let flat = self.flatten_array_slice_index_specs(indices)?;
12917                self.assign_named_array_slice(&aname, flat, val, target.line)
12918            }
12919            ExprKind::HashElement { hash, key } => {
12920                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
12921                {
12922                    return Err(PerlError::runtime(
12923                        format!(
12924                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
12925                            hash, hash
12926                        ),
12927                        target.line,
12928                    )
12929                    .into());
12930                }
12931                if self.scope.is_hash_frozen(hash) {
12932                    return Err(PerlError::runtime(
12933                        format!("Modification of a frozen value: %%{}", hash),
12934                        target.line,
12935                    )
12936                    .into());
12937                }
12938                let k = self.eval_expr(key)?.to_string();
12939                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
12940                    let class = obj
12941                        .as_blessed_ref()
12942                        .map(|b| b.class.clone())
12943                        .unwrap_or_default();
12944                    let full = format!("{}::STORE", class);
12945                    if let Some(sub) = self.subs.get(&full).cloned() {
12946                        let arg_vals = vec![obj, PerlValue::string(k), val];
12947                        return match self.call_sub(
12948                            &sub,
12949                            arg_vals,
12950                            WantarrayCtx::Scalar,
12951                            target.line,
12952                        ) {
12953                            Ok(_) => Ok(PerlValue::UNDEF),
12954                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
12955                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
12956                        };
12957                    }
12958                }
12959                self.scope.set_hash_element(hash, &k, val)?;
12960                Ok(PerlValue::UNDEF)
12961            }
12962            ExprKind::HashSlice { hash, keys } => {
12963                if keys.is_empty() {
12964                    return Err(
12965                        PerlError::runtime("assign to empty hash slice", target.line).into(),
12966                    );
12967                }
12968                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
12969                {
12970                    return Err(PerlError::runtime(
12971                        format!(
12972                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
12973                            hash, hash
12974                        ),
12975                        target.line,
12976                    )
12977                    .into());
12978                }
12979                if self.scope.is_hash_frozen(hash) {
12980                    return Err(PerlError::runtime(
12981                        format!("Modification of a frozen value: %%{}", hash),
12982                        target.line,
12983                    )
12984                    .into());
12985                }
12986                let mut key_vals = Vec::with_capacity(keys.len());
12987                for key_expr in keys {
12988                    let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
12989                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
12990                    } else {
12991                        self.eval_expr(key_expr)?
12992                    };
12993                    key_vals.push(v);
12994                }
12995                self.assign_named_hash_slice(hash, key_vals, val, target.line)
12996            }
12997            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
12998            ExprKind::TypeglobExpr(e) => {
12999                let name = self.eval_expr(e)?.to_string();
13000                let synthetic = Expr {
13001                    kind: ExprKind::Typeglob(name),
13002                    line: target.line,
13003                };
13004                self.assign_value(&synthetic, val)
13005            }
13006            ExprKind::AnonymousListSlice { source, indices } => {
13007                if let ExprKind::Deref {
13008                    expr: inner,
13009                    kind: Sigil::Array,
13010                } = &source.kind
13011                {
13012                    let container = self.eval_arrow_array_base(inner, target.line)?;
13013                    let vals = val.to_list();
13014                    let n = indices.len().min(vals.len());
13015                    for i in 0..n {
13016                        let idx = self.eval_expr(&indices[i])?.to_int();
13017                        self.assign_arrow_array_deref(
13018                            container.clone(),
13019                            idx,
13020                            vals[i].clone(),
13021                            target.line,
13022                        )?;
13023                    }
13024                    return Ok(PerlValue::UNDEF);
13025                }
13026                Err(
13027                    PerlError::runtime("assign to list slice: unsupported base", target.line)
13028                        .into(),
13029                )
13030            }
13031            ExprKind::ArrowDeref {
13032                expr,
13033                index,
13034                kind: DerefKind::Hash,
13035            } => {
13036                let key = self.eval_expr(index)?.to_string();
13037                let container = self.eval_expr(expr)?;
13038                self.assign_arrow_hash_deref(container, key, val, target.line)
13039            }
13040            ExprKind::ArrowDeref {
13041                expr,
13042                index,
13043                kind: DerefKind::Array,
13044            } => {
13045                let container = self.eval_arrow_array_base(expr, target.line)?;
13046                if let ExprKind::List(indices) = &index.kind {
13047                    let vals = val.to_list();
13048                    let n = indices.len().min(vals.len());
13049                    for i in 0..n {
13050                        let idx = self.eval_expr(&indices[i])?.to_int();
13051                        self.assign_arrow_array_deref(
13052                            container.clone(),
13053                            idx,
13054                            vals[i].clone(),
13055                            target.line,
13056                        )?;
13057                    }
13058                    return Ok(PerlValue::UNDEF);
13059                }
13060                let idx = self.eval_expr(index)?.to_int();
13061                self.assign_arrow_array_deref(container, idx, val, target.line)
13062            }
13063            ExprKind::HashSliceDeref { container, keys } => {
13064                let href = self.eval_expr(container)?;
13065                let mut key_vals = Vec::with_capacity(keys.len());
13066                for key_expr in keys {
13067                    key_vals.push(self.eval_expr(key_expr)?);
13068                }
13069                self.assign_hash_slice_deref(href, key_vals, val, target.line)
13070            }
13071            ExprKind::Deref {
13072                expr,
13073                kind: Sigil::Scalar,
13074            } => {
13075                let ref_val = self.eval_expr(expr)?;
13076                self.assign_scalar_ref_deref(ref_val, val, target.line)
13077            }
13078            ExprKind::Deref {
13079                expr,
13080                kind: Sigil::Array,
13081            } => {
13082                let ref_val = self.eval_expr(expr)?;
13083                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
13084            }
13085            ExprKind::Deref {
13086                expr,
13087                kind: Sigil::Hash,
13088            } => {
13089                let ref_val = self.eval_expr(expr)?;
13090                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
13091            }
13092            ExprKind::Deref {
13093                expr,
13094                kind: Sigil::Typeglob,
13095            } => {
13096                let ref_val = self.eval_expr(expr)?;
13097                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
13098            }
13099            ExprKind::Pos(inner) => {
13100                let key = match inner {
13101                    None => "_".to_string(),
13102                    Some(expr) => match &expr.kind {
13103                        ExprKind::ScalarVar(n) => n.clone(),
13104                        _ => self.eval_expr(expr)?.to_string(),
13105                    },
13106                };
13107                if val.is_undef() {
13108                    self.regex_pos.insert(key, None);
13109                } else {
13110                    let u = val.to_int().max(0) as usize;
13111                    self.regex_pos.insert(key, Some(u));
13112                }
13113                Ok(PerlValue::UNDEF)
13114            }
13115            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
13116            // write the substitution result back to the assignment target.
13117            ExprKind::Assign { target, .. } => self.assign_value(target, val),
13118            _ => Ok(PerlValue::UNDEF),
13119        }
13120    }
13121
13122    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
13123    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
13124        (name.starts_with('#') && name.len() > 1)
13125            || name.starts_with('^')
13126            || matches!(
13127                name,
13128                "$$" | "0"
13129                    | "!"
13130                    | "@"
13131                    | "/"
13132                    | "\\"
13133                    | ","
13134                    | "."
13135                    | "]"
13136                    | ";"
13137                    | "ARGV"
13138                    | "^I"
13139                    | "^D"
13140                    | "^P"
13141                    | "^S"
13142                    | "^W"
13143                    | "^O"
13144                    | "^T"
13145                    | "^V"
13146                    | "^E"
13147                    | "^H"
13148                    | "^WARNING_BITS"
13149                    | "^GLOBAL_PHASE"
13150                    | "^MATCH"
13151                    | "^PREMATCH"
13152                    | "^POSTMATCH"
13153                    | "^LAST_SUBMATCH_RESULT"
13154                    | "<"
13155                    | ">"
13156                    | "("
13157                    | ")"
13158                    | "?"
13159                    | "|"
13160                    | "\""
13161                    | "+"
13162                    | "%"
13163                    | "="
13164                    | "-"
13165                    | ":"
13166                    | "*"
13167                    | "INC"
13168            )
13169            || crate::english::is_known_alias(name)
13170    }
13171
13172    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
13173    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
13174    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
13175    /// [`Self::english_no_match_vars`] is set.
13176    #[inline]
13177    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
13178        if !self.english_enabled {
13179            return name;
13180        }
13181        if self
13182            .english_lexical_scalars
13183            .iter()
13184            .any(|s| s.contains(name))
13185        {
13186            return name;
13187        }
13188        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
13189            return short;
13190        }
13191        name
13192    }
13193
13194    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
13195    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
13196        name.starts_with('^')
13197            || matches!(
13198                name,
13199                "0" | "/"
13200                    | "\\"
13201                    | ","
13202                    | ";"
13203                    | "\""
13204                    | "%"
13205                    | "="
13206                    | "-"
13207                    | ":"
13208                    | "*"
13209                    | "INC"
13210                    | "^I"
13211                    | "^D"
13212                    | "^P"
13213                    | "^W"
13214                    | "^H"
13215                    | "^WARNING_BITS"
13216                    | "$$"
13217                    | "]"
13218                    | "^S"
13219                    | "ARGV"
13220                    | "|"
13221                    | "+"
13222                    | "?"
13223                    | "!"
13224                    | "@"
13225                    | "."
13226            )
13227            || crate::english::is_known_alias(name)
13228    }
13229
13230    pub(crate) fn get_special_var(&self, name: &str) -> PerlValue {
13231        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
13232        let name = if !crate::compat_mode() {
13233            match name {
13234                "NR" => ".",
13235                "RS" => "/",
13236                "OFS" => ",",
13237                "ORS" => "\\",
13238                "NF" => {
13239                    let len = self.scope.array_len("F");
13240                    return PerlValue::integer(len as i64);
13241                }
13242                _ => self.english_scalar_name(name),
13243            }
13244        } else {
13245            self.english_scalar_name(name)
13246        };
13247        match name {
13248            "$$" => PerlValue::integer(std::process::id() as i64),
13249            "_" => self.scope.get_scalar("_"),
13250            "^MATCH" => PerlValue::string(self.last_match.clone()),
13251            "^PREMATCH" => PerlValue::string(self.prematch.clone()),
13252            "^POSTMATCH" => PerlValue::string(self.postmatch.clone()),
13253            "^LAST_SUBMATCH_RESULT" => PerlValue::string(self.last_paren_match.clone()),
13254            "0" => PerlValue::string(self.program_name.clone()),
13255            "!" => PerlValue::errno_dual(self.errno_code, self.errno.clone()),
13256            "@" => {
13257                if let Some(ref v) = self.eval_error_value {
13258                    v.clone()
13259                } else {
13260                    PerlValue::errno_dual(self.eval_error_code, self.eval_error.clone())
13261                }
13262            }
13263            "/" => match &self.irs {
13264                Some(s) => PerlValue::string(s.clone()),
13265                None => PerlValue::UNDEF,
13266            },
13267            "\\" => PerlValue::string(self.ors.clone()),
13268            "," => PerlValue::string(self.ofs.clone()),
13269            "." => {
13270                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
13271                if self.last_readline_handle.is_empty() {
13272                    if self.line_number == 0 {
13273                        PerlValue::UNDEF
13274                    } else {
13275                        PerlValue::integer(self.line_number)
13276                    }
13277                } else {
13278                    PerlValue::integer(
13279                        *self
13280                            .handle_line_numbers
13281                            .get(&self.last_readline_handle)
13282                            .unwrap_or(&0),
13283                    )
13284                }
13285            }
13286            "]" => PerlValue::float(perl_bracket_version()),
13287            ";" => PerlValue::string(self.subscript_sep.clone()),
13288            "ARGV" => PerlValue::string(self.argv_current_file.clone()),
13289            "^I" => PerlValue::string(self.inplace_edit.clone()),
13290            "^D" => PerlValue::integer(self.debug_flags),
13291            "^P" => PerlValue::integer(self.perl_debug_flags),
13292            "^S" => PerlValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
13293            "^W" => PerlValue::integer(if self.warnings { 1 } else { 0 }),
13294            "^O" => PerlValue::string(perl_osname()),
13295            "^T" => PerlValue::integer(self.script_start_time),
13296            "^V" => PerlValue::string(perl_version_v_string()),
13297            "^E" => PerlValue::string(extended_os_error_string()),
13298            "^H" => PerlValue::integer(self.compile_hints),
13299            "^WARNING_BITS" => PerlValue::integer(self.warning_bits),
13300            "^GLOBAL_PHASE" => PerlValue::string(self.global_phase.clone()),
13301            "<" | ">" => PerlValue::integer(unix_id_for_special(name)),
13302            "(" | ")" => PerlValue::string(unix_group_list_for_special(name)),
13303            "?" => PerlValue::integer(self.child_exit_status),
13304            "|" => PerlValue::integer(if self.output_autoflush { 1 } else { 0 }),
13305            "\"" => PerlValue::string(self.list_separator.clone()),
13306            "+" => PerlValue::string(self.last_paren_match.clone()),
13307            "%" => PerlValue::integer(self.format_page_number),
13308            "=" => PerlValue::integer(self.format_lines_per_page),
13309            "-" => PerlValue::integer(self.format_lines_left),
13310            ":" => PerlValue::string(self.format_line_break_chars.clone()),
13311            "*" => PerlValue::integer(if self.multiline_match { 1 } else { 0 }),
13312            "^" => PerlValue::string(self.format_top_name.clone()),
13313            "INC" => PerlValue::integer(self.inc_hook_index),
13314            "^A" => PerlValue::string(self.accumulator_format.clone()),
13315            "^C" => PerlValue::integer(if self.sigint_pending_caret.replace(false) {
13316                1
13317            } else {
13318                0
13319            }),
13320            "^F" => PerlValue::integer(self.max_system_fd),
13321            "^L" => PerlValue::string(self.formfeed_string.clone()),
13322            "^M" => PerlValue::string(self.emergency_memory.clone()),
13323            "^N" => PerlValue::string(self.last_subpattern_name.clone()),
13324            "^X" => PerlValue::string(self.executable_path.clone()),
13325            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
13326            "^TAINT" | "^TAINTED" => PerlValue::integer(0),
13327            "^UNICODE" => PerlValue::integer(if self.utf8_pragma { 1 } else { 0 }),
13328            "^OPEN" => PerlValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
13329            "^UTF8LOCALE" => PerlValue::integer(0),
13330            "^UTF8CACHE" => PerlValue::integer(-1),
13331            _ if name.starts_with('^') && name.len() > 1 => self
13332                .special_caret_scalars
13333                .get(name)
13334                .cloned()
13335                .unwrap_or(PerlValue::UNDEF),
13336            _ if name.starts_with('#') && name.len() > 1 => {
13337                let arr = &name[1..];
13338                let aname = self.stash_array_name_for_package(arr);
13339                let len = self.scope.array_len(&aname);
13340                PerlValue::integer(len as i64 - 1)
13341            }
13342            _ => self.scope.get_scalar(name),
13343        }
13344    }
13345
13346    pub(crate) fn set_special_var(&mut self, name: &str, val: &PerlValue) -> Result<(), PerlError> {
13347        let name = self.english_scalar_name(name);
13348        match name {
13349            "!" => {
13350                let code = val.to_int() as i32;
13351                self.errno_code = code;
13352                self.errno = if code == 0 {
13353                    String::new()
13354                } else {
13355                    std::io::Error::from_raw_os_error(code).to_string()
13356                };
13357            }
13358            "@" => {
13359                if let Some((code, msg)) = val.errno_dual_parts() {
13360                    self.eval_error_code = code;
13361                    self.eval_error = msg;
13362                } else {
13363                    self.eval_error = val.to_string();
13364                    let mut code = val.to_int() as i32;
13365                    if code == 0 && !self.eval_error.is_empty() {
13366                        code = 1;
13367                    }
13368                    self.eval_error_code = code;
13369                }
13370            }
13371            "." => {
13372                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
13373                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
13374                let n = val.to_int();
13375                if self.last_readline_handle.is_empty() {
13376                    self.line_number = n;
13377                } else {
13378                    self.handle_line_numbers
13379                        .insert(self.last_readline_handle.clone(), n);
13380                }
13381            }
13382            "0" => self.program_name = val.to_string(),
13383            "/" => {
13384                self.irs = if val.is_undef() {
13385                    None
13386                } else {
13387                    Some(val.to_string())
13388                }
13389            }
13390            "\\" => self.ors = val.to_string(),
13391            "," => self.ofs = val.to_string(),
13392            ";" => self.subscript_sep = val.to_string(),
13393            "\"" => self.list_separator = val.to_string(),
13394            "%" => self.format_page_number = val.to_int(),
13395            "=" => self.format_lines_per_page = val.to_int(),
13396            "-" => self.format_lines_left = val.to_int(),
13397            ":" => self.format_line_break_chars = val.to_string(),
13398            "*" => self.multiline_match = val.to_int() != 0,
13399            "^" => self.format_top_name = val.to_string(),
13400            "INC" => self.inc_hook_index = val.to_int(),
13401            "^A" => self.accumulator_format = val.to_string(),
13402            "^F" => self.max_system_fd = val.to_int(),
13403            "^L" => self.formfeed_string = val.to_string(),
13404            "^M" => self.emergency_memory = val.to_string(),
13405            "^I" => self.inplace_edit = val.to_string(),
13406            "^D" => self.debug_flags = val.to_int(),
13407            "^P" => self.perl_debug_flags = val.to_int(),
13408            "^W" => self.warnings = val.to_int() != 0,
13409            "^H" => self.compile_hints = val.to_int(),
13410            "^WARNING_BITS" => self.warning_bits = val.to_int(),
13411            "|" => {
13412                self.output_autoflush = val.to_int() != 0;
13413                if self.output_autoflush {
13414                    let _ = io::stdout().flush();
13415                }
13416            }
13417            // Read-only or pid-backed
13418            "$$"
13419            | "]"
13420            | "^S"
13421            | "ARGV"
13422            | "?"
13423            | "^O"
13424            | "^T"
13425            | "^V"
13426            | "^E"
13427            | "^GLOBAL_PHASE"
13428            | "^MATCH"
13429            | "^PREMATCH"
13430            | "^POSTMATCH"
13431            | "^LAST_SUBMATCH_RESULT"
13432            | "^C"
13433            | "^N"
13434            | "^X"
13435            | "^TAINT"
13436            | "^TAINTED"
13437            | "^UNICODE"
13438            | "^UTF8LOCALE"
13439            | "^UTF8CACHE"
13440            | "+"
13441            | "<"
13442            | ">"
13443            | "("
13444            | ")" => {}
13445            _ if name.starts_with('^') && name.len() > 1 => {
13446                self.special_caret_scalars
13447                    .insert(name.to_string(), val.clone());
13448            }
13449            _ => self.scope.set_scalar(name, val.clone())?,
13450        }
13451        Ok(())
13452    }
13453
13454    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
13455        match &expr.kind {
13456            ExprKind::ArrayVar(name) => Ok(name.clone()),
13457            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
13458            _ => Err(PerlError::runtime("Expected array", expr.line).into()),
13459        }
13460    }
13461
13462    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
13463    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
13464        match &expr.kind {
13465            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
13466            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
13467            _ => expr,
13468        }
13469    }
13470
13471    /// `@$aref` / `@{...}` after optional peeling — for tree `SpliceExpr` / `pop` fallbacks.
13472    fn try_eval_array_deref_container(
13473        &mut self,
13474        expr: &Expr,
13475    ) -> Result<Option<PerlValue>, FlowOrError> {
13476        let e = Self::peel_array_builtin_operand(expr);
13477        if let ExprKind::Deref {
13478            expr: inner,
13479            kind: Sigil::Array,
13480        } = &e.kind
13481        {
13482            return Ok(Some(self.eval_expr(inner)?));
13483        }
13484        Ok(None)
13485    }
13486
13487    /// Current package (`main` when `__PACKAGE__` is unset or empty).
13488    fn current_package(&self) -> String {
13489        let s = self.scope.get_scalar("__PACKAGE__").to_string();
13490        if s.is_empty() {
13491            "main".to_string()
13492        } else {
13493            s
13494        }
13495    }
13496
13497    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
13498    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
13499    pub(crate) fn package_version_scalar(
13500        &mut self,
13501        package: &str,
13502    ) -> PerlResult<Option<PerlValue>> {
13503        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
13504        let _ = self
13505            .scope
13506            .set_scalar("__PACKAGE__", PerlValue::string(package.to_string()));
13507        let ver = self.get_special_var("VERSION");
13508        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
13509        Ok(if ver.is_undef() { None } else { Some(ver) })
13510    }
13511
13512    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
13513    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<PerlSub>> {
13514        let root = if start_package.is_empty() {
13515            "main"
13516        } else {
13517            start_package
13518        };
13519        for pkg in self.mro_linearize(root) {
13520            let key = if pkg == "main" {
13521                "AUTOLOAD".to_string()
13522            } else {
13523                format!("{}::AUTOLOAD", pkg)
13524            };
13525            if let Some(s) = self.subs.get(&key) {
13526                return Some(s.clone());
13527            }
13528        }
13529        None
13530    }
13531
13532    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
13533    /// qualified missing sub or method name and invoke the handler (same argument list as the
13534    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
13535    /// the package prefix of the missing name (or current package).
13536    pub(crate) fn try_autoload_call(
13537        &mut self,
13538        missing_name: &str,
13539        args: Vec<PerlValue>,
13540        line: usize,
13541        want: WantarrayCtx,
13542        method_invocant_class: Option<&str>,
13543    ) -> Option<ExecResult> {
13544        let pkg = self.current_package();
13545        let full = if missing_name.contains("::") {
13546            missing_name.to_string()
13547        } else {
13548            format!("{}::{}", pkg, missing_name)
13549        };
13550        let start_pkg = method_invocant_class.unwrap_or_else(|| {
13551            full.rsplit_once("::")
13552                .map(|(p, _)| p)
13553                .filter(|p| !p.is_empty())
13554                .unwrap_or("main")
13555        });
13556        let sub = self.resolve_autoload_sub(start_pkg)?;
13557        if let Err(e) = self
13558            .scope
13559            .set_scalar("AUTOLOAD", PerlValue::string(full.clone()))
13560        {
13561            return Some(Err(e.into()));
13562        }
13563        Some(self.call_sub(&sub, args, want, line))
13564    }
13565
13566    pub(crate) fn with_topic_default_args(&self, args: Vec<PerlValue>) -> Vec<PerlValue> {
13567        if args.is_empty() {
13568            vec![self.scope.get_scalar("_").clone()]
13569        } else {
13570            args
13571        }
13572    }
13573
13574    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
13575    /// and [`crate::bytecode::Op::IndirectCall`].
13576    pub(crate) fn dispatch_indirect_call(
13577        &mut self,
13578        target: PerlValue,
13579        arg_vals: Vec<PerlValue>,
13580        want: WantarrayCtx,
13581        line: usize,
13582    ) -> ExecResult {
13583        if let Some(sub) = target.as_code_ref() {
13584            return self.call_sub(&sub, arg_vals, want, line);
13585        }
13586        if let Some(name) = target.as_str() {
13587            return self.call_named_sub(&name, arg_vals, line, want);
13588        }
13589        Err(PerlError::runtime("Can't use non-code reference as a subroutine", line).into())
13590    }
13591
13592    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
13593    /// `sum` / `sum0` /
13594    /// `product` / `min` / `max` / `mean` / `median` / `mode` / `stddev` / `variance` /
13595    /// `any` / `all` / `none` / `first` (Ruby `detect` / `find` parse to `first`; same as `List::Util` after
13596    /// [`crate::list_util::ensure_list_util`]).
13597    pub(crate) fn call_bare_list_util(
13598        &mut self,
13599        name: &str,
13600        args: Vec<PerlValue>,
13601        line: usize,
13602        want: WantarrayCtx,
13603    ) -> ExecResult {
13604        crate::list_util::ensure_list_util(self);
13605        let fq = match name {
13606            "uniq" | "distinct" | "uq" => "List::Util::uniq",
13607            "uniqstr" => "List::Util::uniqstr",
13608            "uniqint" => "List::Util::uniqint",
13609            "uniqnum" => "List::Util::uniqnum",
13610            "shuffle" | "shuf" => "List::Util::shuffle",
13611            "sample" => "List::Util::sample",
13612            "chunked" | "chk" => "List::Util::chunked",
13613            "windowed" | "win" => "List::Util::windowed",
13614            "zip" | "zp" => "List::Util::zip",
13615            "zip_longest" => "List::Util::zip_longest",
13616            "zip_shortest" => "List::Util::zip_shortest",
13617            "mesh" => "List::Util::mesh",
13618            "mesh_longest" => "List::Util::mesh_longest",
13619            "mesh_shortest" => "List::Util::mesh_shortest",
13620            "any" => "List::Util::any",
13621            "all" => "List::Util::all",
13622            "none" => "List::Util::none",
13623            "notall" => "List::Util::notall",
13624            "first" | "fst" => "List::Util::first",
13625            "reduce" | "rd" => "List::Util::reduce",
13626            "reductions" => "List::Util::reductions",
13627            "sum" => "List::Util::sum",
13628            "sum0" => "List::Util::sum0",
13629            "product" => "List::Util::product",
13630            "min" => "List::Util::min",
13631            "max" => "List::Util::max",
13632            "minstr" => "List::Util::minstr",
13633            "maxstr" => "List::Util::maxstr",
13634            "mean" => "List::Util::mean",
13635            "median" | "med" => "List::Util::median",
13636            "mode" => "List::Util::mode",
13637            "stddev" | "std" => "List::Util::stddev",
13638            "variance" | "var" => "List::Util::variance",
13639            "pairs" => "List::Util::pairs",
13640            "unpairs" => "List::Util::unpairs",
13641            "pairkeys" => "List::Util::pairkeys",
13642            "pairvalues" => "List::Util::pairvalues",
13643            "pairgrep" => "List::Util::pairgrep",
13644            "pairmap" => "List::Util::pairmap",
13645            "pairfirst" => "List::Util::pairfirst",
13646            _ => {
13647                return Err(PerlError::runtime(
13648                    format!("internal: not a bare list-util alias: {name}"),
13649                    line,
13650                )
13651                .into());
13652            }
13653        };
13654        let Some(sub) = self.subs.get(fq).cloned() else {
13655            return Err(PerlError::runtime(
13656                format!("internal: missing native stub for {fq}"),
13657                line,
13658            )
13659            .into());
13660        };
13661        let args = self.with_topic_default_args(args);
13662        self.call_sub(&sub, args, want, line)
13663    }
13664
13665    fn call_named_sub(
13666        &mut self,
13667        name: &str,
13668        args: Vec<PerlValue>,
13669        line: usize,
13670        want: WantarrayCtx,
13671    ) -> ExecResult {
13672        if let Some(sub) = self.resolve_sub_by_name(name) {
13673            let args = self.with_topic_default_args(args);
13674            return self.call_sub(&sub, args, want, line);
13675        }
13676        match name {
13677            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
13678            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
13679            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
13680            | "none" | "notall" | "first" | "fst" | "reduce" | "rd" | "reductions" | "sum"
13681            | "sum0" | "product" | "min" | "max" | "minstr" | "maxstr" | "mean" | "median"
13682            | "med" | "mode" | "stddev" | "std" | "variance" | "var" | "pairs" | "unpairs"
13683            | "pairkeys" | "pairvalues" | "pairgrep" | "pairmap" | "pairfirst" => {
13684                self.call_bare_list_util(name, args, line, want)
13685            }
13686            "deque" => {
13687                if !args.is_empty() {
13688                    return Err(PerlError::runtime("deque() takes no arguments", line).into());
13689                }
13690                Ok(PerlValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
13691            }
13692            "defer__internal" => {
13693                if args.len() != 1 {
13694                    return Err(PerlError::runtime(
13695                        "defer__internal expects one coderef argument",
13696                        line,
13697                    )
13698                    .into());
13699                }
13700                self.scope.push_defer(args[0].clone());
13701                Ok(PerlValue::UNDEF)
13702            }
13703            "heap" => {
13704                if args.len() != 1 {
13705                    return Err(
13706                        PerlError::runtime("heap() expects one comparator sub", line).into(),
13707                    );
13708                }
13709                if let Some(sub) = args[0].as_code_ref() {
13710                    Ok(PerlValue::heap(Arc::new(Mutex::new(PerlHeap {
13711                        items: Vec::new(),
13712                        cmp: Arc::clone(&sub),
13713                    }))))
13714                } else {
13715                    Err(PerlError::runtime("heap() requires a code reference", line).into())
13716                }
13717            }
13718            "pipeline" => {
13719                let mut items = Vec::new();
13720                for v in args {
13721                    if let Some(a) = v.as_array_vec() {
13722                        items.extend(a);
13723                    } else {
13724                        items.push(v);
13725                    }
13726                }
13727                Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
13728                    source: items,
13729                    ops: Vec::new(),
13730                    has_scalar_terminal: false,
13731                    par_stream: false,
13732                    streaming: false,
13733                    streaming_workers: 0,
13734                    streaming_buffer: 256,
13735                }))))
13736            }
13737            "par_pipeline" => {
13738                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
13739                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
13740                        .map_err(Into::into);
13741                }
13742                Ok(self.builtin_par_pipeline_stream(&args, line)?)
13743            }
13744            "par_pipeline_stream" => {
13745                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
13746                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
13747                        .map_err(Into::into);
13748                }
13749                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
13750            }
13751            "ppool" => {
13752                if args.len() != 1 {
13753                    return Err(PerlError::runtime(
13754                        "ppool() expects one argument (worker count)",
13755                        line,
13756                    )
13757                    .into());
13758                }
13759                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
13760            }
13761            "barrier" => {
13762                if args.len() != 1 {
13763                    return Err(PerlError::runtime(
13764                        "barrier() expects one argument (party count)",
13765                        line,
13766                    )
13767                    .into());
13768                }
13769                let n = args[0].to_int().max(1) as usize;
13770                Ok(PerlValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
13771            }
13772            "cluster" => {
13773                let items = if args.len() == 1 {
13774                    args[0].to_list()
13775                } else {
13776                    args.to_vec()
13777                };
13778                let c = RemoteCluster::from_list_args(&items)
13779                    .map_err(|msg| PerlError::runtime(msg, line))?;
13780                Ok(PerlValue::remote_cluster(Arc::new(c)))
13781            }
13782            _ => {
13783                // Late static binding: static::method() resolves to runtime class of $self
13784                if let Some(method_name) = name.strip_prefix("static::") {
13785                    let self_val = self.scope.get_scalar("self");
13786                    if let Some(c) = self_val.as_class_inst() {
13787                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
13788                            if let Some(ref body) = m.body {
13789                                let params = m.params.clone();
13790                                let mut call_args = vec![self_val.clone()];
13791                                call_args.extend(args);
13792                                return match self.call_class_method(body, &params, call_args, line)
13793                                {
13794                                    Ok(v) => Ok(v),
13795                                    Err(FlowOrError::Error(e)) => Err(e.into()),
13796                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
13797                                    Err(e) => Err(e),
13798                                };
13799                            }
13800                        }
13801                        return Err(PerlError::runtime(
13802                            format!(
13803                                "static::{} — method not found on class {}",
13804                                method_name, c.def.name
13805                            ),
13806                            line,
13807                        )
13808                        .into());
13809                    }
13810                    return Err(PerlError::runtime(
13811                        "static:: can only be used inside a class method",
13812                        line,
13813                    )
13814                    .into());
13815                }
13816                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
13817                if let Some(def) = self.struct_defs.get(name).cloned() {
13818                    return self.struct_construct(&def, args, line);
13819                }
13820                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
13821                if let Some(def) = self.class_defs.get(name).cloned() {
13822                    return self.class_construct(&def, args, line);
13823                }
13824                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
13825                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
13826                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
13827                        return self.enum_construct(&def, variant_name, args, line);
13828                    }
13829                }
13830                // Check for static class method or static field: Math::add(...) / Counter::count()
13831                if let Some((class_name, member_name)) = name.rsplit_once("::") {
13832                    if let Some(def) = self.class_defs.get(class_name).cloned() {
13833                        // Static method
13834                        if let Some(m) = def.method(member_name) {
13835                            if m.is_static {
13836                                if let Some(ref body) = m.body {
13837                                    let params = m.params.clone();
13838                                    return match self.call_static_class_method(
13839                                        body,
13840                                        &params,
13841                                        args.clone(),
13842                                        line,
13843                                    ) {
13844                                        Ok(v) => Ok(v),
13845                                        Err(FlowOrError::Error(e)) => Err(e.into()),
13846                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
13847                                        Err(e) => Err(e),
13848                                    };
13849                                }
13850                            }
13851                        }
13852                        // Static field access: getter (0 args) or setter (1 arg)
13853                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
13854                            let key = format!("{}::{}", class_name, member_name);
13855                            match args.len() {
13856                                0 => {
13857                                    let val = self.scope.get_scalar(&key);
13858                                    return Ok(val);
13859                                }
13860                                1 => {
13861                                    let _ = self.scope.set_scalar(&key, args[0].clone());
13862                                    return Ok(args[0].clone());
13863                                }
13864                                _ => {
13865                                    return Err(PerlError::runtime(
13866                                        format!(
13867                                            "static field `{}::{}` takes 0 or 1 arguments",
13868                                            class_name, member_name
13869                                        ),
13870                                        line,
13871                                    )
13872                                    .into());
13873                                }
13874                            }
13875                        }
13876                    }
13877                }
13878                let args = self.with_topic_default_args(args);
13879                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
13880                    return r;
13881                }
13882                Err(PerlError::runtime(self.undefined_subroutine_call_message(name), line).into())
13883            }
13884        }
13885    }
13886
13887    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
13888    pub(crate) fn struct_construct(
13889        &mut self,
13890        def: &Arc<StructDef>,
13891        args: Vec<PerlValue>,
13892        line: usize,
13893    ) -> ExecResult {
13894        // Detect if args are named (key => value pairs) or positional
13895        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
13896        let is_named = args.len() >= 2
13897            && args.len().is_multiple_of(2)
13898            && args.iter().step_by(2).all(|v| {
13899                let s = v.to_string();
13900                def.field_index(&s).is_some()
13901            });
13902
13903        let provided = if is_named {
13904            // Named construction: Point(x => 1, y => 2)
13905            let mut pairs = Vec::new();
13906            let mut i = 0;
13907            while i + 1 < args.len() {
13908                let k = args[i].to_string();
13909                let v = args[i + 1].clone();
13910                pairs.push((k, v));
13911                i += 2;
13912            }
13913            pairs
13914        } else {
13915            // Positional construction: Point(1, 2) fills fields in declaration order
13916            def.fields
13917                .iter()
13918                .zip(args.iter())
13919                .map(|(f, v)| (f.name.clone(), v.clone()))
13920                .collect()
13921        };
13922
13923        // Evaluate default expressions
13924        let mut defaults = Vec::with_capacity(def.fields.len());
13925        for field in &def.fields {
13926            if let Some(ref expr) = field.default {
13927                let val = self.eval_expr(expr)?;
13928                defaults.push(Some(val));
13929            } else {
13930                defaults.push(None);
13931            }
13932        }
13933
13934        Ok(crate::native_data::struct_new_with_defaults(
13935            def, &provided, &defaults, line,
13936        )?)
13937    }
13938
13939    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
13940    pub(crate) fn class_construct(
13941        &mut self,
13942        def: &Arc<ClassDef>,
13943        args: Vec<PerlValue>,
13944        _line: usize,
13945    ) -> ExecResult {
13946        use crate::value::ClassInstance;
13947
13948        // Prevent instantiation of abstract classes
13949        if def.is_abstract {
13950            return Err(PerlError::runtime(
13951                format!("cannot instantiate abstract class `{}`", def.name),
13952                _line,
13953            )
13954            .into());
13955        }
13956
13957        // Collect all fields from inheritance chain (parent fields first)
13958        let all_fields = self.collect_class_fields(def);
13959
13960        // Check if args are named
13961        let is_named = args.len() >= 2
13962            && args.len().is_multiple_of(2)
13963            && args.iter().step_by(2).all(|v| {
13964                let s = v.to_string();
13965                all_fields.iter().any(|(name, _, _)| name == &s)
13966            });
13967
13968        let provided: Vec<(String, PerlValue)> = if is_named {
13969            let mut pairs = Vec::new();
13970            let mut i = 0;
13971            while i + 1 < args.len() {
13972                let k = args[i].to_string();
13973                let v = args[i + 1].clone();
13974                pairs.push((k, v));
13975                i += 2;
13976            }
13977            pairs
13978        } else {
13979            all_fields
13980                .iter()
13981                .zip(args.iter())
13982                .map(|((name, _, _), v)| (name.clone(), v.clone()))
13983                .collect()
13984        };
13985
13986        // Build values array for all fields (inherited + own) with type checking
13987        let mut values = Vec::with_capacity(all_fields.len());
13988        for (name, default, ty) in &all_fields {
13989            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
13990                val.clone()
13991            } else if let Some(ref expr) = default {
13992                self.eval_expr(expr)?
13993            } else {
13994                PerlValue::UNDEF
13995            };
13996            ty.check_value(&val).map_err(|msg| {
13997                PerlError::type_error(
13998                    format!("class {} field `{}`: {}", def.name, name, msg),
13999                    _line,
14000                )
14001            })?;
14002            values.push(val);
14003        }
14004
14005        let instance = PerlValue::class_inst(Arc::new(ClassInstance::new(Arc::clone(def), values)));
14006
14007        // Call BUILD hooks: parent BUILD first, then child BUILD
14008        let build_chain = self.collect_build_chain(def);
14009        if !build_chain.is_empty() {
14010            for (body, params) in &build_chain {
14011                let call_args = vec![instance.clone()];
14012                match self.call_class_method(body, params, call_args, _line) {
14013                    Ok(_) => {}
14014                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
14015                    Err(e) => return Err(e),
14016                }
14017            }
14018        }
14019
14020        Ok(instance)
14021    }
14022
14023    /// Collect BUILD methods from parent to child order.
14024    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14025        let mut chain = Vec::new();
14026        // Parent BUILD first
14027        for parent_name in &def.extends {
14028            if let Some(parent_def) = self.class_defs.get(parent_name) {
14029                chain.extend(self.collect_build_chain(parent_def));
14030            }
14031        }
14032        // Own BUILD
14033        if let Some(m) = def.method("BUILD") {
14034            if let Some(ref body) = m.body {
14035                chain.push((body.clone(), m.params.clone()));
14036            }
14037        }
14038        chain
14039    }
14040
14041    /// Collect all fields from a class and its parent hierarchy (parent fields first).
14042    /// Returns (name, default, type, visibility, owning_class_name).
14043    fn collect_class_fields(
14044        &self,
14045        def: &ClassDef,
14046    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
14047        self.collect_class_fields_full(def)
14048            .into_iter()
14049            .map(|(name, default, ty, _, _)| (name, default, ty))
14050            .collect()
14051    }
14052
14053    /// Like collect_class_fields but includes visibility and owning class name.
14054    fn collect_class_fields_full(
14055        &self,
14056        def: &ClassDef,
14057    ) -> Vec<(
14058        String,
14059        Option<Expr>,
14060        crate::ast::PerlTypeName,
14061        crate::ast::Visibility,
14062        String,
14063    )> {
14064        let mut all_fields = Vec::new();
14065
14066        for parent_name in &def.extends {
14067            if let Some(parent_def) = self.class_defs.get(parent_name) {
14068                let parent_fields = self.collect_class_fields_full(parent_def);
14069                all_fields.extend(parent_fields);
14070            }
14071        }
14072
14073        for field in &def.fields {
14074            all_fields.push((
14075                field.name.clone(),
14076                field.default.clone(),
14077                field.ty.clone(),
14078                field.visibility,
14079                def.name.clone(),
14080            ));
14081        }
14082
14083        all_fields
14084    }
14085
14086    /// Collect all method names from class and parents (deduplicates, child overrides parent).
14087    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
14088        // Parent methods first
14089        for parent_name in &def.extends {
14090            if let Some(parent_def) = self.class_defs.get(parent_name) {
14091                self.collect_class_method_names(parent_def, names);
14092            }
14093        }
14094        // Own methods (add if not already present — child overrides parent name)
14095        for m in &def.methods {
14096            if !m.is_static && !names.contains(&m.name) {
14097                names.push(m.name.clone());
14098            }
14099        }
14100    }
14101
14102    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
14103    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14104        let mut chain = Vec::new();
14105        // Own DESTROY first
14106        if let Some(m) = def.method("DESTROY") {
14107            if let Some(ref body) = m.body {
14108                chain.push((body.clone(), m.params.clone()));
14109            }
14110        }
14111        // Then parent DESTROY
14112        for parent_name in &def.extends {
14113            if let Some(parent_def) = self.class_defs.get(parent_name) {
14114                chain.extend(self.collect_destroy_chain(parent_def));
14115            }
14116        }
14117        chain
14118    }
14119
14120    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
14121    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
14122        if let Some(def) = self.class_defs.get(child) {
14123            for parent in &def.extends {
14124                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
14125                    return true;
14126                }
14127            }
14128        }
14129        false
14130    }
14131
14132    /// Find a method in a class or its parent hierarchy (child methods override parent).
14133    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
14134        // First check the current class
14135        if let Some(m) = def.method(method) {
14136            return Some((m.clone(), def.name.clone()));
14137        }
14138        // Then check parent classes
14139        for parent_name in &def.extends {
14140            if let Some(parent_def) = self.class_defs.get(parent_name) {
14141                if let Some(result) = self.find_class_method(parent_def, method) {
14142                    return Some(result);
14143                }
14144            }
14145        }
14146        None
14147    }
14148
14149    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
14150    pub(crate) fn enum_construct(
14151        &mut self,
14152        def: &Arc<EnumDef>,
14153        variant_name: &str,
14154        args: Vec<PerlValue>,
14155        line: usize,
14156    ) -> ExecResult {
14157        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
14158            FlowOrError::Error(PerlError::runtime(
14159                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
14160                line,
14161            ))
14162        })?;
14163        let variant = &def.variants[variant_idx];
14164        let data = if variant.ty.is_some() {
14165            if args.is_empty() {
14166                return Err(PerlError::runtime(
14167                    format!(
14168                        "enum variant `{}::{}` requires data",
14169                        def.name, variant_name
14170                    ),
14171                    line,
14172                )
14173                .into());
14174            }
14175            if args.len() == 1 {
14176                args.into_iter().next().unwrap()
14177            } else {
14178                PerlValue::array(args)
14179            }
14180        } else {
14181            if !args.is_empty() {
14182                return Err(PerlError::runtime(
14183                    format!(
14184                        "enum variant `{}::{}` does not take data",
14185                        def.name, variant_name
14186                    ),
14187                    line,
14188                )
14189                .into());
14190            }
14191            PerlValue::UNDEF
14192        };
14193        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
14194        Ok(PerlValue::enum_inst(Arc::new(inst)))
14195    }
14196
14197    /// True if `name` is a registered or standard process-global handle.
14198    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
14199        matches!(name, "STDIN" | "STDOUT" | "STDERR")
14200            || self.input_handles.contains_key(name)
14201            || self.output_handles.contains_key(name)
14202            || self.io_file_slots.contains_key(name)
14203            || self.pipe_children.contains_key(name)
14204    }
14205
14206    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
14207    pub(crate) fn io_handle_method(
14208        &mut self,
14209        name: &str,
14210        method: &str,
14211        args: &[PerlValue],
14212        line: usize,
14213    ) -> PerlResult<PerlValue> {
14214        match method {
14215            "print" => self.io_handle_print(name, args, false, line),
14216            "say" => self.io_handle_print(name, args, true, line),
14217            "printf" => self.io_handle_printf(name, args, line),
14218            "getline" | "readline" => {
14219                if !args.is_empty() {
14220                    return Err(PerlError::runtime(
14221                        format!("{}: too many arguments", method),
14222                        line,
14223                    ));
14224                }
14225                self.readline_builtin_execute(Some(name))
14226            }
14227            "close" => {
14228                if !args.is_empty() {
14229                    return Err(PerlError::runtime("close: too many arguments", line));
14230                }
14231                self.close_builtin_execute(name.to_string())
14232            }
14233            "eof" => {
14234                if !args.is_empty() {
14235                    return Err(PerlError::runtime("eof: too many arguments", line));
14236                }
14237                let at_eof = !self.has_input_handle(name);
14238                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
14239            }
14240            "getc" => {
14241                if !args.is_empty() {
14242                    return Err(PerlError::runtime("getc: too many arguments", line));
14243                }
14244                match crate::builtins::try_builtin(
14245                    self,
14246                    "getc",
14247                    &[PerlValue::string(name.to_string())],
14248                    line,
14249                ) {
14250                    Some(r) => r,
14251                    None => Err(PerlError::runtime("getc: not available", line)),
14252                }
14253            }
14254            "binmode" => match crate::builtins::try_builtin(
14255                self,
14256                "binmode",
14257                &[PerlValue::string(name.to_string())],
14258                line,
14259            ) {
14260                Some(r) => r,
14261                None => Err(PerlError::runtime("binmode: not available", line)),
14262            },
14263            "fileno" => match crate::builtins::try_builtin(
14264                self,
14265                "fileno",
14266                &[PerlValue::string(name.to_string())],
14267                line,
14268            ) {
14269                Some(r) => r,
14270                None => Err(PerlError::runtime("fileno: not available", line)),
14271            },
14272            "flush" => {
14273                if !args.is_empty() {
14274                    return Err(PerlError::runtime("flush: too many arguments", line));
14275                }
14276                self.io_handle_flush(name, line)
14277            }
14278            _ => Err(PerlError::runtime(
14279                format!("Unknown method for filehandle: {}", method),
14280                line,
14281            )),
14282        }
14283    }
14284
14285    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> PerlResult<PerlValue> {
14286        match handle_name {
14287            "STDOUT" => {
14288                let _ = IoWrite::flush(&mut io::stdout());
14289            }
14290            "STDERR" => {
14291                let _ = IoWrite::flush(&mut io::stderr());
14292            }
14293            name => {
14294                if let Some(writer) = self.output_handles.get_mut(name) {
14295                    let _ = IoWrite::flush(&mut *writer);
14296                } else {
14297                    return Err(PerlError::runtime(
14298                        format!("flush on unopened filehandle {}", name),
14299                        line,
14300                    ));
14301                }
14302            }
14303        }
14304        Ok(PerlValue::integer(1))
14305    }
14306
14307    fn io_handle_print(
14308        &mut self,
14309        handle_name: &str,
14310        args: &[PerlValue],
14311        newline: bool,
14312        line: usize,
14313    ) -> PerlResult<PerlValue> {
14314        if newline && (self.feature_bits & FEAT_SAY) == 0 {
14315            return Err(PerlError::runtime(
14316                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
14317                line,
14318            ));
14319        }
14320        let mut output = String::new();
14321        if args.is_empty() {
14322            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
14323            output.push_str(&self.scope.get_scalar("_").to_string());
14324        } else {
14325            for (i, val) in args.iter().enumerate() {
14326                if i > 0 && !self.ofs.is_empty() {
14327                    output.push_str(&self.ofs);
14328                }
14329                output.push_str(&val.to_string());
14330            }
14331        }
14332        if newline {
14333            output.push('\n');
14334        }
14335        output.push_str(&self.ors);
14336
14337        self.write_formatted_print(handle_name, &output, line)?;
14338        Ok(PerlValue::integer(1))
14339    }
14340
14341    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
14342    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
14343    pub(crate) fn write_formatted_print(
14344        &mut self,
14345        handle_name: &str,
14346        output: &str,
14347        line: usize,
14348    ) -> PerlResult<()> {
14349        match handle_name {
14350            "STDOUT" => {
14351                if !self.suppress_stdout {
14352                    print!("{}", output);
14353                    if self.output_autoflush {
14354                        let _ = io::stdout().flush();
14355                    }
14356                }
14357            }
14358            "STDERR" => {
14359                eprint!("{}", output);
14360                let _ = io::stderr().flush();
14361            }
14362            name => {
14363                if let Some(writer) = self.output_handles.get_mut(name) {
14364                    let _ = writer.write_all(output.as_bytes());
14365                    if self.output_autoflush {
14366                        let _ = writer.flush();
14367                    }
14368                } else {
14369                    return Err(PerlError::runtime(
14370                        format!("print on unopened filehandle {}", name),
14371                        line,
14372                    ));
14373                }
14374            }
14375        }
14376        Ok(())
14377    }
14378
14379    fn io_handle_printf(
14380        &mut self,
14381        handle_name: &str,
14382        args: &[PerlValue],
14383        line: usize,
14384    ) -> PerlResult<PerlValue> {
14385        let (fmt, rest): (String, &[PerlValue]) = if args.is_empty() {
14386            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
14387                Ok(s) => s,
14388                Err(FlowOrError::Error(e)) => return Err(e),
14389                Err(FlowOrError::Flow(_)) => {
14390                    return Err(PerlError::runtime(
14391                        "printf: unexpected control flow in sprintf",
14392                        line,
14393                    ));
14394                }
14395            };
14396            (s, &[])
14397        } else {
14398            (args[0].to_string(), &args[1..])
14399        };
14400        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
14401            Ok(s) => s,
14402            Err(FlowOrError::Error(e)) => return Err(e),
14403            Err(FlowOrError::Flow(_)) => {
14404                return Err(PerlError::runtime(
14405                    "printf: unexpected control flow in sprintf",
14406                    line,
14407                ));
14408            }
14409        };
14410        match handle_name {
14411            "STDOUT" => {
14412                if !self.suppress_stdout {
14413                    print!("{}", output);
14414                    if self.output_autoflush {
14415                        let _ = IoWrite::flush(&mut io::stdout());
14416                    }
14417                }
14418            }
14419            "STDERR" => {
14420                eprint!("{}", output);
14421                let _ = IoWrite::flush(&mut io::stderr());
14422            }
14423            name => {
14424                if let Some(writer) = self.output_handles.get_mut(name) {
14425                    let _ = writer.write_all(output.as_bytes());
14426                    if self.output_autoflush {
14427                        let _ = writer.flush();
14428                    }
14429                } else {
14430                    return Err(PerlError::runtime(
14431                        format!("printf on unopened filehandle {}", name),
14432                        line,
14433                    ));
14434                }
14435            }
14436        }
14437        Ok(PerlValue::integer(1))
14438    }
14439
14440    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
14441    pub(crate) fn try_native_method(
14442        &mut self,
14443        receiver: &PerlValue,
14444        method: &str,
14445        args: &[PerlValue],
14446        line: usize,
14447    ) -> Option<PerlResult<PerlValue>> {
14448        if let Some(name) = receiver.as_io_handle_name() {
14449            return Some(self.io_handle_method(&name, method, args, line));
14450        }
14451        if let Some(ref s) = receiver.as_str() {
14452            if self.is_bound_handle(s) {
14453                return Some(self.io_handle_method(s, method, args, line));
14454            }
14455        }
14456        if let Some(c) = receiver.as_sqlite_conn() {
14457            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
14458        }
14459        if let Some(s) = receiver.as_struct_inst() {
14460            // Field access: $p->x or $p->x(value)
14461            if let Some(idx) = s.def.field_index(method) {
14462                match args.len() {
14463                    0 => {
14464                        return Some(Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF)));
14465                    }
14466                    1 => {
14467                        let field = &s.def.fields[idx];
14468                        let new_val = args[0].clone();
14469                        if let Err(msg) = field.ty.check_value(&new_val) {
14470                            return Some(Err(PerlError::type_error(
14471                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
14472                                line,
14473                            )));
14474                        }
14475                        s.set_field(idx, new_val.clone());
14476                        return Some(Ok(new_val));
14477                    }
14478                    _ => {
14479                        return Some(Err(PerlError::runtime(
14480                            format!(
14481                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
14482                                method,
14483                                args.len()
14484                            ),
14485                            line,
14486                        )));
14487                    }
14488                }
14489            }
14490            // Built-in struct methods
14491            match method {
14492                "with" => {
14493                    // Functional update: $p->with(x => 5) returns new instance with changed field
14494                    let mut new_values = s.get_values();
14495                    let mut i = 0;
14496                    while i + 1 < args.len() {
14497                        let k = args[i].to_string();
14498                        let v = args[i + 1].clone();
14499                        if let Some(idx) = s.def.field_index(&k) {
14500                            let field = &s.def.fields[idx];
14501                            if let Err(msg) = field.ty.check_value(&v) {
14502                                return Some(Err(PerlError::type_error(
14503                                    format!(
14504                                        "struct {} field `{}`: {}",
14505                                        s.def.name, field.name, msg
14506                                    ),
14507                                    line,
14508                                )));
14509                            }
14510                            new_values[idx] = v;
14511                        } else {
14512                            return Some(Err(PerlError::runtime(
14513                                format!("struct {}: unknown field `{}`", s.def.name, k),
14514                                line,
14515                            )));
14516                        }
14517                        i += 2;
14518                    }
14519                    return Some(Ok(PerlValue::struct_inst(Arc::new(
14520                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
14521                    ))));
14522                }
14523                "to_hash" => {
14524                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
14525                    if !args.is_empty() {
14526                        return Some(Err(PerlError::runtime(
14527                            "struct to_hash takes no arguments",
14528                            line,
14529                        )));
14530                    }
14531                    let mut map = IndexMap::new();
14532                    let values = s.get_values();
14533                    for (i, field) in s.def.fields.iter().enumerate() {
14534                        map.insert(field.name.clone(), values[i].clone());
14535                    }
14536                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
14537                }
14538                "fields" => {
14539                    // Field list: $p->fields returns field names
14540                    if !args.is_empty() {
14541                        return Some(Err(PerlError::runtime(
14542                            "struct fields takes no arguments",
14543                            line,
14544                        )));
14545                    }
14546                    let names: Vec<PerlValue> = s
14547                        .def
14548                        .fields
14549                        .iter()
14550                        .map(|f| PerlValue::string(f.name.clone()))
14551                        .collect();
14552                    return Some(Ok(PerlValue::array(names)));
14553                }
14554                "clone" => {
14555                    // Clone: $p->clone deep copies
14556                    if !args.is_empty() {
14557                        return Some(Err(PerlError::runtime(
14558                            "struct clone takes no arguments",
14559                            line,
14560                        )));
14561                    }
14562                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
14563                    return Some(Ok(PerlValue::struct_inst(Arc::new(
14564                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
14565                    ))));
14566                }
14567                _ => {}
14568            }
14569            // User-defined struct method
14570            if let Some(m) = s.def.method(method) {
14571                let body = m.body.clone();
14572                let params = m.params.clone();
14573                // Build args: $self is the receiver, then the passed args
14574                let mut call_args = vec![receiver.clone()];
14575                call_args.extend(args.iter().cloned());
14576                return Some(
14577                    match self.call_struct_method(&body, &params, call_args, line) {
14578                        Ok(v) => Ok(v),
14579                        Err(FlowOrError::Error(e)) => Err(e),
14580                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14581                        Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
14582                            "unexpected control flow in struct method",
14583                            line,
14584                        )),
14585                    },
14586                );
14587            }
14588            return None;
14589        }
14590        // Class instance method dispatch
14591        if let Some(c) = receiver.as_class_inst() {
14592            // Collect all fields from inheritance chain (with visibility)
14593            let all_fields_full = self.collect_class_fields_full(&c.def);
14594            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
14595                .iter()
14596                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
14597                .collect();
14598
14599            // Field access: $obj->name or $obj->name(value)
14600            if let Some(idx) = all_fields_full
14601                .iter()
14602                .position(|(name, _, _, _, _)| name == method)
14603            {
14604                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
14605
14606                // Enforce field visibility
14607                match vis {
14608                    crate::ast::Visibility::Private => {
14609                        // Only accessible from within the owning class's methods
14610                        let caller_class = self
14611                            .scope
14612                            .get_scalar("self")
14613                            .as_class_inst()
14614                            .map(|ci| ci.def.name.clone());
14615                        if caller_class.as_deref() != Some(owner_class.as_str()) {
14616                            return Some(Err(PerlError::runtime(
14617                                format!("field `{}` of class {} is private", method, owner_class),
14618                                line,
14619                            )));
14620                        }
14621                    }
14622                    crate::ast::Visibility::Protected => {
14623                        // Accessible from owning class or subclasses
14624                        let caller_class = self
14625                            .scope
14626                            .get_scalar("self")
14627                            .as_class_inst()
14628                            .map(|ci| ci.def.name.clone());
14629                        let allowed = caller_class.as_deref().is_some_and(|caller| {
14630                            caller == owner_class || self.class_inherits_from(caller, owner_class)
14631                        });
14632                        if !allowed {
14633                            return Some(Err(PerlError::runtime(
14634                                format!("field `{}` of class {} is protected", method, owner_class),
14635                                line,
14636                            )));
14637                        }
14638                    }
14639                    crate::ast::Visibility::Public => {}
14640                }
14641
14642                match args.len() {
14643                    0 => {
14644                        return Some(Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF)));
14645                    }
14646                    1 => {
14647                        let new_val = args[0].clone();
14648                        if let Err(msg) = ty.check_value(&new_val) {
14649                            return Some(Err(PerlError::type_error(
14650                                format!("class {} field `{}`: {}", c.def.name, method, msg),
14651                                line,
14652                            )));
14653                        }
14654                        c.set_field(idx, new_val.clone());
14655                        return Some(Ok(new_val));
14656                    }
14657                    _ => {
14658                        return Some(Err(PerlError::runtime(
14659                            format!(
14660                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
14661                                method,
14662                                args.len()
14663                            ),
14664                            line,
14665                        )));
14666                    }
14667                }
14668            }
14669            // Built-in class methods (use all_fields for inheritance)
14670            match method {
14671                "with" => {
14672                    let mut new_values = c.get_values();
14673                    let mut i = 0;
14674                    while i + 1 < args.len() {
14675                        let k = args[i].to_string();
14676                        let v = args[i + 1].clone();
14677                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
14678                            let (_, _, ref ty) = all_fields[idx];
14679                            if let Err(msg) = ty.check_value(&v) {
14680                                return Some(Err(PerlError::type_error(
14681                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
14682                                    line,
14683                                )));
14684                            }
14685                            new_values[idx] = v;
14686                        } else {
14687                            return Some(Err(PerlError::runtime(
14688                                format!("class {}: unknown field `{}`", c.def.name, k),
14689                                line,
14690                            )));
14691                        }
14692                        i += 2;
14693                    }
14694                    return Some(Ok(PerlValue::class_inst(Arc::new(
14695                        crate::value::ClassInstance::new(Arc::clone(&c.def), new_values),
14696                    ))));
14697                }
14698                "to_hash" => {
14699                    if !args.is_empty() {
14700                        return Some(Err(PerlError::runtime(
14701                            "class to_hash takes no arguments",
14702                            line,
14703                        )));
14704                    }
14705                    let mut map = IndexMap::new();
14706                    let values = c.get_values();
14707                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
14708                        if let Some(v) = values.get(i) {
14709                            map.insert(name.clone(), v.clone());
14710                        }
14711                    }
14712                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
14713                }
14714                "fields" => {
14715                    if !args.is_empty() {
14716                        return Some(Err(PerlError::runtime(
14717                            "class fields takes no arguments",
14718                            line,
14719                        )));
14720                    }
14721                    let names: Vec<PerlValue> = all_fields
14722                        .iter()
14723                        .map(|(name, _, _)| PerlValue::string(name.clone()))
14724                        .collect();
14725                    return Some(Ok(PerlValue::array(names)));
14726                }
14727                "clone" => {
14728                    if !args.is_empty() {
14729                        return Some(Err(PerlError::runtime(
14730                            "class clone takes no arguments",
14731                            line,
14732                        )));
14733                    }
14734                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
14735                    return Some(Ok(PerlValue::class_inst(Arc::new(
14736                        crate::value::ClassInstance::new(Arc::clone(&c.def), new_values),
14737                    ))));
14738                }
14739                "isa" => {
14740                    if args.len() != 1 {
14741                        return Some(Err(PerlError::runtime("isa requires one argument", line)));
14742                    }
14743                    let class_name = args[0].to_string();
14744                    let is_a = c.def.name == class_name || c.def.extends.contains(&class_name);
14745                    return Some(Ok(if is_a {
14746                        PerlValue::integer(1)
14747                    } else {
14748                        PerlValue::string(String::new())
14749                    }));
14750                }
14751                "does" => {
14752                    if args.len() != 1 {
14753                        return Some(Err(PerlError::runtime("does requires one argument", line)));
14754                    }
14755                    let trait_name = args[0].to_string();
14756                    let implements = c.def.implements.contains(&trait_name);
14757                    return Some(Ok(if implements {
14758                        PerlValue::integer(1)
14759                    } else {
14760                        PerlValue::string(String::new())
14761                    }));
14762                }
14763                "methods" => {
14764                    if !args.is_empty() {
14765                        return Some(Err(PerlError::runtime("methods takes no arguments", line)));
14766                    }
14767                    let mut names = Vec::new();
14768                    self.collect_class_method_names(&c.def, &mut names);
14769                    let values: Vec<PerlValue> = names.into_iter().map(PerlValue::string).collect();
14770                    return Some(Ok(PerlValue::array(values)));
14771                }
14772                "superclass" => {
14773                    if !args.is_empty() {
14774                        return Some(Err(PerlError::runtime(
14775                            "superclass takes no arguments",
14776                            line,
14777                        )));
14778                    }
14779                    let parents: Vec<PerlValue> = c
14780                        .def
14781                        .extends
14782                        .iter()
14783                        .map(|s| PerlValue::string(s.clone()))
14784                        .collect();
14785                    return Some(Ok(PerlValue::array(parents)));
14786                }
14787                "destroy" => {
14788                    // Explicit destructor call — runs DESTROY chain child-first
14789                    let destroy_chain = self.collect_destroy_chain(&c.def);
14790                    for (body, params) in &destroy_chain {
14791                        let call_args = vec![receiver.clone()];
14792                        match self.call_class_method(body, params, call_args, line) {
14793                            Ok(_) => {}
14794                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
14795                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
14796                            Err(_) => {}
14797                        }
14798                    }
14799                    return Some(Ok(PerlValue::UNDEF));
14800                }
14801                _ => {}
14802            }
14803            // User-defined class method (search inheritance chain)
14804            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
14805                // Check visibility
14806                match m.visibility {
14807                    crate::ast::Visibility::Private => {
14808                        let caller_class = self
14809                            .scope
14810                            .get_scalar("self")
14811                            .as_class_inst()
14812                            .map(|ci| ci.def.name.clone());
14813                        if caller_class.as_deref() != Some(owner_class.as_str()) {
14814                            return Some(Err(PerlError::runtime(
14815                                format!("method `{}` of class {} is private", method, owner_class),
14816                                line,
14817                            )));
14818                        }
14819                    }
14820                    crate::ast::Visibility::Protected => {
14821                        let caller_class = self
14822                            .scope
14823                            .get_scalar("self")
14824                            .as_class_inst()
14825                            .map(|ci| ci.def.name.clone());
14826                        let allowed = caller_class.as_deref().is_some_and(|caller| {
14827                            caller == owner_class.as_str()
14828                                || self.class_inherits_from(caller, owner_class)
14829                        });
14830                        if !allowed {
14831                            return Some(Err(PerlError::runtime(
14832                                format!(
14833                                    "method `{}` of class {} is protected",
14834                                    method, owner_class
14835                                ),
14836                                line,
14837                            )));
14838                        }
14839                    }
14840                    crate::ast::Visibility::Public => {}
14841                }
14842                if let Some(ref body) = m.body {
14843                    let params = m.params.clone();
14844                    let mut call_args = vec![receiver.clone()];
14845                    call_args.extend(args.iter().cloned());
14846                    return Some(
14847                        match self.call_class_method(body, &params, call_args, line) {
14848                            Ok(v) => Ok(v),
14849                            Err(FlowOrError::Error(e)) => Err(e),
14850                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14851                            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
14852                                "unexpected control flow in class method",
14853                                line,
14854                            )),
14855                        },
14856                    );
14857                }
14858            }
14859            return None;
14860        }
14861        if let Some(d) = receiver.as_dataframe() {
14862            return Some(self.dataframe_method(d, method, args, line));
14863        }
14864        if let Some(s) = crate::value::set_payload(receiver) {
14865            return Some(self.set_method(s, method, args, line));
14866        }
14867        if let Some(d) = receiver.as_deque() {
14868            return Some(self.deque_method(d, method, args, line));
14869        }
14870        if let Some(h) = receiver.as_heap_pq() {
14871            return Some(self.heap_method(h, method, args, line));
14872        }
14873        if let Some(p) = receiver.as_pipeline() {
14874            return Some(self.pipeline_method(p, method, args, line));
14875        }
14876        if let Some(c) = receiver.as_capture() {
14877            return Some(self.capture_method(c, method, args, line));
14878        }
14879        if let Some(p) = receiver.as_ppool() {
14880            return Some(self.ppool_method(p, method, args, line));
14881        }
14882        if let Some(b) = receiver.as_barrier() {
14883            return Some(self.barrier_method(b, method, args, line));
14884        }
14885        if let Some(g) = receiver.as_generator() {
14886            if method == "next" {
14887                if !args.is_empty() {
14888                    return Some(Err(PerlError::runtime(
14889                        "generator->next takes no arguments",
14890                        line,
14891                    )));
14892                }
14893                return Some(self.generator_next(&g));
14894            }
14895            return None;
14896        }
14897        if let Some(arc) = receiver.as_atomic_arc() {
14898            let inner = arc.lock().clone();
14899            if let Some(d) = inner.as_deque() {
14900                return Some(self.deque_method(d, method, args, line));
14901            }
14902            if let Some(h) = inner.as_heap_pq() {
14903                return Some(self.heap_method(h, method, args, line));
14904            }
14905        }
14906        None
14907    }
14908
14909    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
14910    fn dataframe_method(
14911        &mut self,
14912        d: Arc<Mutex<PerlDataFrame>>,
14913        method: &str,
14914        args: &[PerlValue],
14915        line: usize,
14916    ) -> PerlResult<PerlValue> {
14917        match method {
14918            "nrow" | "nrows" => {
14919                if !args.is_empty() {
14920                    return Err(PerlError::runtime(
14921                        format!("dataframe {} takes no arguments", method),
14922                        line,
14923                    ));
14924                }
14925                Ok(PerlValue::integer(d.lock().nrows() as i64))
14926            }
14927            "ncol" | "ncols" => {
14928                if !args.is_empty() {
14929                    return Err(PerlError::runtime(
14930                        format!("dataframe {} takes no arguments", method),
14931                        line,
14932                    ));
14933                }
14934                Ok(PerlValue::integer(d.lock().ncols() as i64))
14935            }
14936            "filter" => {
14937                if args.len() != 1 {
14938                    return Err(PerlError::runtime(
14939                        "dataframe filter expects 1 argument (sub)",
14940                        line,
14941                    ));
14942                }
14943                let Some(sub) = args[0].as_code_ref() else {
14944                    return Err(PerlError::runtime(
14945                        "dataframe filter expects a code reference",
14946                        line,
14947                    ));
14948                };
14949                let df_guard = d.lock();
14950                let n = df_guard.nrows();
14951                let mut keep = vec![false; n];
14952                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
14953                    let row = df_guard.row_hashref(r);
14954                    self.scope_push_hook();
14955                    self.scope.set_topic(row);
14956                    if let Some(ref env) = sub.closure_env {
14957                        self.scope.restore_capture(env);
14958                    }
14959                    let pass = match self.exec_block_no_scope(&sub.body) {
14960                        Ok(v) => v.is_true(),
14961                        Err(_) => false,
14962                    };
14963                    self.scope_pop_hook();
14964                    *row_keep = pass;
14965                }
14966                let columns = df_guard.columns.clone();
14967                let cols: Vec<Vec<PerlValue>> = (0..df_guard.ncols())
14968                    .map(|i| {
14969                        let mut out = Vec::new();
14970                        for (r, pass_row) in keep.iter().enumerate().take(n) {
14971                            if *pass_row {
14972                                out.push(df_guard.cols[i][r].clone());
14973                            }
14974                        }
14975                        out
14976                    })
14977                    .collect();
14978                let group_by = df_guard.group_by.clone();
14979                drop(df_guard);
14980                let new_df = PerlDataFrame {
14981                    columns,
14982                    cols,
14983                    group_by,
14984                };
14985                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
14986            }
14987            "group_by" => {
14988                if args.len() != 1 {
14989                    return Err(PerlError::runtime(
14990                        "dataframe group_by expects 1 column name",
14991                        line,
14992                    ));
14993                }
14994                let key = args[0].to_string();
14995                let inner = d.lock();
14996                if inner.col_index(&key).is_none() {
14997                    return Err(PerlError::runtime(
14998                        format!("dataframe group_by: unknown column \"{}\"", key),
14999                        line,
15000                    ));
15001                }
15002                let new_df = PerlDataFrame {
15003                    columns: inner.columns.clone(),
15004                    cols: inner.cols.clone(),
15005                    group_by: Some(key),
15006                };
15007                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
15008            }
15009            "sum" => {
15010                if args.len() != 1 {
15011                    return Err(PerlError::runtime(
15012                        "dataframe sum expects 1 column name",
15013                        line,
15014                    ));
15015                }
15016                let col_name = args[0].to_string();
15017                let inner = d.lock();
15018                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
15019                    PerlError::runtime(
15020                        format!("dataframe sum: unknown column \"{}\"", col_name),
15021                        line,
15022                    )
15023                })?;
15024                match &inner.group_by {
15025                    Some(gcol) => {
15026                        let gi = inner.col_index(gcol).ok_or_else(|| {
15027                            PerlError::runtime(
15028                                format!("dataframe sum: unknown group column \"{}\"", gcol),
15029                                line,
15030                            )
15031                        })?;
15032                        let mut acc: IndexMap<String, f64> = IndexMap::new();
15033                        for r in 0..inner.nrows() {
15034                            let k = inner.cols[gi][r].to_string();
15035                            let v = inner.cols[val_idx][r].to_number();
15036                            *acc.entry(k).or_insert(0.0) += v;
15037                        }
15038                        let keys: Vec<String> = acc.keys().cloned().collect();
15039                        let sums: Vec<f64> = acc.values().copied().collect();
15040                        let cols = vec![
15041                            keys.into_iter().map(PerlValue::string).collect(),
15042                            sums.into_iter().map(PerlValue::float).collect(),
15043                        ];
15044                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
15045                        let out = PerlDataFrame {
15046                            columns,
15047                            cols,
15048                            group_by: None,
15049                        };
15050                        Ok(PerlValue::dataframe(Arc::new(Mutex::new(out))))
15051                    }
15052                    None => {
15053                        let total: f64 = (0..inner.nrows())
15054                            .map(|r| inner.cols[val_idx][r].to_number())
15055                            .sum();
15056                        Ok(PerlValue::float(total))
15057                    }
15058                }
15059            }
15060            _ => Err(PerlError::runtime(
15061                format!("Unknown method for dataframe: {}", method),
15062                line,
15063            )),
15064        }
15065    }
15066
15067    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
15068    fn set_method(
15069        &self,
15070        s: Arc<crate::value::PerlSet>,
15071        method: &str,
15072        args: &[PerlValue],
15073        line: usize,
15074    ) -> PerlResult<PerlValue> {
15075        match method {
15076            "has" | "contains" | "member" => {
15077                if args.len() != 1 {
15078                    return Err(PerlError::runtime(
15079                        "set->has expects one argument (element)",
15080                        line,
15081                    ));
15082                }
15083                let k = crate::value::set_member_key(&args[0]);
15084                Ok(PerlValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
15085            }
15086            "size" | "len" | "count" => {
15087                if !args.is_empty() {
15088                    return Err(PerlError::runtime("set->size takes no arguments", line));
15089                }
15090                Ok(PerlValue::integer(s.len() as i64))
15091            }
15092            "values" | "list" | "elements" => {
15093                if !args.is_empty() {
15094                    return Err(PerlError::runtime("set->values takes no arguments", line));
15095                }
15096                Ok(PerlValue::array(s.values().cloned().collect()))
15097            }
15098            _ => Err(PerlError::runtime(
15099                format!("Unknown method for set: {}", method),
15100                line,
15101            )),
15102        }
15103    }
15104
15105    fn deque_method(
15106        &mut self,
15107        d: Arc<Mutex<VecDeque<PerlValue>>>,
15108        method: &str,
15109        args: &[PerlValue],
15110        line: usize,
15111    ) -> PerlResult<PerlValue> {
15112        match method {
15113            "push_back" => {
15114                if args.len() != 1 {
15115                    return Err(PerlError::runtime("push_back expects 1 argument", line));
15116                }
15117                d.lock().push_back(args[0].clone());
15118                Ok(PerlValue::integer(d.lock().len() as i64))
15119            }
15120            "push_front" => {
15121                if args.len() != 1 {
15122                    return Err(PerlError::runtime("push_front expects 1 argument", line));
15123                }
15124                d.lock().push_front(args[0].clone());
15125                Ok(PerlValue::integer(d.lock().len() as i64))
15126            }
15127            "pop_back" => Ok(d.lock().pop_back().unwrap_or(PerlValue::UNDEF)),
15128            "pop_front" => Ok(d.lock().pop_front().unwrap_or(PerlValue::UNDEF)),
15129            "size" | "len" => Ok(PerlValue::integer(d.lock().len() as i64)),
15130            _ => Err(PerlError::runtime(
15131                format!("Unknown method for deque: {}", method),
15132                line,
15133            )),
15134        }
15135    }
15136
15137    fn heap_method(
15138        &mut self,
15139        h: Arc<Mutex<PerlHeap>>,
15140        method: &str,
15141        args: &[PerlValue],
15142        line: usize,
15143    ) -> PerlResult<PerlValue> {
15144        match method {
15145            "push" => {
15146                if args.len() != 1 {
15147                    return Err(PerlError::runtime("heap push expects 1 argument", line));
15148                }
15149                let mut g = h.lock();
15150                let n = g.items.len();
15151                g.items.push(args[0].clone());
15152                let cmp = g.cmp.clone();
15153                drop(g);
15154                let mut g = h.lock();
15155                self.heap_sift_up(&mut g.items, &cmp, n);
15156                Ok(PerlValue::integer(g.items.len() as i64))
15157            }
15158            "pop" => {
15159                let mut g = h.lock();
15160                if g.items.is_empty() {
15161                    return Ok(PerlValue::UNDEF);
15162                }
15163                let cmp = g.cmp.clone();
15164                let n = g.items.len();
15165                g.items.swap(0, n - 1);
15166                let v = g.items.pop().unwrap();
15167                if !g.items.is_empty() {
15168                    self.heap_sift_down(&mut g.items, &cmp, 0);
15169                }
15170                Ok(v)
15171            }
15172            "peek" => Ok(h.lock().items.first().cloned().unwrap_or(PerlValue::UNDEF)),
15173            _ => Err(PerlError::runtime(
15174                format!("Unknown method for heap: {}", method),
15175                line,
15176            )),
15177        }
15178    }
15179
15180    fn ppool_method(
15181        &mut self,
15182        pool: PerlPpool,
15183        method: &str,
15184        args: &[PerlValue],
15185        line: usize,
15186    ) -> PerlResult<PerlValue> {
15187        match method {
15188            "submit" => pool.submit(self, args, line),
15189            "collect" => {
15190                if !args.is_empty() {
15191                    return Err(PerlError::runtime("collect() takes no arguments", line));
15192                }
15193                pool.collect(line)
15194            }
15195            _ => Err(PerlError::runtime(
15196                format!("Unknown method for ppool: {}", method),
15197                line,
15198            )),
15199        }
15200    }
15201
15202    fn barrier_method(
15203        &self,
15204        barrier: PerlBarrier,
15205        method: &str,
15206        args: &[PerlValue],
15207        line: usize,
15208    ) -> PerlResult<PerlValue> {
15209        match method {
15210            "wait" => {
15211                if !args.is_empty() {
15212                    return Err(PerlError::runtime("wait() takes no arguments", line));
15213                }
15214                let _ = barrier.0.wait();
15215                Ok(PerlValue::integer(1))
15216            }
15217            _ => Err(PerlError::runtime(
15218                format!("Unknown method for barrier: {}", method),
15219                line,
15220            )),
15221        }
15222    }
15223
15224    fn capture_method(
15225        &self,
15226        c: Arc<CaptureResult>,
15227        method: &str,
15228        args: &[PerlValue],
15229        line: usize,
15230    ) -> PerlResult<PerlValue> {
15231        if !args.is_empty() {
15232            return Err(PerlError::runtime(
15233                format!("capture: {} takes no arguments", method),
15234                line,
15235            ));
15236        }
15237        match method {
15238            "stdout" => Ok(PerlValue::string(c.stdout.clone())),
15239            "stderr" => Ok(PerlValue::string(c.stderr.clone())),
15240            "exitcode" => Ok(PerlValue::integer(c.exitcode)),
15241            "failed" => Ok(PerlValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
15242            _ => Err(PerlError::runtime(
15243                format!("Unknown method for capture: {}", method),
15244                line,
15245            )),
15246        }
15247    }
15248
15249    pub(crate) fn builtin_par_pipeline_stream(
15250        &mut self,
15251        args: &[PerlValue],
15252        _line: usize,
15253    ) -> PerlResult<PerlValue> {
15254        let mut items = Vec::new();
15255        for v in args {
15256            if let Some(a) = v.as_array_vec() {
15257                items.extend(a);
15258            } else {
15259                items.push(v.clone());
15260            }
15261        }
15262        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15263            source: items,
15264            ops: Vec::new(),
15265            has_scalar_terminal: false,
15266            par_stream: true,
15267            streaming: false,
15268            streaming_workers: 0,
15269            streaming_buffer: 256,
15270        }))))
15271    }
15272
15273    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
15274    /// that wires ops through bounded channels on `collect()`.
15275    pub(crate) fn builtin_par_pipeline_stream_new(
15276        &mut self,
15277        args: &[PerlValue],
15278        _line: usize,
15279    ) -> PerlResult<PerlValue> {
15280        let mut items = Vec::new();
15281        let mut workers: usize = 0;
15282        let mut buffer: usize = 256;
15283        // Separate list items from keyword args (workers => N, buffer => N).
15284        let mut i = 0;
15285        while i < args.len() {
15286            let s = args[i].to_string();
15287            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
15288                let val = args[i + 1].to_int().max(1) as usize;
15289                if s == "workers" {
15290                    workers = val;
15291                } else {
15292                    buffer = val;
15293                }
15294                i += 2;
15295            } else if let Some(a) = args[i].as_array_vec() {
15296                items.extend(a);
15297                i += 1;
15298            } else {
15299                items.push(args[i].clone());
15300                i += 1;
15301            }
15302        }
15303        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15304            source: items,
15305            ops: Vec::new(),
15306            has_scalar_terminal: false,
15307            par_stream: false,
15308            streaming: true,
15309            streaming_workers: workers,
15310            streaming_buffer: buffer,
15311        }))))
15312    }
15313
15314    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
15315    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<PerlSub> {
15316        let line = 1usize;
15317        let body = vec![Statement {
15318            label: None,
15319            kind: StmtKind::Expression(Expr {
15320                kind: ExprKind::BinOp {
15321                    left: Box::new(Expr {
15322                        kind: ExprKind::ScalarVar("_".into()),
15323                        line,
15324                    }),
15325                    op: BinOp::Mul,
15326                    right: Box::new(Expr {
15327                        kind: ExprKind::Integer(k),
15328                        line,
15329                    }),
15330                },
15331                line,
15332            }),
15333            line,
15334        }];
15335        Arc::new(PerlSub {
15336            name: "__pipeline_int_mul__".into(),
15337            params: vec![],
15338            body,
15339            closure_env: None,
15340            prototype: None,
15341            fib_like: None,
15342        })
15343    }
15344
15345    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<PerlSub> {
15346        let captured = self.scope.capture();
15347        Arc::new(PerlSub {
15348            name: "__ANON__".into(),
15349            params: vec![],
15350            body: block.clone(),
15351            closure_env: Some(captured),
15352            prototype: None,
15353            fib_like: None,
15354        })
15355    }
15356
15357    pub(crate) fn builtin_collect_execute(
15358        &mut self,
15359        args: &[PerlValue],
15360        line: usize,
15361    ) -> PerlResult<PerlValue> {
15362        if args.is_empty() {
15363            return Err(PerlError::runtime(
15364                "collect() expects at least one argument",
15365                line,
15366            ));
15367        }
15368        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
15369        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
15370        if args.len() == 1 {
15371            if let Some(p) = args[0].as_pipeline() {
15372                return self.pipeline_collect(&p, line);
15373            }
15374            return Ok(PerlValue::array(args[0].to_list()));
15375        }
15376        Ok(PerlValue::array(args.to_vec()))
15377    }
15378
15379    pub(crate) fn pipeline_push(
15380        &self,
15381        p: &Arc<Mutex<PipelineInner>>,
15382        op: PipelineOp,
15383        line: usize,
15384    ) -> PerlResult<()> {
15385        let mut g = p.lock();
15386        if g.has_scalar_terminal {
15387            return Err(PerlError::runtime(
15388                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
15389                line,
15390            ));
15391        }
15392        if matches!(
15393            &op,
15394            PipelineOp::PReduce { .. }
15395                | PipelineOp::PReduceInit { .. }
15396                | PipelineOp::PMapReduce { .. }
15397        ) {
15398            g.has_scalar_terminal = true;
15399        }
15400        g.ops.push(op);
15401        Ok(())
15402    }
15403
15404    fn pipeline_parse_sub_progress(
15405        args: &[PerlValue],
15406        line: usize,
15407        name: &str,
15408    ) -> PerlResult<(Arc<PerlSub>, bool)> {
15409        if args.is_empty() {
15410            return Err(PerlError::runtime(
15411                format!("pipeline {}: expects at least 1 argument (code ref)", name),
15412                line,
15413            ));
15414        }
15415        let Some(sub) = args[0].as_code_ref() else {
15416            return Err(PerlError::runtime(
15417                format!("pipeline {}: first argument must be a code reference", name),
15418                line,
15419            ));
15420        };
15421        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
15422        if args.len() > 2 {
15423            return Err(PerlError::runtime(
15424                format!(
15425                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
15426                    name
15427                ),
15428                line,
15429            ));
15430        }
15431        Ok((sub, progress))
15432    }
15433
15434    pub(crate) fn pipeline_method(
15435        &mut self,
15436        p: Arc<Mutex<PipelineInner>>,
15437        method: &str,
15438        args: &[PerlValue],
15439        line: usize,
15440    ) -> PerlResult<PerlValue> {
15441        match method {
15442            "filter" | "f" | "grep" => {
15443                if args.len() != 1 {
15444                    return Err(PerlError::runtime(
15445                        "pipeline filter/grep expects 1 argument (sub)",
15446                        line,
15447                    ));
15448                }
15449                let Some(sub) = args[0].as_code_ref() else {
15450                    return Err(PerlError::runtime(
15451                        "pipeline filter/grep expects a code reference",
15452                        line,
15453                    ));
15454                };
15455                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
15456                Ok(PerlValue::pipeline(Arc::clone(&p)))
15457            }
15458            "map" => {
15459                if args.len() != 1 {
15460                    return Err(PerlError::runtime(
15461                        "pipeline map expects 1 argument (sub)",
15462                        line,
15463                    ));
15464                }
15465                let Some(sub) = args[0].as_code_ref() else {
15466                    return Err(PerlError::runtime(
15467                        "pipeline map expects a code reference",
15468                        line,
15469                    ));
15470                };
15471                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
15472                Ok(PerlValue::pipeline(Arc::clone(&p)))
15473            }
15474            "tap" | "peek" => {
15475                if args.len() != 1 {
15476                    return Err(PerlError::runtime(
15477                        "pipeline tap/peek expects 1 argument (sub)",
15478                        line,
15479                    ));
15480                }
15481                let Some(sub) = args[0].as_code_ref() else {
15482                    return Err(PerlError::runtime(
15483                        "pipeline tap/peek expects a code reference",
15484                        line,
15485                    ));
15486                };
15487                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
15488                Ok(PerlValue::pipeline(Arc::clone(&p)))
15489            }
15490            "take" => {
15491                if args.len() != 1 {
15492                    return Err(PerlError::runtime("pipeline take expects 1 argument", line));
15493                }
15494                let n = args[0].to_int();
15495                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
15496                Ok(PerlValue::pipeline(Arc::clone(&p)))
15497            }
15498            "pmap" => {
15499                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
15500                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
15501                Ok(PerlValue::pipeline(Arc::clone(&p)))
15502            }
15503            "pgrep" => {
15504                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
15505                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
15506                Ok(PerlValue::pipeline(Arc::clone(&p)))
15507            }
15508            "pfor" => {
15509                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
15510                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
15511                Ok(PerlValue::pipeline(Arc::clone(&p)))
15512            }
15513            "pmap_chunked" => {
15514                if args.len() < 2 {
15515                    return Err(PerlError::runtime(
15516                        "pipeline pmap_chunked expects chunk size and a code reference",
15517                        line,
15518                    ));
15519                }
15520                let chunk = args[0].to_int().max(1);
15521                let Some(sub) = args[1].as_code_ref() else {
15522                    return Err(PerlError::runtime(
15523                        "pipeline pmap_chunked: second argument must be a code reference",
15524                        line,
15525                    ));
15526                };
15527                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
15528                if args.len() > 3 {
15529                    return Err(PerlError::runtime(
15530                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
15531                        line,
15532                    ));
15533                }
15534                self.pipeline_push(
15535                    &p,
15536                    PipelineOp::PMapChunked {
15537                        chunk,
15538                        sub,
15539                        progress,
15540                    },
15541                    line,
15542                )?;
15543                Ok(PerlValue::pipeline(Arc::clone(&p)))
15544            }
15545            "psort" => {
15546                let (cmp, progress) = match args.len() {
15547                    0 => (None, false),
15548                    1 => {
15549                        if let Some(s) = args[0].as_code_ref() {
15550                            (Some(s), false)
15551                        } else {
15552                            (None, args[0].is_true())
15553                        }
15554                    }
15555                    2 => {
15556                        let Some(s) = args[0].as_code_ref() else {
15557                            return Err(PerlError::runtime(
15558                                "pipeline psort: with two arguments, the first must be a comparator sub",
15559                                line,
15560                            ));
15561                        };
15562                        (Some(s), args[1].is_true())
15563                    }
15564                    _ => {
15565                        return Err(PerlError::runtime(
15566                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
15567                            line,
15568                        ));
15569                    }
15570                };
15571                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
15572                Ok(PerlValue::pipeline(Arc::clone(&p)))
15573            }
15574            "pcache" => {
15575                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
15576                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
15577                Ok(PerlValue::pipeline(Arc::clone(&p)))
15578            }
15579            "preduce" => {
15580                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
15581                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
15582                Ok(PerlValue::pipeline(Arc::clone(&p)))
15583            }
15584            "preduce_init" => {
15585                if args.len() < 2 {
15586                    return Err(PerlError::runtime(
15587                        "pipeline preduce_init expects init value and a code reference",
15588                        line,
15589                    ));
15590                }
15591                let init = args[0].clone();
15592                let Some(sub) = args[1].as_code_ref() else {
15593                    return Err(PerlError::runtime(
15594                        "pipeline preduce_init: second argument must be a code reference",
15595                        line,
15596                    ));
15597                };
15598                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
15599                if args.len() > 3 {
15600                    return Err(PerlError::runtime(
15601                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
15602                        line,
15603                    ));
15604                }
15605                self.pipeline_push(
15606                    &p,
15607                    PipelineOp::PReduceInit {
15608                        init,
15609                        sub,
15610                        progress,
15611                    },
15612                    line,
15613                )?;
15614                Ok(PerlValue::pipeline(Arc::clone(&p)))
15615            }
15616            "pmap_reduce" => {
15617                if args.len() < 2 {
15618                    return Err(PerlError::runtime(
15619                        "pipeline pmap_reduce expects map sub and reduce sub",
15620                        line,
15621                    ));
15622                }
15623                let Some(map) = args[0].as_code_ref() else {
15624                    return Err(PerlError::runtime(
15625                        "pipeline pmap_reduce: first argument must be a code reference (map)",
15626                        line,
15627                    ));
15628                };
15629                let Some(reduce) = args[1].as_code_ref() else {
15630                    return Err(PerlError::runtime(
15631                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
15632                        line,
15633                    ));
15634                };
15635                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
15636                if args.len() > 3 {
15637                    return Err(PerlError::runtime(
15638                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
15639                        line,
15640                    ));
15641                }
15642                self.pipeline_push(
15643                    &p,
15644                    PipelineOp::PMapReduce {
15645                        map,
15646                        reduce,
15647                        progress,
15648                    },
15649                    line,
15650                )?;
15651                Ok(PerlValue::pipeline(Arc::clone(&p)))
15652            }
15653            "collect" => {
15654                if !args.is_empty() {
15655                    return Err(PerlError::runtime(
15656                        "pipeline collect takes no arguments",
15657                        line,
15658                    ));
15659                }
15660                self.pipeline_collect(&p, line)
15661            }
15662            _ => {
15663                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
15664                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
15665                if let Some(sub) = self.resolve_sub_by_name(method) {
15666                    if !args.is_empty() {
15667                        return Err(PerlError::runtime(
15668                            format!(
15669                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
15670                                method
15671                            ),
15672                            line,
15673                        ));
15674                    }
15675                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
15676                    Ok(PerlValue::pipeline(Arc::clone(&p)))
15677                } else {
15678                    Err(PerlError::runtime(
15679                        format!("Unknown method for pipeline: {}", method),
15680                        line,
15681                    ))
15682                }
15683            }
15684        }
15685    }
15686
15687    fn pipeline_parallel_map(
15688        &mut self,
15689        items: Vec<PerlValue>,
15690        sub: &Arc<PerlSub>,
15691        progress: bool,
15692    ) -> Vec<PerlValue> {
15693        let subs = self.subs.clone();
15694        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
15695        let pmap_progress = PmapProgress::new(progress, items.len());
15696        let results: Vec<PerlValue> = items
15697            .into_par_iter()
15698            .map(|item| {
15699                let mut local_interp = Interpreter::new();
15700                local_interp.subs = subs.clone();
15701                local_interp.scope.restore_capture(&scope_capture);
15702                local_interp
15703                    .scope
15704                    .restore_atomics(&atomic_arrays, &atomic_hashes);
15705                local_interp.enable_parallel_guard();
15706                local_interp.scope.set_topic(item);
15707                local_interp.scope_push_hook();
15708                let val = match local_interp.exec_block_no_scope(&sub.body) {
15709                    Ok(val) => val,
15710                    Err(_) => PerlValue::UNDEF,
15711                };
15712                local_interp.scope_pop_hook();
15713                pmap_progress.tick();
15714                val
15715            })
15716            .collect();
15717        pmap_progress.finish();
15718        results
15719    }
15720
15721    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
15722    fn pipeline_par_stream_filter(
15723        &mut self,
15724        items: Vec<PerlValue>,
15725        sub: &Arc<PerlSub>,
15726    ) -> Vec<PerlValue> {
15727        if items.is_empty() {
15728            return items;
15729        }
15730        let subs = self.subs.clone();
15731        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
15732        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
15733        let mut kept: Vec<(usize, PerlValue)> = indexed
15734            .into_par_iter()
15735            .filter_map(|(i, item)| {
15736                let mut local_interp = Interpreter::new();
15737                local_interp.subs = subs.clone();
15738                local_interp.scope.restore_capture(&scope_capture);
15739                local_interp
15740                    .scope
15741                    .restore_atomics(&atomic_arrays, &atomic_hashes);
15742                local_interp.enable_parallel_guard();
15743                local_interp.scope.set_topic(item.clone());
15744                local_interp.scope_push_hook();
15745                let keep = match local_interp.exec_block_no_scope(&sub.body) {
15746                    Ok(val) => val.is_true(),
15747                    Err(_) => false,
15748                };
15749                local_interp.scope_pop_hook();
15750                if keep {
15751                    Some((i, item))
15752                } else {
15753                    None
15754                }
15755            })
15756            .collect();
15757        kept.sort_by_key(|(i, _)| *i);
15758        kept.into_iter().map(|(_, x)| x).collect()
15759    }
15760
15761    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
15762    fn pipeline_par_stream_map(
15763        &mut self,
15764        items: Vec<PerlValue>,
15765        sub: &Arc<PerlSub>,
15766    ) -> Vec<PerlValue> {
15767        if items.is_empty() {
15768            return items;
15769        }
15770        let subs = self.subs.clone();
15771        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
15772        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
15773        let mut mapped: Vec<(usize, PerlValue)> = indexed
15774            .into_par_iter()
15775            .map(|(i, item)| {
15776                let mut local_interp = Interpreter::new();
15777                local_interp.subs = subs.clone();
15778                local_interp.scope.restore_capture(&scope_capture);
15779                local_interp
15780                    .scope
15781                    .restore_atomics(&atomic_arrays, &atomic_hashes);
15782                local_interp.enable_parallel_guard();
15783                local_interp.scope.set_topic(item);
15784                local_interp.scope_push_hook();
15785                let val = match local_interp.exec_block_no_scope(&sub.body) {
15786                    Ok(val) => val,
15787                    Err(_) => PerlValue::UNDEF,
15788                };
15789                local_interp.scope_pop_hook();
15790                (i, val)
15791            })
15792            .collect();
15793        mapped.sort_by_key(|(i, _)| *i);
15794        mapped.into_iter().map(|(_, x)| x).collect()
15795    }
15796
15797    fn pipeline_collect(
15798        &mut self,
15799        p: &Arc<Mutex<PipelineInner>>,
15800        line: usize,
15801    ) -> PerlResult<PerlValue> {
15802        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
15803            let g = p.lock();
15804            (
15805                g.source.clone(),
15806                g.ops.clone(),
15807                g.par_stream,
15808                g.streaming,
15809                g.streaming_workers,
15810                g.streaming_buffer,
15811            )
15812        };
15813        if streaming {
15814            return self.pipeline_collect_streaming(
15815                v,
15816                &ops,
15817                streaming_workers,
15818                streaming_buffer,
15819                line,
15820            );
15821        }
15822        for op in ops {
15823            match op {
15824                PipelineOp::Filter(sub) => {
15825                    if par_stream {
15826                        v = self.pipeline_par_stream_filter(v, &sub);
15827                    } else {
15828                        let mut out = Vec::new();
15829                        for item in v {
15830                            self.scope_push_hook();
15831                            self.scope.set_topic(item.clone());
15832                            if let Some(ref env) = sub.closure_env {
15833                                self.scope.restore_capture(env);
15834                            }
15835                            let keep = match self.exec_block_no_scope(&sub.body) {
15836                                Ok(val) => val.is_true(),
15837                                Err(_) => false,
15838                            };
15839                            self.scope_pop_hook();
15840                            if keep {
15841                                out.push(item);
15842                            }
15843                        }
15844                        v = out;
15845                    }
15846                }
15847                PipelineOp::Map(sub) => {
15848                    if par_stream {
15849                        v = self.pipeline_par_stream_map(v, &sub);
15850                    } else {
15851                        let mut out = Vec::new();
15852                        for item in v {
15853                            self.scope_push_hook();
15854                            self.scope.set_topic(item);
15855                            if let Some(ref env) = sub.closure_env {
15856                                self.scope.restore_capture(env);
15857                            }
15858                            let mapped = match self.exec_block_no_scope(&sub.body) {
15859                                Ok(val) => val,
15860                                Err(_) => PerlValue::UNDEF,
15861                            };
15862                            self.scope_pop_hook();
15863                            out.push(mapped);
15864                        }
15865                        v = out;
15866                    }
15867                }
15868                PipelineOp::Tap(sub) => {
15869                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
15870                        Ok(_) => {}
15871                        Err(FlowOrError::Error(e)) => return Err(e),
15872                        Err(FlowOrError::Flow(_)) => {
15873                            return Err(PerlError::runtime(
15874                                "tap: unsupported control flow in block",
15875                                line,
15876                            ));
15877                        }
15878                    }
15879                }
15880                PipelineOp::Take(n) => {
15881                    let n = n.max(0) as usize;
15882                    if v.len() > n {
15883                        v.truncate(n);
15884                    }
15885                }
15886                PipelineOp::PMap { sub, progress } => {
15887                    v = self.pipeline_parallel_map(v, &sub, progress);
15888                }
15889                PipelineOp::PGrep { sub, progress } => {
15890                    let subs = self.subs.clone();
15891                    let (scope_capture, atomic_arrays, atomic_hashes) =
15892                        self.scope.capture_with_atomics();
15893                    let pmap_progress = PmapProgress::new(progress, v.len());
15894                    v = v
15895                        .into_par_iter()
15896                        .filter_map(|item| {
15897                            let mut local_interp = Interpreter::new();
15898                            local_interp.subs = subs.clone();
15899                            local_interp.scope.restore_capture(&scope_capture);
15900                            local_interp
15901                                .scope
15902                                .restore_atomics(&atomic_arrays, &atomic_hashes);
15903                            local_interp.enable_parallel_guard();
15904                            local_interp.scope.set_topic(item.clone());
15905                            local_interp.scope_push_hook();
15906                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
15907                                Ok(val) => val.is_true(),
15908                                Err(_) => false,
15909                            };
15910                            local_interp.scope_pop_hook();
15911                            pmap_progress.tick();
15912                            if keep {
15913                                Some(item)
15914                            } else {
15915                                None
15916                            }
15917                        })
15918                        .collect();
15919                    pmap_progress.finish();
15920                }
15921                PipelineOp::PFor { sub, progress } => {
15922                    let subs = self.subs.clone();
15923                    let (scope_capture, atomic_arrays, atomic_hashes) =
15924                        self.scope.capture_with_atomics();
15925                    let pmap_progress = PmapProgress::new(progress, v.len());
15926                    let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
15927                    v.clone().into_par_iter().for_each(|item| {
15928                        if first_err.lock().is_some() {
15929                            return;
15930                        }
15931                        let mut local_interp = Interpreter::new();
15932                        local_interp.subs = subs.clone();
15933                        local_interp.scope.restore_capture(&scope_capture);
15934                        local_interp
15935                            .scope
15936                            .restore_atomics(&atomic_arrays, &atomic_hashes);
15937                        local_interp.enable_parallel_guard();
15938                        local_interp.scope.set_topic(item);
15939                        local_interp.scope_push_hook();
15940                        match local_interp.exec_block_no_scope(&sub.body) {
15941                            Ok(_) => {}
15942                            Err(e) => {
15943                                let stryke = match e {
15944                                    FlowOrError::Error(stryke) => stryke,
15945                                    FlowOrError::Flow(_) => PerlError::runtime(
15946                                        "return/last/next/redo not supported inside pipeline pfor block",
15947                                        line,
15948                                    ),
15949                                };
15950                                let mut g = first_err.lock();
15951                                if g.is_none() {
15952                                    *g = Some(stryke);
15953                                }
15954                            }
15955                        }
15956                        local_interp.scope_pop_hook();
15957                        pmap_progress.tick();
15958                    });
15959                    pmap_progress.finish();
15960                    let pfor_err = first_err.lock().take();
15961                    if let Some(e) = pfor_err {
15962                        return Err(e);
15963                    }
15964                }
15965                PipelineOp::PMapChunked {
15966                    chunk,
15967                    sub,
15968                    progress,
15969                } => {
15970                    let chunk_n = chunk.max(1) as usize;
15971                    let subs = self.subs.clone();
15972                    let (scope_capture, atomic_arrays, atomic_hashes) =
15973                        self.scope.capture_with_atomics();
15974                    let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = v
15975                        .chunks(chunk_n)
15976                        .enumerate()
15977                        .map(|(i, c)| (i, c.to_vec()))
15978                        .collect();
15979                    let n_chunks = indexed_chunks.len();
15980                    let pmap_progress = PmapProgress::new(progress, n_chunks);
15981                    let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
15982                        .into_par_iter()
15983                        .map(|(chunk_idx, chunk)| {
15984                            let mut local_interp = Interpreter::new();
15985                            local_interp.subs = subs.clone();
15986                            local_interp.scope.restore_capture(&scope_capture);
15987                            local_interp
15988                                .scope
15989                                .restore_atomics(&atomic_arrays, &atomic_hashes);
15990                            local_interp.enable_parallel_guard();
15991                            let mut out = Vec::with_capacity(chunk.len());
15992                            for item in chunk {
15993                                local_interp.scope.set_topic(item);
15994                                local_interp.scope_push_hook();
15995                                match local_interp.exec_block_no_scope(&sub.body) {
15996                                    Ok(val) => {
15997                                        local_interp.scope_pop_hook();
15998                                        out.push(val);
15999                                    }
16000                                    Err(_) => {
16001                                        local_interp.scope_pop_hook();
16002                                        out.push(PerlValue::UNDEF);
16003                                    }
16004                                }
16005                            }
16006                            pmap_progress.tick();
16007                            (chunk_idx, out)
16008                        })
16009                        .collect();
16010                    pmap_progress.finish();
16011                    chunk_results.sort_by_key(|(i, _)| *i);
16012                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
16013                }
16014                PipelineOp::PSort { cmp, progress } => {
16015                    let pmap_progress = PmapProgress::new(progress, 2);
16016                    pmap_progress.tick();
16017                    match cmp {
16018                        Some(cmp_block) => {
16019                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
16020                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
16021                            } else {
16022                                let subs = self.subs.clone();
16023                                let scope_capture = self.scope.capture();
16024                                v.par_sort_by(|a, b| {
16025                                    let mut local_interp = Interpreter::new();
16026                                    local_interp.subs = subs.clone();
16027                                    local_interp.scope.restore_capture(&scope_capture);
16028                                    local_interp.enable_parallel_guard();
16029                                    let _ = local_interp.scope.set_scalar("a", a.clone());
16030                                    let _ = local_interp.scope.set_scalar("b", b.clone());
16031                                    let _ = local_interp.scope.set_scalar("_0", a.clone());
16032                                    let _ = local_interp.scope.set_scalar("_1", b.clone());
16033                                    local_interp.scope_push_hook();
16034                                    let ord =
16035                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
16036                                            Ok(v) => {
16037                                                let n = v.to_int();
16038                                                if n < 0 {
16039                                                    std::cmp::Ordering::Less
16040                                                } else if n > 0 {
16041                                                    std::cmp::Ordering::Greater
16042                                                } else {
16043                                                    std::cmp::Ordering::Equal
16044                                                }
16045                                            }
16046                                            Err(_) => std::cmp::Ordering::Equal,
16047                                        };
16048                                    local_interp.scope_pop_hook();
16049                                    ord
16050                                });
16051                            }
16052                        }
16053                        None => {
16054                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
16055                        }
16056                    }
16057                    pmap_progress.tick();
16058                    pmap_progress.finish();
16059                }
16060                PipelineOp::PCache { sub, progress } => {
16061                    let subs = self.subs.clone();
16062                    let scope_capture = self.scope.capture();
16063                    let cache = &*crate::pcache::GLOBAL_PCACHE;
16064                    let pmap_progress = PmapProgress::new(progress, v.len());
16065                    v = v
16066                        .into_par_iter()
16067                        .map(|item| {
16068                            let k = crate::pcache::cache_key(&item);
16069                            if let Some(cached) = cache.get(&k) {
16070                                pmap_progress.tick();
16071                                return cached.clone();
16072                            }
16073                            let mut local_interp = Interpreter::new();
16074                            local_interp.subs = subs.clone();
16075                            local_interp.scope.restore_capture(&scope_capture);
16076                            local_interp.enable_parallel_guard();
16077                            local_interp.scope.set_topic(item.clone());
16078                            local_interp.scope_push_hook();
16079                            let val = match local_interp.exec_block_no_scope(&sub.body) {
16080                                Ok(v) => v,
16081                                Err(_) => PerlValue::UNDEF,
16082                            };
16083                            local_interp.scope_pop_hook();
16084                            cache.insert(k, val.clone());
16085                            pmap_progress.tick();
16086                            val
16087                        })
16088                        .collect();
16089                    pmap_progress.finish();
16090                }
16091                PipelineOp::PReduce { sub, progress } => {
16092                    if v.is_empty() {
16093                        return Ok(PerlValue::UNDEF);
16094                    }
16095                    if v.len() == 1 {
16096                        return Ok(v.into_iter().next().unwrap());
16097                    }
16098                    let block = sub.body.clone();
16099                    let subs = self.subs.clone();
16100                    let scope_capture = self.scope.capture();
16101                    let pmap_progress = PmapProgress::new(progress, v.len());
16102                    let result = v
16103                        .into_par_iter()
16104                        .map(|x| {
16105                            pmap_progress.tick();
16106                            x
16107                        })
16108                        .reduce_with(|a, b| {
16109                            let mut local_interp = Interpreter::new();
16110                            local_interp.subs = subs.clone();
16111                            local_interp.scope.restore_capture(&scope_capture);
16112                            local_interp.enable_parallel_guard();
16113                            let _ = local_interp.scope.set_scalar("a", a.clone());
16114                            let _ = local_interp.scope.set_scalar("b", b.clone());
16115                            let _ = local_interp.scope.set_scalar("_0", a);
16116                            let _ = local_interp.scope.set_scalar("_1", b);
16117                            match local_interp.exec_block(&block) {
16118                                Ok(val) => val,
16119                                Err(_) => PerlValue::UNDEF,
16120                            }
16121                        });
16122                    pmap_progress.finish();
16123                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16124                }
16125                PipelineOp::PReduceInit {
16126                    init,
16127                    sub,
16128                    progress,
16129                } => {
16130                    if v.is_empty() {
16131                        return Ok(init);
16132                    }
16133                    let block = sub.body.clone();
16134                    let subs = self.subs.clone();
16135                    let scope_capture = self.scope.capture();
16136                    let cap: &[(String, PerlValue)] = scope_capture.as_slice();
16137                    if v.len() == 1 {
16138                        return Ok(fold_preduce_init_step(
16139                            &subs,
16140                            cap,
16141                            &block,
16142                            preduce_init_fold_identity(&init),
16143                            v.into_iter().next().unwrap(),
16144                        ));
16145                    }
16146                    let pmap_progress = PmapProgress::new(progress, v.len());
16147                    let result = v
16148                        .into_par_iter()
16149                        .fold(
16150                            || preduce_init_fold_identity(&init),
16151                            |acc, item| {
16152                                pmap_progress.tick();
16153                                fold_preduce_init_step(&subs, cap, &block, acc, item)
16154                            },
16155                        )
16156                        .reduce(
16157                            || preduce_init_fold_identity(&init),
16158                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
16159                        );
16160                    pmap_progress.finish();
16161                    return Ok(result);
16162                }
16163                PipelineOp::PMapReduce {
16164                    map,
16165                    reduce,
16166                    progress,
16167                } => {
16168                    if v.is_empty() {
16169                        return Ok(PerlValue::UNDEF);
16170                    }
16171                    let map_block = map.body.clone();
16172                    let reduce_block = reduce.body.clone();
16173                    let subs = self.subs.clone();
16174                    let scope_capture = self.scope.capture();
16175                    if v.len() == 1 {
16176                        let mut local_interp = Interpreter::new();
16177                        local_interp.subs = subs.clone();
16178                        local_interp.scope.restore_capture(&scope_capture);
16179                        local_interp.scope.set_topic(v[0].clone());
16180                        return match local_interp.exec_block_no_scope(&map_block) {
16181                            Ok(val) => Ok(val),
16182                            Err(_) => Ok(PerlValue::UNDEF),
16183                        };
16184                    }
16185                    let pmap_progress = PmapProgress::new(progress, v.len());
16186                    let result = v
16187                        .into_par_iter()
16188                        .map(|item| {
16189                            let mut local_interp = Interpreter::new();
16190                            local_interp.subs = subs.clone();
16191                            local_interp.scope.restore_capture(&scope_capture);
16192                            local_interp.scope.set_topic(item);
16193                            let val = match local_interp.exec_block_no_scope(&map_block) {
16194                                Ok(val) => val,
16195                                Err(_) => PerlValue::UNDEF,
16196                            };
16197                            pmap_progress.tick();
16198                            val
16199                        })
16200                        .reduce_with(|a, b| {
16201                            let mut local_interp = Interpreter::new();
16202                            local_interp.subs = subs.clone();
16203                            local_interp.scope.restore_capture(&scope_capture);
16204                            let _ = local_interp.scope.set_scalar("a", a.clone());
16205                            let _ = local_interp.scope.set_scalar("b", b.clone());
16206                            let _ = local_interp.scope.set_scalar("_0", a);
16207                            let _ = local_interp.scope.set_scalar("_1", b);
16208                            match local_interp.exec_block_no_scope(&reduce_block) {
16209                                Ok(val) => val,
16210                                Err(_) => PerlValue::UNDEF,
16211                            }
16212                        });
16213                    pmap_progress.finish();
16214                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16215                }
16216            }
16217        }
16218        Ok(PerlValue::array(v))
16219    }
16220
16221    /// Streaming collect: wire pipeline ops through bounded channels so items flow
16222    /// between stages concurrently.  Order is **not** preserved.
16223    fn pipeline_collect_streaming(
16224        &mut self,
16225        source: Vec<PerlValue>,
16226        ops: &[PipelineOp],
16227        workers_per_stage: usize,
16228        buffer: usize,
16229        line: usize,
16230    ) -> PerlResult<PerlValue> {
16231        use crossbeam::channel::{bounded, Receiver, Sender};
16232
16233        // Validate: reject ops that require all items (can't stream).
16234        for op in ops {
16235            match op {
16236                PipelineOp::PSort { .. }
16237                | PipelineOp::PReduce { .. }
16238                | PipelineOp::PReduceInit { .. }
16239                | PipelineOp::PMapReduce { .. }
16240                | PipelineOp::PMapChunked { .. } => {
16241                    return Err(PerlError::runtime(
16242                        format!(
16243                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
16244                            std::mem::discriminant(op)
16245                        ),
16246                        line,
16247                    ));
16248                }
16249                _ => {}
16250            }
16251        }
16252
16253        // Filter out non-streamable ops and collect streamable ones.
16254        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
16255        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
16256        if streamable_ops.is_empty() {
16257            return Ok(PerlValue::array(source));
16258        }
16259
16260        let n_stages = streamable_ops.len();
16261        let wn = if workers_per_stage > 0 {
16262            workers_per_stage
16263        } else {
16264            self.parallel_thread_count()
16265        };
16266        let subs = self.subs.clone();
16267        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16268
16269        // Build channels: one between each pair of stages, plus one for output.
16270        // channel[0]: source → stage 0
16271        // channel[i]: stage i-1 → stage i
16272        // channel[n_stages]: stage n_stages-1 → collector
16273        let mut channels: Vec<(Sender<PerlValue>, Receiver<PerlValue>)> =
16274            (0..=n_stages).map(|_| bounded(buffer)).collect();
16275
16276        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
16277        let take_done: Arc<std::sync::atomic::AtomicBool> =
16278            Arc::new(std::sync::atomic::AtomicBool::new(false));
16279
16280        // Collect senders/receivers for each stage.
16281        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
16282        let source_tx = channels[0].0.clone();
16283        let result_rx = channels[n_stages].1.clone();
16284        let results: Arc<Mutex<Vec<PerlValue>>> = Arc::new(Mutex::new(Vec::new()));
16285
16286        std::thread::scope(|scope| {
16287            // Collector thread: drain results concurrently to avoid deadlock
16288            // when bounded channels fill up.
16289            let result_rx_c = result_rx.clone();
16290            let results_c = Arc::clone(&results);
16291            scope.spawn(move || {
16292                while let Ok(item) = result_rx_c.recv() {
16293                    results_c.lock().push(item);
16294                }
16295            });
16296
16297            // Source feeder thread.
16298            let err_s = Arc::clone(&err);
16299            let take_done_s = Arc::clone(&take_done);
16300            scope.spawn(move || {
16301                for item in source {
16302                    if err_s.lock().is_some()
16303                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
16304                    {
16305                        break;
16306                    }
16307                    if source_tx.send(item).is_err() {
16308                        break;
16309                    }
16310                }
16311            });
16312
16313            // Spawn workers for each stage.
16314            for (stage_idx, op) in streamable_ops.iter().enumerate() {
16315                let rx = channels[stage_idx].1.clone();
16316                let tx = channels[stage_idx + 1].0.clone();
16317
16318                for _ in 0..wn {
16319                    let rx = rx.clone();
16320                    let tx = tx.clone();
16321                    let subs = subs.clone();
16322                    let capture = capture.clone();
16323                    let atomic_arrays = atomic_arrays.clone();
16324                    let atomic_hashes = atomic_hashes.clone();
16325                    let err_w = Arc::clone(&err);
16326                    let take_done_w = Arc::clone(&take_done);
16327
16328                    match *op {
16329                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
16330                            let sub = Arc::clone(sub);
16331                            scope.spawn(move || {
16332                                while let Ok(item) = rx.recv() {
16333                                    if err_w.lock().is_some() {
16334                                        break;
16335                                    }
16336                                    let mut interp = Interpreter::new();
16337                                    interp.subs = subs.clone();
16338                                    interp.scope.restore_capture(&capture);
16339                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16340                                    interp.enable_parallel_guard();
16341                                    interp.scope.set_topic(item.clone());
16342                                    interp.scope_push_hook();
16343                                    let keep = match interp.exec_block_no_scope(&sub.body) {
16344                                        Ok(val) => val.is_true(),
16345                                        Err(_) => false,
16346                                    };
16347                                    interp.scope_pop_hook();
16348                                    if keep && tx.send(item).is_err() {
16349                                        break;
16350                                    }
16351                                }
16352                            });
16353                        }
16354                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
16355                            let sub = Arc::clone(sub);
16356                            scope.spawn(move || {
16357                                while let Ok(item) = rx.recv() {
16358                                    if err_w.lock().is_some() {
16359                                        break;
16360                                    }
16361                                    let mut interp = Interpreter::new();
16362                                    interp.subs = subs.clone();
16363                                    interp.scope.restore_capture(&capture);
16364                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16365                                    interp.enable_parallel_guard();
16366                                    interp.scope.set_topic(item);
16367                                    interp.scope_push_hook();
16368                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
16369                                        Ok(val) => val,
16370                                        Err(_) => PerlValue::UNDEF,
16371                                    };
16372                                    interp.scope_pop_hook();
16373                                    if tx.send(mapped).is_err() {
16374                                        break;
16375                                    }
16376                                }
16377                            });
16378                        }
16379                        PipelineOp::Take(n) => {
16380                            let limit = (*n).max(0) as usize;
16381                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
16382                            let count_w = Arc::clone(&count);
16383                            scope.spawn(move || {
16384                                while let Ok(item) = rx.recv() {
16385                                    let prev =
16386                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
16387                                    if prev >= limit {
16388                                        take_done_w
16389                                            .store(true, std::sync::atomic::Ordering::Relaxed);
16390                                        break;
16391                                    }
16392                                    if tx.send(item).is_err() {
16393                                        break;
16394                                    }
16395                                }
16396                            });
16397                            // Take only needs 1 worker; skip remaining worker spawns.
16398                            break;
16399                        }
16400                        PipelineOp::PFor { ref sub, .. } => {
16401                            let sub = Arc::clone(sub);
16402                            scope.spawn(move || {
16403                                while let Ok(item) = rx.recv() {
16404                                    if err_w.lock().is_some() {
16405                                        break;
16406                                    }
16407                                    let mut interp = Interpreter::new();
16408                                    interp.subs = subs.clone();
16409                                    interp.scope.restore_capture(&capture);
16410                                    interp
16411                                        .scope
16412                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16413                                    interp.enable_parallel_guard();
16414                                    interp.scope.set_topic(item.clone());
16415                                    interp.scope_push_hook();
16416                                    match interp.exec_block_no_scope(&sub.body) {
16417                                        Ok(_) => {}
16418                                        Err(e) => {
16419                                            let msg = match e {
16420                                                FlowOrError::Error(stryke) => stryke.to_string(),
16421                                                FlowOrError::Flow(_) => {
16422                                                    "unexpected control flow in par_pipeline_stream pfor".into()
16423                                                }
16424                                            };
16425                                            let mut g = err_w.lock();
16426                                            if g.is_none() {
16427                                                *g = Some(msg);
16428                                            }
16429                                            interp.scope_pop_hook();
16430                                            break;
16431                                        }
16432                                    }
16433                                    interp.scope_pop_hook();
16434                                    if tx.send(item).is_err() {
16435                                        break;
16436                                    }
16437                                }
16438                            });
16439                        }
16440                        PipelineOp::Tap(ref sub) => {
16441                            let sub = Arc::clone(sub);
16442                            scope.spawn(move || {
16443                                while let Ok(item) = rx.recv() {
16444                                    if err_w.lock().is_some() {
16445                                        break;
16446                                    }
16447                                    let mut interp = Interpreter::new();
16448                                    interp.subs = subs.clone();
16449                                    interp.scope.restore_capture(&capture);
16450                                    interp
16451                                        .scope
16452                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16453                                    interp.enable_parallel_guard();
16454                                    match interp.call_sub(
16455                                        &sub,
16456                                        vec![item.clone()],
16457                                        WantarrayCtx::Void,
16458                                        line,
16459                                    )
16460                                    {
16461                                        Ok(_) => {}
16462                                        Err(e) => {
16463                                            let msg = match e {
16464                                                FlowOrError::Error(stryke) => stryke.to_string(),
16465                                                FlowOrError::Flow(_) => {
16466                                                    "unexpected control flow in par_pipeline_stream tap"
16467                                                        .into()
16468                                                }
16469                                            };
16470                                            let mut g = err_w.lock();
16471                                            if g.is_none() {
16472                                                *g = Some(msg);
16473                                            }
16474                                            break;
16475                                        }
16476                                    }
16477                                    if tx.send(item).is_err() {
16478                                        break;
16479                                    }
16480                                }
16481                            });
16482                        }
16483                        PipelineOp::PCache { ref sub, .. } => {
16484                            let sub = Arc::clone(sub);
16485                            scope.spawn(move || {
16486                                while let Ok(item) = rx.recv() {
16487                                    if err_w.lock().is_some() {
16488                                        break;
16489                                    }
16490                                    let k = crate::pcache::cache_key(&item);
16491                                    let val = if let Some(cached) =
16492                                        crate::pcache::GLOBAL_PCACHE.get(&k)
16493                                    {
16494                                        cached.clone()
16495                                    } else {
16496                                        let mut interp = Interpreter::new();
16497                                        interp.subs = subs.clone();
16498                                        interp.scope.restore_capture(&capture);
16499                                        interp
16500                                            .scope
16501                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
16502                                        interp.enable_parallel_guard();
16503                                        interp.scope.set_topic(item);
16504                                        interp.scope_push_hook();
16505                                        let v = match interp.exec_block_no_scope(&sub.body) {
16506                                            Ok(v) => v,
16507                                            Err(_) => PerlValue::UNDEF,
16508                                        };
16509                                        interp.scope_pop_hook();
16510                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
16511                                        v
16512                                    };
16513                                    if tx.send(val).is_err() {
16514                                        break;
16515                                    }
16516                                }
16517                            });
16518                        }
16519                        // Non-streaming ops already rejected above.
16520                        _ => unreachable!(),
16521                    }
16522                }
16523            }
16524
16525            // Drop our copies of intermediate senders/receivers so channels disconnect
16526            // when workers finish.  Also drop result_rx so the collector thread exits
16527            // once all stage workers are done.
16528            channels.clear();
16529            drop(result_rx);
16530        });
16531
16532        if let Some(msg) = err.lock().take() {
16533            return Err(PerlError::runtime(msg, line));
16534        }
16535
16536        let results = std::mem::take(&mut *results.lock());
16537        Ok(PerlValue::array(results))
16538    }
16539
16540    fn heap_compare(&mut self, cmp: &Arc<PerlSub>, a: &PerlValue, b: &PerlValue) -> Ordering {
16541        self.scope_push_hook();
16542        if let Some(ref env) = cmp.closure_env {
16543            self.scope.restore_capture(env);
16544        }
16545        let _ = self.scope.set_scalar("a", a.clone());
16546        let _ = self.scope.set_scalar("b", b.clone());
16547        let _ = self.scope.set_scalar("_0", a.clone());
16548        let _ = self.scope.set_scalar("_1", b.clone());
16549        let ord = match self.exec_block_no_scope(&cmp.body) {
16550            Ok(v) => {
16551                let n = v.to_int();
16552                if n < 0 {
16553                    Ordering::Less
16554                } else if n > 0 {
16555                    Ordering::Greater
16556                } else {
16557                    Ordering::Equal
16558                }
16559            }
16560            Err(_) => Ordering::Equal,
16561        };
16562        self.scope_pop_hook();
16563        ord
16564    }
16565
16566    fn heap_sift_up(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
16567        while i > 0 {
16568            let p = (i - 1) / 2;
16569            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
16570                break;
16571            }
16572            items.swap(i, p);
16573            i = p;
16574        }
16575    }
16576
16577    fn heap_sift_down(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
16578        let n = items.len();
16579        loop {
16580            let mut sm = i;
16581            let l = 2 * i + 1;
16582            let r = 2 * i + 2;
16583            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
16584                sm = l;
16585            }
16586            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
16587                sm = r;
16588            }
16589            if sm == i {
16590                break;
16591            }
16592            items.swap(i, sm);
16593            i = sm;
16594        }
16595    }
16596
16597    fn hash_for_signature_destruct(
16598        &mut self,
16599        v: &PerlValue,
16600        line: usize,
16601    ) -> PerlResult<IndexMap<String, PerlValue>> {
16602        let Some(m) = self.match_subject_as_hash(v) else {
16603            return Err(PerlError::runtime(
16604                format!(
16605                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
16606                    v.ref_type()
16607                ),
16608                line,
16609            ));
16610        };
16611        Ok(m)
16612    }
16613
16614    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
16615    pub(crate) fn apply_sub_signature(
16616        &mut self,
16617        sub: &PerlSub,
16618        argv: &[PerlValue],
16619        line: usize,
16620    ) -> PerlResult<()> {
16621        if sub.params.is_empty() {
16622            return Ok(());
16623        }
16624        let mut i = 0usize;
16625        for p in &sub.params {
16626            match p {
16627                SubSigParam::Scalar(name, ty) => {
16628                    let val = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
16629                    i += 1;
16630                    if let Some(t) = ty {
16631                        if let Err(e) = t.check_value(&val) {
16632                            return Err(PerlError::runtime(
16633                                format!("sub parameter ${}: {}", name, e),
16634                                line,
16635                            ));
16636                        }
16637                    }
16638                    let n = self.english_scalar_name(name);
16639                    self.scope.declare_scalar(n, val);
16640                }
16641                SubSigParam::ArrayDestruct(elems) => {
16642                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
16643                    i += 1;
16644                    let Some(arr) = self.match_subject_as_array(&arg) else {
16645                        return Err(PerlError::runtime(
16646                            format!(
16647                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
16648                                arg.ref_type()
16649                            ),
16650                            line,
16651                        ));
16652                    };
16653                    let binds = self
16654                        .match_array_pattern_elems(&arr, elems, line)
16655                        .map_err(|e| match e {
16656                            FlowOrError::Error(stryke) => stryke,
16657                            FlowOrError::Flow(_) => PerlError::runtime(
16658                                "unexpected flow in sub signature array destruct",
16659                                line,
16660                            ),
16661                        })?;
16662                    let Some(binds) = binds else {
16663                        return Err(PerlError::runtime(
16664                            "sub signature array destruct: length or element mismatch",
16665                            line,
16666                        ));
16667                    };
16668                    for b in binds {
16669                        match b {
16670                            PatternBinding::Scalar(name, v) => {
16671                                let n = self.english_scalar_name(&name);
16672                                self.scope.declare_scalar(n, v);
16673                            }
16674                            PatternBinding::Array(name, elems) => {
16675                                self.scope.declare_array(&name, elems);
16676                            }
16677                        }
16678                    }
16679                }
16680                SubSigParam::HashDestruct(pairs) => {
16681                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
16682                    i += 1;
16683                    let map = self.hash_for_signature_destruct(&arg, line)?;
16684                    for (key, varname) in pairs {
16685                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
16686                        let n = self.english_scalar_name(varname);
16687                        self.scope.declare_scalar(n, v);
16688                    }
16689                }
16690            }
16691        }
16692        Ok(())
16693    }
16694
16695    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
16696    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
16697    /// These are `PerlSub`s with empty bodies and magic keys in `closure_env`.
16698    pub(crate) fn try_hof_dispatch(
16699        &mut self,
16700        sub: &PerlSub,
16701        args: &[PerlValue],
16702        want: WantarrayCtx,
16703        line: usize,
16704    ) -> Option<ExecResult> {
16705        let env = sub.closure_env.as_ref()?;
16706        fn env_get<'a>(env: &'a [(String, PerlValue)], key: &str) -> Option<&'a PerlValue> {
16707            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
16708        }
16709
16710        match sub.name.as_str() {
16711            // ── compose: right-to-left function application ──
16712            "__comp__" => {
16713                let fns = env_get(env, "__comp_fns__")?.to_list();
16714                let mut val = args.first().cloned().unwrap_or(PerlValue::UNDEF);
16715                for f in fns.iter().rev() {
16716                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
16717                        Ok(v) => val = v,
16718                        Err(e) => return Some(Err(e)),
16719                    }
16720                }
16721                Some(Ok(val))
16722            }
16723            // ── constantly: always return the captured value ──
16724            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
16725            // ── juxt: call each fn with same args, collect results ──
16726            "__juxt__" => {
16727                let fns = env_get(env, "__juxt_fns__")?.to_list();
16728                let mut results = Vec::with_capacity(fns.len());
16729                for f in &fns {
16730                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
16731                        Ok(v) => results.push(v),
16732                        Err(e) => return Some(Err(e)),
16733                    }
16734                }
16735                Some(Ok(PerlValue::array(results)))
16736            }
16737            // ── partial: prepend bound args ──
16738            "__partial__" => {
16739                let fn_val = env_get(env, "__partial_fn__")?.clone();
16740                let bound = env_get(env, "__partial_args__")?.to_list();
16741                let mut all_args = bound;
16742                all_args.extend_from_slice(args);
16743                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
16744            }
16745            // ── complement: negate the result ──
16746            "__complement__" => {
16747                let fn_val = env_get(env, "__complement_fn__")?.clone();
16748                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
16749                    Ok(v) => Some(Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }))),
16750                    Err(e) => Some(Err(e)),
16751                }
16752            }
16753            // ── fnil: replace undef args with defaults ──
16754            "__fnil__" => {
16755                let fn_val = env_get(env, "__fnil_fn__")?.clone();
16756                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
16757                let mut patched = args.to_vec();
16758                for (i, d) in defaults.iter().enumerate() {
16759                    if i < patched.len() {
16760                        if patched[i].is_undef() {
16761                            patched[i] = d.clone();
16762                        }
16763                    } else {
16764                        patched.push(d.clone());
16765                    }
16766                }
16767                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
16768            }
16769            // ── memoize: cache by stringified args ──
16770            "__memoize__" => {
16771                let fn_val = env_get(env, "__memoize_fn__")?.clone();
16772                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
16773                let key = args
16774                    .iter()
16775                    .map(|a| a.to_string())
16776                    .collect::<Vec<_>>()
16777                    .join("\x00");
16778                if let Some(href) = cache_ref.as_hash_ref() {
16779                    if let Some(cached) = href.read().get(&key) {
16780                        return Some(Ok(cached.clone()));
16781                    }
16782                }
16783                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
16784                    Ok(v) => {
16785                        if let Some(href) = cache_ref.as_hash_ref() {
16786                            href.write().insert(key, v.clone());
16787                        }
16788                        Some(Ok(v))
16789                    }
16790                    Err(e) => Some(Err(e)),
16791                }
16792            }
16793            // ── curry: accumulate args until arity reached ──
16794            "__curry__" => {
16795                let fn_val = env_get(env, "__curry_fn__")?.clone();
16796                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
16797                let bound = env_get(env, "__curry_bound__")?.to_list();
16798                let mut all = bound;
16799                all.extend_from_slice(args);
16800                if all.len() >= arity {
16801                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
16802                } else {
16803                    let curry_sub = PerlSub {
16804                        name: "__curry__".to_string(),
16805                        params: vec![],
16806                        body: vec![],
16807                        closure_env: Some(vec![
16808                            ("__curry_fn__".to_string(), fn_val),
16809                            (
16810                                "__curry_arity__".to_string(),
16811                                PerlValue::integer(arity as i64),
16812                            ),
16813                            ("__curry_bound__".to_string(), PerlValue::array(all)),
16814                        ]),
16815                        prototype: None,
16816                        fib_like: None,
16817                    };
16818                    Some(Ok(PerlValue::code_ref(Arc::new(curry_sub))))
16819                }
16820            }
16821            // ── once: call once, cache forever ──
16822            "__once__" => {
16823                let cache_ref = env_get(env, "__once_cache__")?.clone();
16824                if let Some(href) = cache_ref.as_hash_ref() {
16825                    let r = href.read();
16826                    if r.contains_key("done") {
16827                        return Some(Ok(r.get("val").cloned().unwrap_or(PerlValue::UNDEF)));
16828                    }
16829                }
16830                let fn_val = env_get(env, "__once_fn__")?.clone();
16831                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
16832                    Ok(v) => {
16833                        if let Some(href) = cache_ref.as_hash_ref() {
16834                            let mut w = href.write();
16835                            w.insert("done".to_string(), PerlValue::integer(1));
16836                            w.insert("val".to_string(), v.clone());
16837                        }
16838                        Some(Ok(v))
16839                    }
16840                    Err(e) => Some(Err(e)),
16841                }
16842            }
16843            _ => None,
16844        }
16845    }
16846
16847    pub(crate) fn call_sub(
16848        &mut self,
16849        sub: &PerlSub,
16850        args: Vec<PerlValue>,
16851        want: WantarrayCtx,
16852        _line: usize,
16853    ) -> ExecResult {
16854        // Push current sub for __SUB__ access
16855        self.current_sub_stack.push(Arc::new(sub.clone()));
16856
16857        // Single frame for both @_ and the block's local variables —
16858        // avoids the double push_frame/pop_frame overhead per call.
16859        self.scope_push_hook();
16860        self.scope.declare_array("_", args.clone());
16861        if let Some(ref env) = sub.closure_env {
16862            self.scope.restore_capture(env);
16863        }
16864        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
16865        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
16866        // Must be AFTER restore_capture so we don't get shadowed by captured $_
16867        self.scope.set_closure_args(&args);
16868        // Move `@_` out so `native_dispatch` / `fib_like` take `&[PerlValue]` without `get_array` cloning.
16869        let argv = self.scope.take_sub_underscore().unwrap_or_default();
16870        self.apply_sub_signature(sub, &argv, _line)?;
16871        let saved = self.wantarray_kind;
16872        self.wantarray_kind = want;
16873        if let Some(r) = crate::list_util::native_dispatch(self, sub, &argv, want) {
16874            self.wantarray_kind = saved;
16875            self.scope_pop_hook();
16876            self.current_sub_stack.pop();
16877            return match r {
16878                Ok(v) => Ok(v),
16879                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16880                Err(e) => Err(e),
16881            };
16882        }
16883        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
16884            self.wantarray_kind = saved;
16885            self.scope_pop_hook();
16886            self.current_sub_stack.pop();
16887            return match r {
16888                Ok(v) => Ok(v),
16889                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16890                Err(e) => Err(e),
16891            };
16892        }
16893        if let Some(pat) = sub.fib_like.as_ref() {
16894            if argv.len() == 1 {
16895                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
16896                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
16897                    if let Some(p) = &mut self.profiler {
16898                        p.enter_sub(&sub.name);
16899                    }
16900                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
16901                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
16902                        p.exit_sub(t0.elapsed());
16903                    }
16904                    self.wantarray_kind = saved;
16905                    self.scope_pop_hook();
16906                    self.current_sub_stack.pop();
16907                    return Ok(PerlValue::integer(n));
16908                }
16909            }
16910        }
16911        self.scope.declare_array("_", argv.clone());
16912        // Note: set_closure_args was already called at line 15077; don't call it again
16913        // as that would incorrectly shift the outer topic stack a second time.
16914        let t0 = self.profiler.is_some().then(std::time::Instant::now);
16915        if let Some(p) = &mut self.profiler {
16916            p.enter_sub(&sub.name);
16917        }
16918        // Pass wantarray context for the implicit return (last expression in block).
16919        let result = self.exec_block_no_scope_with_tail(&sub.body, self.wantarray_kind);
16920        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
16921            p.exit_sub(t0.elapsed());
16922        }
16923        // For goto &sub, capture @_ before popping the frame
16924        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
16925            Some(self.scope.get_array("_"))
16926        } else {
16927            None
16928        };
16929        self.wantarray_kind = saved;
16930        self.scope_pop_hook();
16931        self.current_sub_stack.pop();
16932        match result {
16933            Ok(v) => Ok(v),
16934            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16935            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
16936                // goto &sub — tail call: look up target and call with same @_
16937                let goto_args = goto_args.unwrap_or_default();
16938                let fqn = if target_name.contains("::") {
16939                    target_name.clone()
16940                } else {
16941                    format!("{}::{}", self.current_package(), target_name)
16942                };
16943                if let Some(target_sub) = self
16944                    .subs
16945                    .get(&fqn)
16946                    .cloned()
16947                    .or_else(|| self.subs.get(&target_name).cloned())
16948                {
16949                    self.call_sub(&target_sub, goto_args, want, _line)
16950                } else {
16951                    Err(
16952                        PerlError::runtime(format!("Undefined subroutine &{}", target_name), _line)
16953                            .into(),
16954                    )
16955                }
16956            }
16957            Err(FlowOrError::Flow(Flow::Yield(_))) => {
16958                Err(PerlError::runtime("yield is only valid inside gen { }", 0).into())
16959            }
16960            Err(e) => Err(e),
16961        }
16962    }
16963
16964    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
16965    fn call_struct_method(
16966        &mut self,
16967        body: &Block,
16968        params: &[SubSigParam],
16969        args: Vec<PerlValue>,
16970        line: usize,
16971    ) -> ExecResult {
16972        self.scope_push_hook();
16973        self.scope.declare_array("_", args.clone());
16974        // Bind $self to first arg (the receiver)
16975        if let Some(self_val) = args.first() {
16976            self.scope.declare_scalar("self", self_val.clone());
16977        }
16978        // Set $_0, $_1, etc. for all args
16979        self.scope.set_closure_args(&args);
16980        // Apply signature if provided - skip the first arg ($self) for user params
16981        let user_args: Vec<PerlValue> = args.iter().skip(1).cloned().collect();
16982        self.apply_params_to_argv(params, &user_args, line)?;
16983        let result = self.exec_block_no_scope(body);
16984        self.scope_pop_hook();
16985        match result {
16986            Ok(v) => Ok(v),
16987            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
16988            Err(e) => Err(e),
16989        }
16990    }
16991
16992    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
16993    pub(crate) fn call_class_method(
16994        &mut self,
16995        body: &Block,
16996        params: &[SubSigParam],
16997        args: Vec<PerlValue>,
16998        line: usize,
16999    ) -> ExecResult {
17000        self.call_class_method_inner(body, params, args, line, false)
17001    }
17002
17003    /// Call a static class method: `Math::add(...)`.
17004    pub(crate) fn call_static_class_method(
17005        &mut self,
17006        body: &Block,
17007        params: &[SubSigParam],
17008        args: Vec<PerlValue>,
17009        line: usize,
17010    ) -> ExecResult {
17011        self.call_class_method_inner(body, params, args, line, true)
17012    }
17013
17014    fn call_class_method_inner(
17015        &mut self,
17016        body: &Block,
17017        params: &[SubSigParam],
17018        args: Vec<PerlValue>,
17019        line: usize,
17020        is_static: bool,
17021    ) -> ExecResult {
17022        self.scope_push_hook();
17023        self.scope.declare_array("_", args.clone());
17024        if !is_static {
17025            // Bind $self to first arg (the receiver) for instance methods
17026            if let Some(self_val) = args.first() {
17027                self.scope.declare_scalar("self", self_val.clone());
17028            }
17029        }
17030        // Set $_0, $_1, etc. for all args
17031        self.scope.set_closure_args(&args);
17032        // Apply signature: skip first arg ($self) only for instance methods
17033        let user_args: Vec<PerlValue> = if is_static {
17034            args.clone()
17035        } else {
17036            args.iter().skip(1).cloned().collect()
17037        };
17038        self.apply_params_to_argv(params, &user_args, line)?;
17039        let result = self.exec_block_no_scope(body);
17040        self.scope_pop_hook();
17041        match result {
17042            Ok(v) => Ok(v),
17043            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17044            Err(e) => Err(e),
17045        }
17046    }
17047
17048    /// Apply SubSigParam bindings without the full PerlSub machinery.
17049    fn apply_params_to_argv(
17050        &mut self,
17051        params: &[SubSigParam],
17052        argv: &[PerlValue],
17053        line: usize,
17054    ) -> PerlResult<()> {
17055        let mut i = 0;
17056        for param in params {
17057            match param {
17058                SubSigParam::Scalar(name, ty_opt) => {
17059                    let v = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17060                    i += 1;
17061                    if let Some(ty) = ty_opt {
17062                        ty.check_value(&v).map_err(|msg| {
17063                            PerlError::type_error(
17064                                format!("method parameter ${}: {}", name, msg),
17065                                line,
17066                            )
17067                        })?;
17068                    }
17069                    let n = self.english_scalar_name(name);
17070                    self.scope.declare_scalar(n, v);
17071                }
17072                SubSigParam::ArrayDestruct(elems) => {
17073                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17074                    i += 1;
17075                    let Some(arr) = self.match_subject_as_array(&arg) else {
17076                        return Err(PerlError::runtime(
17077                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
17078                            line,
17079                        ));
17080                    };
17081                    let binds = self
17082                        .match_array_pattern_elems(&arr, elems, line)
17083                        .map_err(|e| match e {
17084                            FlowOrError::Error(stryke) => stryke,
17085                            FlowOrError::Flow(_) => {
17086                                PerlError::runtime("unexpected flow in method array destruct", line)
17087                            }
17088                        })?;
17089                    let Some(binds) = binds else {
17090                        return Err(PerlError::runtime(
17091                            format!(
17092                                "method parameter: array destructure failed at position {}",
17093                                i
17094                            ),
17095                            line,
17096                        ));
17097                    };
17098                    for b in binds {
17099                        match b {
17100                            PatternBinding::Scalar(name, v) => {
17101                                let n = self.english_scalar_name(&name);
17102                                self.scope.declare_scalar(n, v);
17103                            }
17104                            PatternBinding::Array(name, elems) => {
17105                                self.scope.declare_array(&name, elems);
17106                            }
17107                        }
17108                    }
17109                }
17110                SubSigParam::HashDestruct(pairs) => {
17111                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17112                    i += 1;
17113                    let map = self.hash_for_signature_destruct(&arg, line)?;
17114                    for (key, varname) in pairs {
17115                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
17116                        let n = self.english_scalar_name(varname);
17117                        self.scope.declare_scalar(n, v);
17118                    }
17119                }
17120            }
17121        }
17122        Ok(())
17123    }
17124
17125    fn builtin_new(&mut self, class: &str, args: Vec<PerlValue>, line: usize) -> ExecResult {
17126        if class == "Set" {
17127            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
17128        }
17129        if let Some(def) = self.struct_defs.get(class).cloned() {
17130            let mut provided = Vec::new();
17131            let mut i = 1;
17132            while i + 1 < args.len() {
17133                let k = args[i].to_string();
17134                let v = args[i + 1].clone();
17135                provided.push((k, v));
17136                i += 2;
17137            }
17138            let mut defaults = Vec::with_capacity(def.fields.len());
17139            for field in &def.fields {
17140                if let Some(ref expr) = field.default {
17141                    let val = self.eval_expr(expr)?;
17142                    defaults.push(Some(val));
17143                } else {
17144                    defaults.push(None);
17145                }
17146            }
17147            return Ok(crate::native_data::struct_new_with_defaults(
17148                &def, &provided, &defaults, line,
17149            )?);
17150        }
17151        // Default OO constructor: Class->new(%args) → bless {%args}, class
17152        let mut map = IndexMap::new();
17153        let mut i = 1; // skip $self (first arg is class name)
17154        while i + 1 < args.len() {
17155            let k = args[i].to_string();
17156            let v = args[i + 1].clone();
17157            map.insert(k, v);
17158            i += 2;
17159        }
17160        Ok(PerlValue::blessed(Arc::new(
17161            crate::value::BlessedRef::new_blessed(class.to_string(), PerlValue::hash(map)),
17162        )))
17163    }
17164
17165    fn exec_print(
17166        &mut self,
17167        handle: Option<&str>,
17168        args: &[Expr],
17169        newline: bool,
17170        line: usize,
17171    ) -> ExecResult {
17172        if newline && (self.feature_bits & FEAT_SAY) == 0 {
17173            return Err(PerlError::runtime(
17174                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
17175                line,
17176            )
17177            .into());
17178        }
17179        let mut output = String::new();
17180        if args.is_empty() {
17181            // Perl: print with no LIST prints $_ (same for say).
17182            let topic = self.scope.get_scalar("_").clone();
17183            let s = self.stringify_value(topic, line)?;
17184            output.push_str(&s);
17185        } else {
17186            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
17187            // between those top-level expressions only (not between elements of an expanded `@arr`).
17188            for (i, a) in args.iter().enumerate() {
17189                if i > 0 {
17190                    output.push_str(&self.ofs);
17191                }
17192                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17193                for item in val.to_list() {
17194                    let s = self.stringify_value(item, line)?;
17195                    output.push_str(&s);
17196                }
17197            }
17198        }
17199        if newline {
17200            output.push('\n');
17201        }
17202        output.push_str(&self.ors);
17203
17204        let handle_name =
17205            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17206        self.write_formatted_print(handle_name.as_str(), &output, line)?;
17207        Ok(PerlValue::integer(1))
17208    }
17209
17210    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
17211        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
17212            // Perl: printf with no args uses $_ as the format string.
17213            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
17214            (s, &[])
17215        } else {
17216            (self.eval_expr(&args[0])?.to_string(), &args[1..])
17217        };
17218        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
17219        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
17220        // ranges to flip-flop values, so go through list-context eval and splat.
17221        let mut arg_vals = Vec::new();
17222        for a in rest {
17223            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17224            if let Some(items) = v.as_array_vec() {
17225                arg_vals.extend(items);
17226            } else {
17227                arg_vals.push(v);
17228            }
17229        }
17230        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
17231        let handle_name =
17232            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17233        match handle_name.as_str() {
17234            "STDOUT" => {
17235                if !self.suppress_stdout {
17236                    print!("{}", output);
17237                    if self.output_autoflush {
17238                        let _ = io::stdout().flush();
17239                    }
17240                }
17241            }
17242            "STDERR" => {
17243                eprint!("{}", output);
17244                let _ = io::stderr().flush();
17245            }
17246            name => {
17247                if let Some(writer) = self.output_handles.get_mut(name) {
17248                    let _ = writer.write_all(output.as_bytes());
17249                    if self.output_autoflush {
17250                        let _ = writer.flush();
17251                    }
17252                }
17253            }
17254        }
17255        Ok(PerlValue::integer(1))
17256    }
17257
17258    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
17259    pub(crate) fn eval_substr_expr(
17260        &mut self,
17261        string: &Expr,
17262        offset: &Expr,
17263        length: Option<&Expr>,
17264        replacement: Option<&Expr>,
17265        _line: usize,
17266    ) -> Result<PerlValue, FlowOrError> {
17267        let s = self.eval_expr(string)?.to_string();
17268        let off = self.eval_expr(offset)?.to_int();
17269        let start = if off < 0 {
17270            (s.len() as i64 + off).max(0) as usize
17271        } else {
17272            off as usize
17273        };
17274        let len = if let Some(l) = length {
17275            self.eval_expr(l)?.to_int() as usize
17276        } else {
17277            s.len().saturating_sub(start)
17278        };
17279        let end = (start + len).min(s.len());
17280        let result = s.get(start..end).unwrap_or("").to_string();
17281        if let Some(rep) = replacement {
17282            let rep_s = self.eval_expr(rep)?.to_string();
17283            let mut new_s = String::new();
17284            new_s.push_str(&s[..start]);
17285            new_s.push_str(&rep_s);
17286            new_s.push_str(&s[end..]);
17287            self.assign_value(string, PerlValue::string(new_s))?;
17288        }
17289        Ok(PerlValue::string(result))
17290    }
17291
17292    pub(crate) fn eval_push_expr(
17293        &mut self,
17294        array: &Expr,
17295        values: &[Expr],
17296        line: usize,
17297    ) -> Result<PerlValue, FlowOrError> {
17298        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17299            for v in values {
17300                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17301                self.push_array_deref_value(aref.clone(), val, line)?;
17302            }
17303            let len = self.array_deref_len(aref, line)?;
17304            return Ok(PerlValue::integer(len));
17305        }
17306        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17307        if self.scope.is_array_frozen(&arr_name) {
17308            return Err(PerlError::runtime(
17309                format!("Modification of a frozen value: @{}", arr_name),
17310                line,
17311            )
17312            .into());
17313        }
17314        for v in values {
17315            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17316            if let Some(items) = val.as_array_vec() {
17317                for item in items {
17318                    self.scope
17319                        .push_to_array(&arr_name, item)
17320                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17321                }
17322            } else {
17323                self.scope
17324                    .push_to_array(&arr_name, val)
17325                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17326            }
17327        }
17328        let len = self.scope.array_len(&arr_name);
17329        Ok(PerlValue::integer(len as i64))
17330    }
17331
17332    pub(crate) fn eval_pop_expr(
17333        &mut self,
17334        array: &Expr,
17335        line: usize,
17336    ) -> Result<PerlValue, FlowOrError> {
17337        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17338            return self.pop_array_deref(aref, line);
17339        }
17340        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17341        self.scope
17342            .pop_from_array(&arr_name)
17343            .map_err(|e| FlowOrError::Error(e.at_line(line)))
17344    }
17345
17346    pub(crate) fn eval_shift_expr(
17347        &mut self,
17348        array: &Expr,
17349        line: usize,
17350    ) -> Result<PerlValue, FlowOrError> {
17351        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17352            return self.shift_array_deref(aref, line);
17353        }
17354        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17355        self.scope
17356            .shift_from_array(&arr_name)
17357            .map_err(|e| FlowOrError::Error(e.at_line(line)))
17358    }
17359
17360    pub(crate) fn eval_unshift_expr(
17361        &mut self,
17362        array: &Expr,
17363        values: &[Expr],
17364        line: usize,
17365    ) -> Result<PerlValue, FlowOrError> {
17366        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17367            let mut vals = Vec::new();
17368            for v in values {
17369                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17370                if let Some(items) = val.as_array_vec() {
17371                    vals.extend(items);
17372                } else {
17373                    vals.push(val);
17374                }
17375            }
17376            let len = self.unshift_array_deref_multi(aref, vals, line)?;
17377            return Ok(PerlValue::integer(len));
17378        }
17379        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17380        let mut vals = Vec::new();
17381        for v in values {
17382            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17383            if let Some(items) = val.as_array_vec() {
17384                vals.extend(items);
17385            } else {
17386                vals.push(val);
17387            }
17388        }
17389        let arr = self
17390            .scope
17391            .get_array_mut(&arr_name)
17392            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17393        for (i, v) in vals.into_iter().enumerate() {
17394            arr.insert(i, v);
17395        }
17396        let len = arr.len();
17397        Ok(PerlValue::integer(len as i64))
17398    }
17399
17400    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
17401    pub(crate) fn push_array_deref_value(
17402        &mut self,
17403        arr_ref: PerlValue,
17404        val: PerlValue,
17405        line: usize,
17406    ) -> Result<(), FlowOrError> {
17407        if let Some(r) = arr_ref.as_array_ref() {
17408            let mut w = r.write();
17409            if let Some(items) = val.as_array_vec() {
17410                w.extend(items.iter().cloned());
17411            } else {
17412                w.push(val);
17413            }
17414            return Ok(());
17415        }
17416        if let Some(name) = arr_ref.as_array_binding_name() {
17417            if let Some(items) = val.as_array_vec() {
17418                for item in items {
17419                    self.scope
17420                        .push_to_array(&name, item)
17421                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17422                }
17423            } else {
17424                self.scope
17425                    .push_to_array(&name, val)
17426                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17427            }
17428            return Ok(());
17429        }
17430        if let Some(s) = arr_ref.as_str() {
17431            if self.strict_refs {
17432                return Err(PerlError::runtime(
17433                    format!(
17434                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17435                        s
17436                    ),
17437                    line,
17438                )
17439                .into());
17440            }
17441            let name = s.to_string();
17442            if let Some(items) = val.as_array_vec() {
17443                for item in items {
17444                    self.scope
17445                        .push_to_array(&name, item)
17446                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17447                }
17448            } else {
17449                self.scope
17450                    .push_to_array(&name, val)
17451                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17452            }
17453            return Ok(());
17454        }
17455        Err(PerlError::runtime("push argument is not an ARRAY reference", line).into())
17456    }
17457
17458    pub(crate) fn array_deref_len(
17459        &self,
17460        arr_ref: PerlValue,
17461        line: usize,
17462    ) -> Result<i64, FlowOrError> {
17463        if let Some(r) = arr_ref.as_array_ref() {
17464            return Ok(r.read().len() as i64);
17465        }
17466        if let Some(name) = arr_ref.as_array_binding_name() {
17467            return Ok(self.scope.array_len(&name) as i64);
17468        }
17469        if let Some(s) = arr_ref.as_str() {
17470            if self.strict_refs {
17471                return Err(PerlError::runtime(
17472                    format!(
17473                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17474                        s
17475                    ),
17476                    line,
17477                )
17478                .into());
17479            }
17480            return Ok(self.scope.array_len(&s) as i64);
17481        }
17482        Err(PerlError::runtime("argument is not an ARRAY reference", line).into())
17483    }
17484
17485    pub(crate) fn pop_array_deref(
17486        &mut self,
17487        arr_ref: PerlValue,
17488        line: usize,
17489    ) -> Result<PerlValue, FlowOrError> {
17490        if let Some(r) = arr_ref.as_array_ref() {
17491            let mut w = r.write();
17492            return Ok(w.pop().unwrap_or(PerlValue::UNDEF));
17493        }
17494        if let Some(name) = arr_ref.as_array_binding_name() {
17495            return self
17496                .scope
17497                .pop_from_array(&name)
17498                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17499        }
17500        if let Some(s) = arr_ref.as_str() {
17501            if self.strict_refs {
17502                return Err(PerlError::runtime(
17503                    format!(
17504                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17505                        s
17506                    ),
17507                    line,
17508                )
17509                .into());
17510            }
17511            return self
17512                .scope
17513                .pop_from_array(&s)
17514                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17515        }
17516        Err(PerlError::runtime("pop argument is not an ARRAY reference", line).into())
17517    }
17518
17519    pub(crate) fn shift_array_deref(
17520        &mut self,
17521        arr_ref: PerlValue,
17522        line: usize,
17523    ) -> Result<PerlValue, FlowOrError> {
17524        if let Some(r) = arr_ref.as_array_ref() {
17525            let mut w = r.write();
17526            return Ok(if w.is_empty() {
17527                PerlValue::UNDEF
17528            } else {
17529                w.remove(0)
17530            });
17531        }
17532        if let Some(name) = arr_ref.as_array_binding_name() {
17533            return self
17534                .scope
17535                .shift_from_array(&name)
17536                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17537        }
17538        if let Some(s) = arr_ref.as_str() {
17539            if self.strict_refs {
17540                return Err(PerlError::runtime(
17541                    format!(
17542                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17543                        s
17544                    ),
17545                    line,
17546                )
17547                .into());
17548            }
17549            return self
17550                .scope
17551                .shift_from_array(&s)
17552                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17553        }
17554        Err(PerlError::runtime("shift argument is not an ARRAY reference", line).into())
17555    }
17556
17557    pub(crate) fn unshift_array_deref_multi(
17558        &mut self,
17559        arr_ref: PerlValue,
17560        vals: Vec<PerlValue>,
17561        line: usize,
17562    ) -> Result<i64, FlowOrError> {
17563        let mut flat: Vec<PerlValue> = Vec::new();
17564        for v in vals {
17565            if let Some(items) = v.as_array_vec() {
17566                flat.extend(items);
17567            } else {
17568                flat.push(v);
17569            }
17570        }
17571        if let Some(r) = arr_ref.as_array_ref() {
17572            let mut w = r.write();
17573            for (i, v) in flat.into_iter().enumerate() {
17574                w.insert(i, v);
17575            }
17576            return Ok(w.len() as i64);
17577        }
17578        if let Some(name) = arr_ref.as_array_binding_name() {
17579            let arr = self
17580                .scope
17581                .get_array_mut(&name)
17582                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17583            for (i, v) in flat.into_iter().enumerate() {
17584                arr.insert(i, v);
17585            }
17586            return Ok(arr.len() as i64);
17587        }
17588        if let Some(s) = arr_ref.as_str() {
17589            if self.strict_refs {
17590                return Err(PerlError::runtime(
17591                    format!(
17592                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17593                        s
17594                    ),
17595                    line,
17596                )
17597                .into());
17598            }
17599            let name = s.to_string();
17600            let arr = self
17601                .scope
17602                .get_array_mut(&name)
17603                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17604            for (i, v) in flat.into_iter().enumerate() {
17605                arr.insert(i, v);
17606            }
17607            return Ok(arr.len() as i64);
17608        }
17609        Err(PerlError::runtime("unshift argument is not an ARRAY reference", line).into())
17610    }
17611
17612    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
17613    /// / compiler wraps `splice` like other context-sensitive builtins).
17614    pub(crate) fn splice_array_deref(
17615        &mut self,
17616        aref: PerlValue,
17617        offset_val: PerlValue,
17618        length_val: PerlValue,
17619        rep_vals: Vec<PerlValue>,
17620        line: usize,
17621    ) -> Result<PerlValue, FlowOrError> {
17622        let ctx = self.wantarray_kind;
17623        if let Some(r) = aref.as_array_ref() {
17624            let arr_len = r.read().len();
17625            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17626            let mut w = r.write();
17627            let removed: Vec<PerlValue> = w.drain(off..end).collect();
17628            for (i, v) in rep_vals.into_iter().enumerate() {
17629                w.insert(off + i, v);
17630            }
17631            return Ok(match ctx {
17632                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
17633                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
17634            });
17635        }
17636        if let Some(name) = aref.as_array_binding_name() {
17637            let arr_len = self.scope.array_len(&name);
17638            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17639            let arr = self
17640                .scope
17641                .get_array_mut(&name)
17642                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17643            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
17644            for (i, v) in rep_vals.into_iter().enumerate() {
17645                arr.insert(off + i, v);
17646            }
17647            return Ok(match ctx {
17648                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
17649                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
17650            });
17651        }
17652        if let Some(s) = aref.as_str() {
17653            if self.strict_refs {
17654                return Err(PerlError::runtime(
17655                    format!(
17656                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17657                        s
17658                    ),
17659                    line,
17660                )
17661                .into());
17662            }
17663            let arr_len = self.scope.array_len(&s);
17664            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17665            let arr = self
17666                .scope
17667                .get_array_mut(&s)
17668                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17669            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
17670            for (i, v) in rep_vals.into_iter().enumerate() {
17671                arr.insert(off + i, v);
17672            }
17673            return Ok(match ctx {
17674                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
17675                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
17676            });
17677        }
17678        Err(PerlError::runtime("splice argument is not an ARRAY reference", line).into())
17679    }
17680
17681    pub(crate) fn eval_splice_expr(
17682        &mut self,
17683        array: &Expr,
17684        offset: Option<&Expr>,
17685        length: Option<&Expr>,
17686        replacement: &[Expr],
17687        ctx: WantarrayCtx,
17688        line: usize,
17689    ) -> Result<PerlValue, FlowOrError> {
17690        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17691            let offset_val = if let Some(o) = offset {
17692                self.eval_expr(o)?
17693            } else {
17694                PerlValue::integer(0)
17695            };
17696            let length_val = if let Some(l) = length {
17697                self.eval_expr(l)?
17698            } else {
17699                PerlValue::UNDEF
17700            };
17701            let mut rep_vals = Vec::new();
17702            for r in replacement {
17703                rep_vals.push(self.eval_expr(r)?);
17704            }
17705            let saved = self.wantarray_kind;
17706            self.wantarray_kind = ctx;
17707            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
17708            self.wantarray_kind = saved;
17709            return out;
17710        }
17711        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17712        let arr_len = self.scope.array_len(&arr_name);
17713        let offset_val = if let Some(o) = offset {
17714            self.eval_expr(o)?
17715        } else {
17716            PerlValue::integer(0)
17717        };
17718        let length_val = if let Some(l) = length {
17719            self.eval_expr(l)?
17720        } else {
17721            PerlValue::UNDEF
17722        };
17723        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17724        let mut rep_vals = Vec::new();
17725        for r in replacement {
17726            rep_vals.push(self.eval_expr(r)?);
17727        }
17728        let arr = self
17729            .scope
17730            .get_array_mut(&arr_name)
17731            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17732        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
17733        for (i, v) in rep_vals.into_iter().enumerate() {
17734            arr.insert(off + i, v);
17735        }
17736        Ok(match ctx {
17737            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
17738            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
17739        })
17740    }
17741
17742    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
17743    pub(crate) fn keys_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
17744        if let Some(h) = val.as_hash_map() {
17745            Ok(PerlValue::array(
17746                h.keys().map(|k| PerlValue::string(k.clone())).collect(),
17747            ))
17748        } else if let Some(r) = val.as_hash_ref() {
17749            Ok(PerlValue::array(
17750                r.read()
17751                    .keys()
17752                    .map(|k| PerlValue::string(k.clone()))
17753                    .collect(),
17754            ))
17755        } else {
17756            Err(PerlError::runtime("keys requires hash", line).into())
17757        }
17758    }
17759
17760    pub(crate) fn eval_keys_expr(
17761        &mut self,
17762        expr: &Expr,
17763        line: usize,
17764    ) -> Result<PerlValue, FlowOrError> {
17765        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
17766        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
17767        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
17768        Self::keys_from_value(val, line)
17769    }
17770
17771    /// Result of `values EXPR` after `EXPR` has been evaluated.
17772    pub(crate) fn values_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
17773        if let Some(h) = val.as_hash_map() {
17774            Ok(PerlValue::array(h.values().cloned().collect()))
17775        } else if let Some(r) = val.as_hash_ref() {
17776            Ok(PerlValue::array(r.read().values().cloned().collect()))
17777        } else {
17778            Err(PerlError::runtime("values requires hash", line).into())
17779        }
17780    }
17781
17782    pub(crate) fn eval_values_expr(
17783        &mut self,
17784        expr: &Expr,
17785        line: usize,
17786    ) -> Result<PerlValue, FlowOrError> {
17787        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
17788        Self::values_from_value(val, line)
17789    }
17790
17791    pub(crate) fn eval_delete_operand(
17792        &mut self,
17793        expr: &Expr,
17794        line: usize,
17795    ) -> Result<PerlValue, FlowOrError> {
17796        match &expr.kind {
17797            ExprKind::HashElement { hash, key } => {
17798                let k = self.eval_expr(key)?.to_string();
17799                self.touch_env_hash(hash);
17800                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
17801                    let class = obj
17802                        .as_blessed_ref()
17803                        .map(|b| b.class.clone())
17804                        .unwrap_or_default();
17805                    let full = format!("{}::DELETE", class);
17806                    if let Some(sub) = self.subs.get(&full).cloned() {
17807                        return self.call_sub(
17808                            &sub,
17809                            vec![obj, PerlValue::string(k)],
17810                            WantarrayCtx::Scalar,
17811                            line,
17812                        );
17813                    }
17814                }
17815                self.scope
17816                    .delete_hash_element(hash, &k)
17817                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
17818            }
17819            ExprKind::ArrayElement { array, index } => {
17820                self.check_strict_array_var(array, line)?;
17821                let idx = self.eval_expr(index)?.to_int();
17822                let aname = self.stash_array_name_for_package(array);
17823                self.scope
17824                    .delete_array_element(&aname, idx)
17825                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
17826            }
17827            ExprKind::ArrowDeref {
17828                expr: inner,
17829                index,
17830                kind: DerefKind::Hash,
17831            } => {
17832                let k = self.eval_expr(index)?.to_string();
17833                let container = self.eval_expr(inner)?;
17834                self.delete_arrow_hash_element(container, &k, line)
17835                    .map_err(Into::into)
17836            }
17837            ExprKind::ArrowDeref {
17838                expr: inner,
17839                index,
17840                kind: DerefKind::Array,
17841            } => {
17842                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
17843                    return Err(PerlError::runtime(
17844                        "delete on array element needs scalar subscript",
17845                        line,
17846                    )
17847                    .into());
17848                }
17849                let container = self.eval_expr(inner)?;
17850                let idx = self.eval_expr(index)?.to_int();
17851                self.delete_arrow_array_element(container, idx, line)
17852                    .map_err(Into::into)
17853            }
17854            _ => Err(PerlError::runtime("delete requires hash or array element", line).into()),
17855        }
17856    }
17857
17858    pub(crate) fn eval_exists_operand(
17859        &mut self,
17860        expr: &Expr,
17861        line: usize,
17862    ) -> Result<PerlValue, FlowOrError> {
17863        match &expr.kind {
17864            ExprKind::HashElement { hash, key } => {
17865                let k = self.eval_expr(key)?.to_string();
17866                self.touch_env_hash(hash);
17867                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
17868                    let class = obj
17869                        .as_blessed_ref()
17870                        .map(|b| b.class.clone())
17871                        .unwrap_or_default();
17872                    let full = format!("{}::EXISTS", class);
17873                    if let Some(sub) = self.subs.get(&full).cloned() {
17874                        return self.call_sub(
17875                            &sub,
17876                            vec![obj, PerlValue::string(k)],
17877                            WantarrayCtx::Scalar,
17878                            line,
17879                        );
17880                    }
17881                }
17882                Ok(PerlValue::integer(
17883                    if self.scope.exists_hash_element(hash, &k) {
17884                        1
17885                    } else {
17886                        0
17887                    },
17888                ))
17889            }
17890            ExprKind::ArrayElement { array, index } => {
17891                self.check_strict_array_var(array, line)?;
17892                let idx = self.eval_expr(index)?.to_int();
17893                let aname = self.stash_array_name_for_package(array);
17894                Ok(PerlValue::integer(
17895                    if self.scope.exists_array_element(&aname, idx) {
17896                        1
17897                    } else {
17898                        0
17899                    },
17900                ))
17901            }
17902            ExprKind::ArrowDeref {
17903                expr: inner,
17904                index,
17905                kind: DerefKind::Hash,
17906            } => {
17907                let k = self.eval_expr(index)?.to_string();
17908                let container = self.eval_expr(inner)?;
17909                let yes = self.exists_arrow_hash_element(container, &k, line)?;
17910                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
17911            }
17912            ExprKind::ArrowDeref {
17913                expr: inner,
17914                index,
17915                kind: DerefKind::Array,
17916            } => {
17917                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
17918                    return Err(PerlError::runtime(
17919                        "exists on array element needs scalar subscript",
17920                        line,
17921                    )
17922                    .into());
17923                }
17924                let container = self.eval_expr(inner)?;
17925                let idx = self.eval_expr(index)?.to_int();
17926                let yes = self.exists_arrow_array_element(container, idx, line)?;
17927                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
17928            }
17929            _ => Err(PerlError::runtime("exists requires hash or array element", line).into()),
17930        }
17931    }
17932
17933    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
17934    ///
17935    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
17936    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
17937    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
17938    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
17939    /// the new path amortizes the handshake across the whole map.
17940    pub(crate) fn eval_pmap_remote(
17941        &mut self,
17942        cluster_pv: PerlValue,
17943        list_pv: PerlValue,
17944        show_progress: bool,
17945        block: &Block,
17946        flat_outputs: bool,
17947        line: usize,
17948    ) -> Result<PerlValue, FlowOrError> {
17949        let Some(cluster) = cluster_pv.as_remote_cluster() else {
17950            return Err(PerlError::runtime("pmap_on: expected cluster(...) value", line).into());
17951        };
17952        let items = list_pv.to_list();
17953        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
17954        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
17955            return Err(PerlError::runtime(
17956                "pmap_on: mysync/atomic capture is not supported for remote workers",
17957                line,
17958            )
17959            .into());
17960        }
17961        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
17962            .map_err(|e| PerlError::runtime(e, line))?;
17963        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
17964        let block_src = crate::fmt::format_block(block);
17965        let item_jsons =
17966            crate::cluster::perl_items_to_json(&items).map_err(|e| PerlError::runtime(e, line))?;
17967
17968        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
17969        // synchronous from the caller's POV, so we drive the bar before/after the call.
17970        let pmap_progress = PmapProgress::new(show_progress, items.len());
17971        let result_values =
17972            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
17973                .map_err(|e| PerlError::runtime(format!("pmap_on remote: {e}"), line))?;
17974        for _ in 0..result_values.len() {
17975            pmap_progress.tick();
17976        }
17977        pmap_progress.finish();
17978
17979        if flat_outputs {
17980            let flattened: Vec<PerlValue> = result_values
17981                .into_iter()
17982                .flat_map(|v| v.map_flatten_outputs(true))
17983                .collect();
17984            Ok(PerlValue::array(flattened))
17985        } else {
17986            Ok(PerlValue::array(result_values))
17987        }
17988    }
17989
17990    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
17991    pub(crate) fn eval_par_lines_expr(
17992        &mut self,
17993        path: &Expr,
17994        callback: &Expr,
17995        progress: Option<&Expr>,
17996        line: usize,
17997    ) -> Result<PerlValue, FlowOrError> {
17998        let show_progress = progress
17999            .map(|p| self.eval_expr(p))
18000            .transpose()?
18001            .map(|v| v.is_true())
18002            .unwrap_or(false);
18003        let path_s = self.eval_expr(path)?.to_string();
18004        let cb_val = self.eval_expr(callback)?;
18005        let sub = if let Some(s) = cb_val.as_code_ref() {
18006            s
18007        } else {
18008            return Err(PerlError::runtime(
18009                "par_lines: second argument must be a code reference",
18010                line,
18011            )
18012            .into());
18013        };
18014        let subs = self.subs.clone();
18015        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18016        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
18017            FlowOrError::Error(PerlError::runtime(format!("par_lines: {}", e), line))
18018        })?;
18019        let mmap = unsafe {
18020            memmap2::Mmap::map(&file).map_err(|e| {
18021                FlowOrError::Error(PerlError::runtime(format!("par_lines: mmap: {}", e), line))
18022            })?
18023        };
18024        let data: &[u8] = &mmap;
18025        if data.is_empty() {
18026            return Ok(PerlValue::UNDEF);
18027        }
18028        let line_total = crate::par_lines::line_count_bytes(data);
18029        let pmap_progress = PmapProgress::new(show_progress, line_total);
18030        if self.num_threads == 0 {
18031            self.num_threads = rayon::current_num_threads();
18032        }
18033        let num_chunks = self.num_threads.saturating_mul(8).max(1);
18034        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
18035        chunks.into_par_iter().try_for_each(|(start, end)| {
18036            let slice = &data[start..end];
18037            let mut s = 0usize;
18038            while s < slice.len() {
18039                let e = slice[s..]
18040                    .iter()
18041                    .position(|&b| b == b'\n')
18042                    .map(|p| s + p)
18043                    .unwrap_or(slice.len());
18044                let line_bytes = &slice[s..e];
18045                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
18046                let mut local_interp = Interpreter::new();
18047                local_interp.subs = subs.clone();
18048                local_interp.scope.restore_capture(&scope_capture);
18049                local_interp
18050                    .scope
18051                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18052                local_interp.enable_parallel_guard();
18053                local_interp.scope.set_topic(PerlValue::string(line_str));
18054                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
18055                    Ok(_) => {}
18056                    Err(e) => return Err(e),
18057                }
18058                pmap_progress.tick();
18059                if e >= slice.len() {
18060                    break;
18061                }
18062                s = e + 1;
18063            }
18064            Ok(())
18065        })?;
18066        pmap_progress.finish();
18067        Ok(PerlValue::UNDEF)
18068    }
18069
18070    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
18071    pub(crate) fn eval_par_walk_expr(
18072        &mut self,
18073        path: &Expr,
18074        callback: &Expr,
18075        progress: Option<&Expr>,
18076        line: usize,
18077    ) -> Result<PerlValue, FlowOrError> {
18078        let show_progress = progress
18079            .map(|p| self.eval_expr(p))
18080            .transpose()?
18081            .map(|v| v.is_true())
18082            .unwrap_or(false);
18083        let path_val = self.eval_expr(path)?;
18084        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
18085            arr.into_iter()
18086                .map(|v| PathBuf::from(v.to_string()))
18087                .collect()
18088        } else {
18089            vec![PathBuf::from(path_val.to_string())]
18090        };
18091        let cb_val = self.eval_expr(callback)?;
18092        let sub = if let Some(s) = cb_val.as_code_ref() {
18093            s
18094        } else {
18095            return Err(PerlError::runtime(
18096                "par_walk: second argument must be a code reference",
18097                line,
18098            )
18099            .into());
18100        };
18101        let subs = self.subs.clone();
18102        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18103
18104        if show_progress {
18105            let paths = crate::par_walk::collect_paths(&roots);
18106            let pmap_progress = PmapProgress::new(true, paths.len());
18107            paths.into_par_iter().try_for_each(|p| {
18108                let s = p.to_string_lossy().into_owned();
18109                let mut local_interp = Interpreter::new();
18110                local_interp.subs = subs.clone();
18111                local_interp.scope.restore_capture(&scope_capture);
18112                local_interp
18113                    .scope
18114                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18115                local_interp.enable_parallel_guard();
18116                local_interp.scope.set_topic(PerlValue::string(s));
18117                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
18118                    Ok(_) => {}
18119                    Err(e) => return Err(e),
18120                }
18121                pmap_progress.tick();
18122                Ok(())
18123            })?;
18124            pmap_progress.finish();
18125        } else {
18126            for r in &roots {
18127                par_walk_recursive(
18128                    r.as_path(),
18129                    &sub,
18130                    &subs,
18131                    &scope_capture,
18132                    &atomic_arrays,
18133                    &atomic_hashes,
18134                    line,
18135                )?;
18136            }
18137        }
18138        Ok(PerlValue::UNDEF)
18139    }
18140
18141    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
18142    pub(crate) fn builtin_par_sed(
18143        &mut self,
18144        args: &[PerlValue],
18145        line: usize,
18146        has_progress: bool,
18147    ) -> PerlResult<PerlValue> {
18148        let show_progress = if has_progress {
18149            args.last().map(|v| v.is_true()).unwrap_or(false)
18150        } else {
18151            false
18152        };
18153        let slice = if has_progress {
18154            &args[..args.len().saturating_sub(1)]
18155        } else {
18156            args
18157        };
18158        if slice.len() < 3 {
18159            return Err(PerlError::runtime(
18160                "par_sed: need pattern, replacement, and at least one file path",
18161                line,
18162            ));
18163        }
18164        let pat_val = &slice[0];
18165        let repl = slice[1].to_string();
18166        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
18167
18168        let re = if let Some(rx) = pat_val.as_regex() {
18169            rx
18170        } else {
18171            let pattern = pat_val.to_string();
18172            match self.compile_regex(&pattern, "g", line) {
18173                Ok(r) => r,
18174                Err(FlowOrError::Error(e)) => return Err(e),
18175                Err(FlowOrError::Flow(f)) => {
18176                    return Err(PerlError::runtime(format!("par_sed: {:?}", f), line))
18177                }
18178            }
18179        };
18180
18181        let pmap = PmapProgress::new(show_progress, files.len());
18182        let touched = AtomicUsize::new(0);
18183        files.par_iter().try_for_each(|path| {
18184            let content = read_file_text_perl_compat(path)
18185                .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18186            let new_s = re.replace_all(&content, &repl);
18187            if new_s != content {
18188                std::fs::write(path, new_s.as_bytes())
18189                    .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18190                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
18191            }
18192            pmap.tick();
18193            Ok(())
18194        })?;
18195        pmap.finish();
18196        Ok(PerlValue::integer(
18197            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
18198        ))
18199    }
18200
18201    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
18202    pub(crate) fn eval_pwatch_expr(
18203        &mut self,
18204        path: &Expr,
18205        callback: &Expr,
18206        line: usize,
18207    ) -> Result<PerlValue, FlowOrError> {
18208        let pattern_s = self.eval_expr(path)?.to_string();
18209        let cb_val = self.eval_expr(callback)?;
18210        let sub = if let Some(s) = cb_val.as_code_ref() {
18211            s
18212        } else {
18213            return Err(PerlError::runtime(
18214                "pwatch: second argument must be a code reference",
18215                line,
18216            )
18217            .into());
18218        };
18219        let subs = self.subs.clone();
18220        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18221        crate::pwatch::run_pwatch(
18222            &pattern_s,
18223            sub,
18224            subs,
18225            scope_capture,
18226            atomic_arrays,
18227            atomic_hashes,
18228            line,
18229        )
18230        .map_err(FlowOrError::Error)
18231    }
18232
18233    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
18234    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
18235        let mut out = String::with_capacity(pattern.len());
18236        let chars: Vec<char> = pattern.chars().collect();
18237        let mut i = 0;
18238        while i < chars.len() {
18239            if chars[i] == '\\' && i + 1 < chars.len() {
18240                // Preserve escape sequences (including \$ which is literal $)
18241                out.push(chars[i]);
18242                out.push(chars[i + 1]);
18243                i += 2;
18244                continue;
18245            }
18246            if chars[i] == '$' && i + 1 < chars.len() {
18247                i += 1;
18248                // `$` at end of pattern is an anchor, not a variable
18249                if i >= chars.len()
18250                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
18251                {
18252                    out.push('$');
18253                    continue;
18254                }
18255                let mut name = String::new();
18256                if chars[i] == '{' {
18257                    i += 1;
18258                    while i < chars.len() && chars[i] != '}' {
18259                        name.push(chars[i]);
18260                        i += 1;
18261                    }
18262                    if i < chars.len() {
18263                        i += 1;
18264                    } // skip }
18265                } else {
18266                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18267                        name.push(chars[i]);
18268                        i += 1;
18269                    }
18270                }
18271                if !name.is_empty() {
18272                    let val = self.scope.get_scalar(&name);
18273                    out.push_str(&val.to_string());
18274                } else {
18275                    out.push('$');
18276                }
18277                continue;
18278            }
18279            out.push(chars[i]);
18280            i += 1;
18281        }
18282        out
18283    }
18284
18285    pub(crate) fn compile_regex(
18286        &mut self,
18287        pattern: &str,
18288        flags: &str,
18289        line: usize,
18290    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
18291        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
18292        let pattern = if pattern.contains('$') || pattern.contains('@') {
18293            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
18294        } else {
18295            std::borrow::Cow::Borrowed(pattern)
18296        };
18297        let pattern = pattern.as_ref();
18298        // Fast path: same regex as last call (common in loops).
18299        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
18300        let multiline = self.multiline_match;
18301        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
18302            if lp == pattern && lf == flags && *lm == multiline {
18303                return Ok(lr.clone());
18304            }
18305        }
18306        // Slow path: HashMap lookup
18307        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
18308        if let Some(cached) = self.regex_cache.get(&key) {
18309            self.regex_last = Some((
18310                pattern.to_string(),
18311                flags.to_string(),
18312                multiline,
18313                cached.clone(),
18314            ));
18315            return Ok(cached.clone());
18316        }
18317        let expanded = expand_perl_regex_quotemeta(pattern);
18318        let expanded = expand_perl_regex_octal_escapes(&expanded);
18319        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
18320        let mut re_str = String::new();
18321        if flags.contains('i') {
18322            re_str.push_str("(?i)");
18323        }
18324        if flags.contains('s') {
18325            re_str.push_str("(?s)");
18326        }
18327        if flags.contains('m') {
18328            re_str.push_str("(?m)");
18329        }
18330        if flags.contains('x') {
18331            re_str.push_str("(?x)");
18332        }
18333        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
18334        if multiline {
18335            re_str.push_str("(?s)");
18336        }
18337        re_str.push_str(&expanded);
18338        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
18339            FlowOrError::Error(PerlError::runtime(
18340                format!("Invalid regex /{}/: {}", pattern, e),
18341                line,
18342            ))
18343        })?;
18344        let arc = re;
18345        self.regex_last = Some((
18346            pattern.to_string(),
18347            flags.to_string(),
18348            multiline,
18349            arc.clone(),
18350        ));
18351        self.regex_cache.insert(key, arc.clone());
18352        Ok(arc)
18353    }
18354
18355    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
18356    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
18357        if self.last_readline_handle.is_empty() {
18358            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
18359        }
18360        let n = *self
18361            .handle_line_numbers
18362            .get(&self.last_readline_handle)
18363            .unwrap_or(&0);
18364        if n <= 0 {
18365            return None;
18366        }
18367        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
18368        {
18369            return Some(("<>".to_string(), n));
18370        }
18371        if self.last_readline_handle == "STDIN" {
18372            return Some((self.last_stdin_die_bracket.clone(), n));
18373        }
18374        Some((format!("<{}>", self.last_readline_handle), n))
18375    }
18376
18377    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
18378    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
18379        let mut s = format!(" at {} line {}", self.file, source_line);
18380        if let Some((bracket, n)) = self.die_warn_io_annotation() {
18381            s.push_str(&format!(", {} line {}.", bracket, n));
18382        } else {
18383            s.push('.');
18384        }
18385        s
18386    }
18387
18388    /// Process a line in -n/-p mode.
18389    ///
18390    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
18391    /// file so `eof` with no arguments matches Perl behavior on that line.
18392    pub fn process_line(
18393        &mut self,
18394        line_str: &str,
18395        program: &Program,
18396        is_last_input_line: bool,
18397    ) -> PerlResult<Option<String>> {
18398        self.line_mode_eof_pending = is_last_input_line;
18399        let result: PerlResult<Option<String>> = (|| {
18400            self.line_number += 1;
18401            self.scope
18402                .set_topic(PerlValue::string(line_str.to_string()));
18403
18404            if self.auto_split {
18405                let sep = self.field_separator.as_deref().unwrap_or(" ");
18406                let re = regex::Regex::new(sep).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
18407                let fields: Vec<PerlValue> = re
18408                    .split(line_str)
18409                    .map(|s| PerlValue::string(s.to_string()))
18410                    .collect();
18411                self.scope.set_array("F", fields)?;
18412            }
18413
18414            for stmt in &program.statements {
18415                match &stmt.kind {
18416                    StmtKind::SubDecl { .. }
18417                    | StmtKind::Begin(_)
18418                    | StmtKind::UnitCheck(_)
18419                    | StmtKind::Check(_)
18420                    | StmtKind::Init(_)
18421                    | StmtKind::End(_) => continue,
18422                    _ => match self.exec_statement(stmt) {
18423                        Ok(_) => {}
18424                        Err(FlowOrError::Error(e)) => return Err(e),
18425                        Err(FlowOrError::Flow(_)) => {}
18426                    },
18427                }
18428            }
18429
18430            // `-p` implicit print matches `print $_` (appends `$\` / [`Self::ors`] — set by `-l`).
18431            let mut out = self.scope.get_scalar("_").to_string();
18432            out.push_str(&self.ors);
18433            Ok(Some(out))
18434        })();
18435        self.line_mode_eof_pending = false;
18436        result
18437    }
18438}
18439
18440fn par_walk_invoke_entry(
18441    path: &Path,
18442    sub: &Arc<PerlSub>,
18443    subs: &HashMap<String, Arc<PerlSub>>,
18444    scope_capture: &[(String, PerlValue)],
18445    atomic_arrays: &[(String, crate::scope::AtomicArray)],
18446    atomic_hashes: &[(String, crate::scope::AtomicHash)],
18447    line: usize,
18448) -> Result<(), FlowOrError> {
18449    let s = path.to_string_lossy().into_owned();
18450    let mut local_interp = Interpreter::new();
18451    local_interp.subs = subs.clone();
18452    local_interp.scope.restore_capture(scope_capture);
18453    local_interp
18454        .scope
18455        .restore_atomics(atomic_arrays, atomic_hashes);
18456    local_interp.enable_parallel_guard();
18457    local_interp.scope.set_topic(PerlValue::string(s));
18458    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
18459    Ok(())
18460}
18461
18462fn par_walk_recursive(
18463    path: &Path,
18464    sub: &Arc<PerlSub>,
18465    subs: &HashMap<String, Arc<PerlSub>>,
18466    scope_capture: &[(String, PerlValue)],
18467    atomic_arrays: &[(String, crate::scope::AtomicArray)],
18468    atomic_hashes: &[(String, crate::scope::AtomicHash)],
18469    line: usize,
18470) -> Result<(), FlowOrError> {
18471    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
18472        return par_walk_invoke_entry(
18473            path,
18474            sub,
18475            subs,
18476            scope_capture,
18477            atomic_arrays,
18478            atomic_hashes,
18479            line,
18480        );
18481    }
18482    if !path.is_dir() {
18483        return Ok(());
18484    }
18485    par_walk_invoke_entry(
18486        path,
18487        sub,
18488        subs,
18489        scope_capture,
18490        atomic_arrays,
18491        atomic_hashes,
18492        line,
18493    )?;
18494    let read = match std::fs::read_dir(path) {
18495        Ok(r) => r,
18496        Err(_) => return Ok(()),
18497    };
18498    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
18499    entries.par_iter().try_for_each(|e| {
18500        par_walk_recursive(
18501            &e.path(),
18502            sub,
18503            subs,
18504            scope_capture,
18505            atomic_arrays,
18506            atomic_hashes,
18507            line,
18508        )
18509    })?;
18510    Ok(())
18511}
18512
18513/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
18514pub(crate) fn perl_sprintf_format_with<F>(
18515    fmt: &str,
18516    args: &[PerlValue],
18517    mut string_for_s: F,
18518) -> Result<String, FlowOrError>
18519where
18520    F: FnMut(&PerlValue) -> Result<String, FlowOrError>,
18521{
18522    let mut result = String::new();
18523    let mut arg_idx = 0;
18524    let chars: Vec<char> = fmt.chars().collect();
18525    let mut i = 0;
18526
18527    while i < chars.len() {
18528        if chars[i] == '%' {
18529            i += 1;
18530            if i >= chars.len() {
18531                break;
18532            }
18533            if chars[i] == '%' {
18534                result.push('%');
18535                i += 1;
18536                continue;
18537            }
18538
18539            // Parse format specifier
18540            let mut flags = String::new();
18541            while i < chars.len() && "-+ #0".contains(chars[i]) {
18542                flags.push(chars[i]);
18543                i += 1;
18544            }
18545            let mut width = String::new();
18546            while i < chars.len() && chars[i].is_ascii_digit() {
18547                width.push(chars[i]);
18548                i += 1;
18549            }
18550            let mut precision = String::new();
18551            if i < chars.len() && chars[i] == '.' {
18552                i += 1;
18553                while i < chars.len() && chars[i].is_ascii_digit() {
18554                    precision.push(chars[i]);
18555                    i += 1;
18556                }
18557            }
18558            if i >= chars.len() {
18559                break;
18560            }
18561            let spec = chars[i];
18562            i += 1;
18563
18564            let arg = args.get(arg_idx).cloned().unwrap_or(PerlValue::UNDEF);
18565            arg_idx += 1;
18566
18567            let w: usize = width.parse().unwrap_or(0);
18568            let p: usize = precision.parse().unwrap_or(6);
18569
18570            let zero_pad = flags.contains('0') && !flags.contains('-');
18571            let left_align = flags.contains('-');
18572            let formatted = match spec {
18573                'd' | 'i' => {
18574                    if zero_pad {
18575                        format!("{:0width$}", arg.to_int(), width = w)
18576                    } else if left_align {
18577                        format!("{:<width$}", arg.to_int(), width = w)
18578                    } else {
18579                        format!("{:width$}", arg.to_int(), width = w)
18580                    }
18581                }
18582                'u' => {
18583                    if zero_pad {
18584                        format!("{:0width$}", arg.to_int() as u64, width = w)
18585                    } else {
18586                        format!("{:width$}", arg.to_int() as u64, width = w)
18587                    }
18588                }
18589                'f' => format!("{:width$.prec$}", arg.to_number(), width = w, prec = p),
18590                'e' => format!("{:width$.prec$e}", arg.to_number(), width = w, prec = p),
18591                'g' => {
18592                    let n = arg.to_number();
18593                    if n.abs() >= 1e-4 && n.abs() < 1e15 {
18594                        format!("{:width$.prec$}", n, width = w, prec = p)
18595                    } else {
18596                        format!("{:width$.prec$e}", n, width = w, prec = p)
18597                    }
18598                }
18599                's' => {
18600                    let s = string_for_s(&arg)?;
18601                    if !precision.is_empty() {
18602                        let truncated: String = s.chars().take(p).collect();
18603                        if flags.contains('-') {
18604                            format!("{:<width$}", truncated, width = w)
18605                        } else {
18606                            format!("{:>width$}", truncated, width = w)
18607                        }
18608                    } else if flags.contains('-') {
18609                        format!("{:<width$}", s, width = w)
18610                    } else {
18611                        format!("{:>width$}", s, width = w)
18612                    }
18613                }
18614                'x' => {
18615                    let v = arg.to_int();
18616                    if zero_pad && w > 0 {
18617                        format!("{:0width$x}", v, width = w)
18618                    } else if left_align {
18619                        format!("{:<width$x}", v, width = w)
18620                    } else if w > 0 {
18621                        format!("{:width$x}", v, width = w)
18622                    } else {
18623                        format!("{:x}", v)
18624                    }
18625                }
18626                'X' => {
18627                    let v = arg.to_int();
18628                    if zero_pad && w > 0 {
18629                        format!("{:0width$X}", v, width = w)
18630                    } else if left_align {
18631                        format!("{:<width$X}", v, width = w)
18632                    } else if w > 0 {
18633                        format!("{:width$X}", v, width = w)
18634                    } else {
18635                        format!("{:X}", v)
18636                    }
18637                }
18638                'o' => {
18639                    let v = arg.to_int();
18640                    if zero_pad && w > 0 {
18641                        format!("{:0width$o}", v, width = w)
18642                    } else if left_align {
18643                        format!("{:<width$o}", v, width = w)
18644                    } else if w > 0 {
18645                        format!("{:width$o}", v, width = w)
18646                    } else {
18647                        format!("{:o}", v)
18648                    }
18649                }
18650                'b' => {
18651                    let v = arg.to_int();
18652                    if zero_pad && w > 0 {
18653                        format!("{:0width$b}", v, width = w)
18654                    } else if left_align {
18655                        format!("{:<width$b}", v, width = w)
18656                    } else if w > 0 {
18657                        format!("{:width$b}", v, width = w)
18658                    } else {
18659                        format!("{:b}", v)
18660                    }
18661                }
18662                'c' => char::from_u32(arg.to_int() as u32)
18663                    .map(|c| c.to_string())
18664                    .unwrap_or_default(),
18665                _ => arg.to_string(),
18666            };
18667
18668            result.push_str(&formatted);
18669        } else {
18670            result.push(chars[i]);
18671            i += 1;
18672        }
18673    }
18674    Ok(result)
18675}
18676
18677#[cfg(test)]
18678mod regex_expand_tests {
18679    use super::Interpreter;
18680
18681    #[test]
18682    fn compile_regex_quotemeta_qe_matches_literal() {
18683        let mut i = Interpreter::new();
18684        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
18685        assert!(re.is_match("a.c"));
18686        assert!(!re.is_match("abc"));
18687    }
18688
18689    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
18690    /// stay literal (not rewritten to `(?:\n?\z)`).
18691    #[test]
18692    fn compile_regex_char_class_leading_close_bracket_is_literal() {
18693        let mut i = Interpreter::new();
18694        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
18695        assert!(re.is_match("$"));
18696        assert!(re.is_match("]"));
18697        assert!(!re.is_match("x"));
18698    }
18699}
18700
18701#[cfg(test)]
18702mod special_scalar_name_tests {
18703    use super::Interpreter;
18704
18705    #[test]
18706    fn special_scalar_name_for_get_matches_magic_globals() {
18707        assert!(Interpreter::is_special_scalar_name_for_get("0"));
18708        assert!(Interpreter::is_special_scalar_name_for_get("!"));
18709        assert!(Interpreter::is_special_scalar_name_for_get("^W"));
18710        assert!(Interpreter::is_special_scalar_name_for_get("^O"));
18711        assert!(Interpreter::is_special_scalar_name_for_get("^MATCH"));
18712        assert!(Interpreter::is_special_scalar_name_for_get("<"));
18713        assert!(Interpreter::is_special_scalar_name_for_get("?"));
18714        assert!(Interpreter::is_special_scalar_name_for_get("|"));
18715        assert!(Interpreter::is_special_scalar_name_for_get("^UNICODE"));
18716        assert!(Interpreter::is_special_scalar_name_for_get("\""));
18717        assert!(!Interpreter::is_special_scalar_name_for_get("foo"));
18718        assert!(!Interpreter::is_special_scalar_name_for_get("plainvar"));
18719    }
18720
18721    #[test]
18722    fn special_scalar_name_for_set_matches_set_special_var_arms() {
18723        assert!(Interpreter::is_special_scalar_name_for_set("0"));
18724        assert!(Interpreter::is_special_scalar_name_for_set("^D"));
18725        assert!(Interpreter::is_special_scalar_name_for_set("^H"));
18726        assert!(Interpreter::is_special_scalar_name_for_set("^WARNING_BITS"));
18727        assert!(Interpreter::is_special_scalar_name_for_set("ARGV"));
18728        assert!(Interpreter::is_special_scalar_name_for_set("|"));
18729        assert!(Interpreter::is_special_scalar_name_for_set("?"));
18730        assert!(Interpreter::is_special_scalar_name_for_set("^UNICODE"));
18731        assert!(Interpreter::is_special_scalar_name_for_set("."));
18732        assert!(!Interpreter::is_special_scalar_name_for_set("foo"));
18733        assert!(!Interpreter::is_special_scalar_name_for_set("__PACKAGE__"));
18734    }
18735
18736    #[test]
18737    fn caret_and_id_specials_roundtrip_get() {
18738        let i = Interpreter::new();
18739        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
18740        assert_eq!(
18741            i.get_special_var("^V").to_string(),
18742            format!("v{}", env!("CARGO_PKG_VERSION"))
18743        );
18744        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
18745        assert!(i.get_special_var("^T").to_int() >= 0);
18746        #[cfg(unix)]
18747        {
18748            assert!(i.get_special_var("<").to_int() >= 0);
18749        }
18750    }
18751
18752    #[test]
18753    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
18754        let mut i = Interpreter::new();
18755        i.last_readline_handle.clear();
18756        i.line_number = 3;
18757        i.prepare_flip_flop_vm_slots(1);
18758        assert_eq!(
18759            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
18760            1
18761        );
18762        assert!(i.flip_flop_active[0]);
18763        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
18764        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
18765        assert_eq!(
18766            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
18767            1
18768        );
18769        assert!(i.flip_flop_active[0]);
18770    }
18771
18772    #[test]
18773    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
18774        let mut i = Interpreter::new();
18775        i.last_readline_handle.clear();
18776        i.line_number = 2;
18777        i.prepare_flip_flop_vm_slots(1);
18778        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
18779        assert!(i.flip_flop_active[0]);
18780        i.line_number = 3;
18781        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
18782        assert!(!i.flip_flop_active[0]);
18783        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
18784    }
18785}