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        ExprKind::List(_) => WantarrayCtx::List,
380        _ => WantarrayCtx::Scalar,
381    }
382}
383
384/// Memoized inputs + result for a non-`g` `regex_match_execute` call. Populated on every
385/// successful match and consulted at the top of the next call; on exact-match (same pattern,
386/// flags, multiline, and haystack content) we skip regex execution + capture-var scope population
387/// entirely, replaying the stored `PerlValue` result. See [`Interpreter::regex_match_memo`].
388#[derive(Clone)]
389pub(crate) struct RegexMatchMemo {
390    pub pattern: String,
391    pub flags: String,
392    pub multiline: bool,
393    pub haystack: String,
394    pub result: PerlValue,
395}
396
397/// Tree-walker state for scalar `..` / `...` (key: `Expr` address).
398#[derive(Clone, Copy, Default)]
399struct FlipFlopTreeState {
400    active: bool,
401    /// Exclusive `...`: `$.` line where the left bound matched — right is only tested when `$.` is
402    /// strictly greater (Perl: do not test the right operand until the next evaluation; for numeric
403    /// `$.` that defers past the left-match line, including multiple evals on that line).
404    exclusive_left_line: Option<i64>,
405}
406
407/// `BufReader` / `print` / `sysread` / `tell` on the same handle share this [`File`] cursor.
408#[derive(Clone)]
409pub(crate) struct IoSharedFile(pub Arc<Mutex<File>>);
410
411impl Read for IoSharedFile {
412    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
413        self.0.lock().read(buf)
414    }
415}
416
417pub(crate) struct IoSharedFileWrite(pub Arc<Mutex<File>>);
418
419impl IoWrite for IoSharedFileWrite {
420    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
421        self.0.lock().write(buf)
422    }
423
424    fn flush(&mut self) -> io::Result<()> {
425        self.0.lock().flush()
426    }
427}
428
429pub struct Interpreter {
430    pub scope: Scope,
431    pub(crate) subs: HashMap<String, Arc<PerlSub>>,
432    pub(crate) file: String,
433    /// File handles: name → writer
434    pub(crate) output_handles: HashMap<String, Box<dyn IoWrite + Send>>,
435    pub(crate) input_handles: HashMap<String, BufReader<Box<dyn Read + Send>>>,
436    /// Output separator ($,)
437    pub ofs: String,
438    /// Output record separator ($\)
439    pub ors: String,
440    /// Input record separator (`$/`). `None` represents undef (slurp mode in `<>`).
441    /// Default at startup: `Some("\n")`. `local $/` (no init) sets `None`.
442    pub irs: Option<String>,
443    /// $! — last OS error
444    pub errno: String,
445    /// Numeric errno for `$!` dualvar (`raw_os_error()`), `0` when unset.
446    pub errno_code: i32,
447    /// $@ — last eval error (string)
448    pub eval_error: String,
449    /// Numeric side of `$@` dualvar (`0` when cleared; `1` for typical exception strings; or explicit code from assignment / dualvar).
450    pub eval_error_code: i32,
451    /// When `die` is called with a ref argument, the ref value is preserved here.
452    pub eval_error_value: Option<PerlValue>,
453    /// @ARGV
454    pub argv: Vec<String>,
455    /// %ENV (mirrors `scope` hash `"ENV"` after [`Self::materialize_env_if_needed`])
456    pub env: IndexMap<String, PerlValue>,
457    /// False until first [`Self::materialize_env_if_needed`] (defers `std::env::vars()` cost).
458    pub env_materialized: bool,
459    /// $0
460    pub program_name: String,
461    /// Current line number $. (global increment; see `handle_line_numbers` for per-handle)
462    pub line_number: i64,
463    /// Last handle key used for `$.` (e.g. `STDIN`, `FH`, `ARGV:path`).
464    pub last_readline_handle: String,
465    /// Bracket text for `die` / `warn` after a stdin read: `"<>"` (diamond / `-n` queue) vs `"<STDIN>"`.
466    pub(crate) last_stdin_die_bracket: String,
467    /// Line count per handle for `$.` when keyed (Perl-style last-read handle).
468    pub handle_line_numbers: HashMap<String, i64>,
469    /// Scalar and regex `..` / `...` flip-flop state for bytecode ([`crate::bytecode::Op::ScalarFlipFlop`],
470    /// [`crate::bytecode::Op::RegexFlipFlop`], [`crate::bytecode::Op::RegexEofFlipFlop`],
471    /// [`crate::bytecode::Op::RegexFlipFlopExprRhs`]).
472    pub(crate) flip_flop_active: Vec<bool>,
473    /// Exclusive `...`: parallel to [`Self::flip_flop_active`] — `Some($. )` where the left bound
474    /// matched; right is only compared when `$.` is strictly greater (see [`FlipFlopTreeState`]).
475    pub(crate) flip_flop_exclusive_left_line: Vec<Option<i64>>,
476    /// Running match counter for each scalar flip-flop slot — emitted as the *value* of a
477    /// scalar `..`/`...` range (`"1"`, `"2"`, …, trailing `"E0"` on the exclusive close line)
478    /// so `my $x = 1..5` matches Perl's stringification rather than returning a plain integer.
479    pub(crate) flip_flop_sequence: Vec<i64>,
480    /// Last `$.` seen for each slot so scalar flip-flop `seq` increments once per line, not
481    /// per re-evaluation on the same `$.` (matches Perl `pp_flop`: two evaluations of the same
482    /// range on one line return the same sequence number).
483    pub(crate) flip_flop_last_dot: Vec<Option<i64>>,
484    /// Scalar `..` / `...` flip-flop for tree-walker (key: `Expr` address).
485    flip_flop_tree: HashMap<usize, FlipFlopTreeState>,
486    /// `$^C` — set when SIGINT is pending before handler runs (cleared on read).
487    pub sigint_pending_caret: Cell<bool>,
488    /// Auto-split mode (-a)
489    pub auto_split: bool,
490    /// Field separator for -F
491    pub field_separator: Option<String>,
492    /// BEGIN blocks
493    begin_blocks: Vec<Block>,
494    /// `UNITCHECK` blocks (LIFO at run)
495    unit_check_blocks: Vec<Block>,
496    /// `CHECK` blocks (LIFO at run)
497    check_blocks: Vec<Block>,
498    /// `INIT` blocks (FIFO at run)
499    init_blocks: Vec<Block>,
500    /// END blocks
501    end_blocks: Vec<Block>,
502    /// -w warnings / `use warnings` / `$^W`
503    pub warnings: bool,
504    /// Output autoflush (`$|`).
505    pub output_autoflush: bool,
506    /// Default handle for `print` / `say` / `printf` with no explicit handle (`select FH` sets this).
507    pub default_print_handle: String,
508    /// Suppress stdout output (fan workers with progress bars).
509    pub suppress_stdout: bool,
510    /// Child wait status (`$?`) — POSIX-style (exit code in high byte, etc.).
511    pub child_exit_status: i64,
512    /// Last successful match (`$&`, `${^MATCH}`).
513    pub last_match: String,
514    /// Before match (`` $` ``, `${^PREMATCH}`).
515    pub prematch: String,
516    /// After match (`$'`, `${^POSTMATCH}`).
517    pub postmatch: String,
518    /// Last bracket match (`$+`, `${^LAST_SUBMATCH_RESULT}`).
519    pub last_paren_match: String,
520    /// List separator for array stringification in concatenation / interpolation (`$"`).
521    pub list_separator: String,
522    /// Script start time (`$^T`) — seconds since Unix epoch.
523    pub script_start_time: i64,
524    /// `$^H` — compile-time hints (bit flags; pragma / `BEGIN` may update).
525    pub compile_hints: i64,
526    /// `${^WARNING_BITS}` — warnings bitmask (Perl internal; surfaced for compatibility).
527    pub warning_bits: i64,
528    /// `${^GLOBAL_PHASE}` — interpreter phase (`RUN`, …).
529    pub global_phase: String,
530    /// `$;` — hash subscript separator (multi-key join); Perl default `\034`.
531    pub subscript_sep: String,
532    /// `$^I` — in-place edit backup suffix (empty when no backup; also unset when `-i` was not passed).
533    /// The `stryke` driver sets this from `-i` / `-i.ext`.
534    pub inplace_edit: String,
535    /// `$^D` — debugging flags (integer; mostly ignored).
536    pub debug_flags: i64,
537    /// `$^P` — debugging / profiling flags (integer; mostly ignored).
538    pub perl_debug_flags: i64,
539    /// Nesting depth for `eval` / `evalblock` (`$^S` is non-zero while inside eval).
540    pub eval_nesting: u32,
541    /// `$ARGV` — name of the file last opened by `<>` (empty for stdin or before first file).
542    pub argv_current_file: String,
543    /// Next `@ARGV` index to open for `<>` (after `ARGV` is exhausted, `<>` returns undef).
544    pub(crate) diamond_next_idx: usize,
545    /// Buffered reader for the current `<>` file (stdin uses the existing stdin path).
546    pub(crate) diamond_reader: Option<BufReader<File>>,
547    /// `use strict` / `use strict 'refs'` / `qw(refs subs vars)` (Perl names).
548    pub strict_refs: bool,
549    pub strict_subs: bool,
550    pub strict_vars: bool,
551    /// `use utf8` — source is UTF-8 (reserved for future lexer/string semantics).
552    pub utf8_pragma: bool,
553    /// `use open ':encoding(UTF-8)'` / `qw(:std :encoding(UTF-8))` / `:utf8` — readline uses UTF-8 lossy decode.
554    pub open_pragma_utf8: bool,
555    /// `use feature` — bit flags (`FEAT_*`).
556    pub feature_bits: u64,
557    /// Number of parallel threads
558    pub num_threads: usize,
559    /// Compiled regex cache: "flags///pattern" → [`PerlCompiledRegex`] (Rust `regex` or `fancy-regex`).
560    regex_cache: HashMap<String, Arc<PerlCompiledRegex>>,
561    /// Last compiled regex — fast-path to avoid format! + HashMap lookup in tight loops.
562    /// Third flag: `$*` multiline (prepends `(?s)` when true).
563    regex_last: Option<(String, String, bool, Arc<PerlCompiledRegex>)>,
564    /// Memo of the most-recent match's inputs and result for `regex_match_execute` (non-`g`,
565    /// non-`scalar_g` path). Hot loops that re-match the same text against the same pattern
566    /// (e.g. `while (...) { $text =~ /p/ }`) skip the regex execution AND the capture-variable
567    /// scope population entirely on cache hit.
568    ///
569    /// Invalidation: any VM write to a capture variable (`$&`, `` $` ``, `$'`, `$+`, `$1`..`$9`,
570    /// `@-`, `@+`, `%+`) clears the "scope still in sync" flag. The memo survives; only the
571    /// capture-var side-effect replay is forced on the next hit.
572    regex_match_memo: Option<RegexMatchMemo>,
573    /// False when the user (or some non-regex code path) has written to one of the capture
574    /// variables since the last `apply_regex_captures` call. The memoized match result is still
575    /// valid, but the scope side effects need to be reapplied on the next hit.
576    regex_capture_scope_fresh: bool,
577    /// Offsets for Perl `m//g` in scalar context (`pos`), keyed by scalar name (`"_"` for `$_`).
578    pub(crate) regex_pos: HashMap<String, Option<usize>>,
579    /// Persistent storage for `state` variables, keyed by "line:name".
580    pub(crate) state_vars: HashMap<String, PerlValue>,
581    /// Per-frame tracking of state variable bindings: (var_name, state_key).
582    state_bindings_stack: Vec<Vec<(String, String)>>,
583    /// PRNG for `rand` / `srand` (matches Perl-style seeding, not crypto).
584    pub(crate) rand_rng: StdRng,
585    /// Directory handles from `opendir`: name → snapshot + read cursor (`readdir` / `rewinddir` / …).
586    pub(crate) dir_handles: HashMap<String, DirHandleState>,
587    /// Raw `File` per handle (shared with buffered input / `print` / `sys*`) so `tell` matches writes.
588    pub(crate) io_file_slots: HashMap<String, Arc<Mutex<File>>>,
589    /// Child processes for `open(H, "-|", cmd)` / `open(H, "|-", cmd)`; waited on `close`.
590    pub(crate) pipe_children: HashMap<String, Child>,
591    /// Sockets from `socket` / `accept` / `connect`.
592    pub(crate) socket_handles: HashMap<String, PerlSocket>,
593    /// `wantarray()` inside the current subroutine (`WantarrayCtx`; VM threads it on `Call`/`MethodCall`/`ArrowCall`).
594    pub(crate) wantarray_kind: WantarrayCtx,
595    /// `struct Name { ... }` definitions (merged from VM chunks and tree-walker).
596    pub struct_defs: HashMap<String, Arc<StructDef>>,
597    /// `enum Name { ... }` definitions (merged from VM chunks and tree-walker).
598    pub enum_defs: HashMap<String, Arc<EnumDef>>,
599    /// `class Name extends ... impl ... { ... }` definitions.
600    pub class_defs: HashMap<String, Arc<ClassDef>>,
601    /// `trait Name { ... }` definitions.
602    pub trait_defs: HashMap<String, Arc<TraitDef>>,
603    /// When set, `stryke --profile` records timings: VM path uses per-opcode line samples and sub
604    /// call/return (JIT disabled); tree-walker fallback uses per-statement lines and subs.
605    pub profiler: Option<Profiler>,
606    /// Per-module `our @EXPORT` / `our @EXPORT_OK` (Exporter-style). Absent key → legacy import-all.
607    pub(crate) module_export_lists: HashMap<String, ModuleExportLists>,
608    /// Virtual modules: path → source (for AOT bundles). Checked before filesystem in `require`.
609    pub(crate) virtual_modules: HashMap<String, String>,
610    /// `tie %name, ...` — object that implements FETCH/STORE for that hash.
611    pub(crate) tied_hashes: HashMap<String, PerlValue>,
612    /// `tie $name` — TIESCALAR object for FETCH/STORE.
613    pub(crate) tied_scalars: HashMap<String, PerlValue>,
614    /// `tie @name` — TIEARRAY object for FETCH/STORE (indexed).
615    pub(crate) tied_arrays: HashMap<String, PerlValue>,
616    /// `use overload` — class → Perl overload key → short method name in that package.
617    pub(crate) overload_table: HashMap<String, HashMap<String, String>>,
618    /// `format NAME =` bodies (parsed) keyed `Package::NAME`.
619    pub(crate) format_templates: HashMap<String, Arc<crate::format::FormatTemplate>>,
620    /// `${^NAME}` scalars not stored in dedicated fields (default `undef`; assign may stash).
621    pub(crate) special_caret_scalars: HashMap<String, PerlValue>,
622    /// `$%` — format output page number.
623    pub format_page_number: i64,
624    /// `$=` — format lines per page.
625    pub format_lines_per_page: i64,
626    /// `$-` — lines remaining on format page.
627    pub format_lines_left: i64,
628    /// `$:` — characters to break format lines (Perl default `\n`).
629    pub format_line_break_chars: String,
630    /// `$^` — top-of-form format name.
631    pub format_top_name: String,
632    /// `$^A` — format write accumulator.
633    pub accumulator_format: String,
634    /// `$^F` — max system file descriptor (Perl default 2).
635    pub max_system_fd: i64,
636    /// `$^M` — emergency memory buffer (no-op pool in stryke).
637    pub emergency_memory: String,
638    /// `$^N` — last opened named regexp capture name.
639    pub last_subpattern_name: String,
640    /// `$INC` — `@INC` hook iterator (Perl 5.37+).
641    pub inc_hook_index: i64,
642    /// `$*` — multiline matching (deprecated in Perl); when true, `compile_regex` prepends `(?s)`.
643    pub multiline_match: bool,
644    /// `$^X` — path to this executable (cached).
645    pub executable_path: String,
646    /// `$^L` — formfeed string for formats (Perl default `\f`).
647    pub formfeed_string: String,
648    /// Limited typeglob: I/O handle alias (`*FOO` → underlying handle name).
649    pub(crate) glob_handle_alias: HashMap<String, String>,
650    /// Parallel to [`Scope`] frames: `local *GLOB` entries to restore on [`Self::scope_pop_hook`].
651    glob_restore_frames: Vec<Vec<(String, Option<String>)>>,
652    /// `local` saves of special-variable backing fields (`$/`, `$\`, `$,`, `$"`, …).
653    /// Mirrors `glob_restore_frames`: one Vec per scope frame; on `scope_pop_hook` each
654    /// `(name, old_value)` is replayed via `set_special_var` so the underlying interpreter
655    /// state (`self.irs` / `self.ofs` / etc.) restores when a `{ local $X = … }` block exits.
656    pub(crate) special_var_restore_frames: Vec<Vec<(String, PerlValue)>>,
657    /// `use English` — long names ([`crate::english::scalar_alias`]) map to short special scalars.
658    /// Lazy-init flag: reflection hashes (`%b`, `%stryke::builtins`, etc.)
659    /// are only built on first access to avoid startup cost.
660    pub(crate) reflection_hashes_ready: bool,
661    pub(crate) english_enabled: bool,
662    /// `use English qw(-no_match_vars)` — suppress `$MATCH`/`$PREMATCH`/`$POSTMATCH` aliases.
663    pub(crate) english_no_match_vars: bool,
664    /// Once `use English` (without `-no_match_vars`) has activated match vars, they stay
665    /// available for the rest of the program — Perl exports them into the caller's namespace
666    /// and later `no English` / `use English qw(-no_match_vars)` cannot un-export them.
667    pub(crate) english_match_vars_ever_enabled: bool,
668    /// Lexical scalar names (`my`/`our`/`foreach`/`given`/`match`/`try` catch) per scope frame (parallel to [`Scope`] depth).
669    english_lexical_scalars: Vec<HashSet<String>>,
670    /// Bare names from `our $x` per frame — same length as [`Self::english_lexical_scalars`].
671    our_lexical_scalars: Vec<HashSet<String>>,
672    /// When false, the bytecode VM runs without Cranelift (see [`crate::try_vm_execute`]). Disabled by
673    /// `STRYKE_NO_JIT=1` / `true` / `yes`, or `stryke --no-jit` after [`Self::new`].
674    pub vm_jit_enabled: bool,
675    /// When true, [`crate::try_vm_execute`] prints bytecode disassembly to stderr before running the VM.
676    pub disasm_bytecode: bool,
677    /// Sideband: precompiled [`crate::bytecode::Chunk`] loaded from a `.pec` cache hit. When
678    /// `Some`, [`crate::try_vm_execute`] uses it directly and skips `compile_program`. Consumed
679    /// (`.take()`) on first read so re-entry compiles normally.
680    pub pec_precompiled_chunk: Option<crate::bytecode::Chunk>,
681    /// Sideband: fingerprint to save the compiled chunk under after a cache miss (pairs with
682    /// [`crate::pec::try_save`]). `None` when the cache is disabled or the caller does not want
683    /// the compiled chunk persisted.
684    pub pec_cache_fingerprint: Option<[u8; 32]>,
685    /// Set while stepping a `gen { }` body (`yield`).
686    pub(crate) in_generator: bool,
687    /// `-n`/`-p` driver: prelude only in [`Self::execute_tree`]; body runs in [`Self::process_line`].
688    pub line_mode_skip_main: bool,
689    /// Set for the duration of each [`Self::process_line`] call when the current line is the last
690    /// from the active input source (stdin or current `@ARGV` file), so `eof` with no arguments
691    /// matches Perl (true on the last line of that source).
692    pub(crate) line_mode_eof_pending: bool,
693    /// `-n`/`-p` stdin driver: lines **peek-read** to compute `eof` / `is_last` are pushed here so
694    /// `<>` / `readline` in the body reads them before the real stdin stream (Perl shares one fd).
695    pub line_mode_stdin_pending: VecDeque<String>,
696    /// Sliding-window timestamps for `rate_limit(...)` (indexed by parse-time slot).
697    pub(crate) rate_limit_slots: Vec<VecDeque<Instant>>,
698    /// `log_level('…')` override; when `None`, use `%ENV{LOG_LEVEL}` (default `info`).
699    pub(crate) log_level_override: Option<LogLevelFilter>,
700    /// Stack of currently-executing subroutines for `__SUB__` (anonymous recursion).
701    /// Pushed on `call_sub` entry, popped on exit.
702    pub(crate) current_sub_stack: Vec<Arc<PerlSub>>,
703    /// Interactive debugger state (`-d` flag).
704    pub debugger: Option<crate::debugger::Debugger>,
705    /// Call stack for debugger: (sub_name, call_line).
706    pub(crate) debug_call_stack: Vec<(String, usize)>,
707}
708
709/// Snapshot of stash + `@ISA` for REPL `$obj->method` tab-completion (no `Interpreter` handle needed).
710#[derive(Debug, Clone, Default)]
711pub struct ReplCompletionSnapshot {
712    pub subs: Vec<String>,
713    pub blessed_scalars: HashMap<String, String>,
714    pub isa_for_class: HashMap<String, Vec<String>>,
715}
716
717impl ReplCompletionSnapshot {
718    /// Method names (short names) visible for `class->` from [`Self::subs`] and C3 MRO.
719    pub fn methods_for_class(&self, class: &str) -> Vec<String> {
720        let parents = |c: &str| self.isa_for_class.get(c).cloned().unwrap_or_default();
721        let mro = linearize_c3(class, &parents, 0);
722        let mut names = HashSet::new();
723        for pkg in &mro {
724            if pkg == "UNIVERSAL" {
725                continue;
726            }
727            let prefix = format!("{}::", pkg);
728            for k in &self.subs {
729                if k.starts_with(&prefix) {
730                    let rest = &k[prefix.len()..];
731                    if !rest.contains("::") {
732                        names.insert(rest.to_string());
733                    }
734                }
735            }
736        }
737        for k in &self.subs {
738            if let Some(rest) = k.strip_prefix("UNIVERSAL::") {
739                if !rest.contains("::") {
740                    names.insert(rest.to_string());
741                }
742            }
743        }
744        let mut v: Vec<String> = names.into_iter().collect();
745        v.sort();
746        v
747    }
748}
749
750fn repl_resolve_class_for_arrow(state: &ReplCompletionSnapshot, left: &str) -> Option<String> {
751    let left = left.trim_end();
752    if left.is_empty() {
753        return None;
754    }
755    if let Some(i) = left.rfind('$') {
756        let name = left[i + 1..].trim();
757        if name.chars().all(|c| c.is_alphanumeric() || c == '_') && !name.is_empty() {
758            return state.blessed_scalars.get(name).cloned();
759        }
760    }
761    let tok = left.split_whitespace().last()?;
762    if tok.contains("::") {
763        return Some(tok.to_string());
764    }
765    if tok.chars().all(|c| c.is_alphanumeric() || c == '_') && !tok.starts_with('$') {
766        return Some(tok.to_string());
767    }
768    None
769}
770
771/// Tab-complete method name after `->` when the invocant resolves to a class (see [`ReplCompletionSnapshot`]).
772pub fn repl_arrow_method_completions(
773    state: &ReplCompletionSnapshot,
774    line: &str,
775    pos: usize,
776) -> Option<(usize, Vec<String>)> {
777    let pos = pos.min(line.len());
778    let before = &line[..pos];
779    let arrow_idx = before.rfind("->")?;
780    let after_arrow = &before[arrow_idx + 2..];
781    let rest = after_arrow.trim_start();
782    let ws_len = after_arrow.len() - rest.len();
783    let method_start = arrow_idx + 2 + ws_len;
784    let method_prefix = &line[method_start..pos];
785    if !method_prefix
786        .chars()
787        .all(|c| c.is_alphanumeric() || c == '_')
788    {
789        return None;
790    }
791    let left = line[..arrow_idx].trim_end();
792    let class = repl_resolve_class_for_arrow(state, left)?;
793    let mut methods = state.methods_for_class(&class);
794    methods.retain(|m| m.starts_with(method_prefix));
795    Some((method_start, methods))
796}
797
798/// `Exporter`-style lists for `use Module` / `use Module qw(...)`.
799#[derive(Debug, Clone, Default)]
800pub(crate) struct ModuleExportLists {
801    /// Default imports for `use Module` with no list.
802    pub export: Vec<String>,
803    /// Extra symbols allowed in `use Module qw(name)`.
804    pub export_ok: Vec<String>,
805}
806
807/// Shell command for `open(H, "-|", cmd)` / `open(H, "|-", cmd)` (list form not yet supported).
808fn piped_shell_command(cmd: &str) -> Command {
809    if cfg!(windows) {
810        let mut c = Command::new("cmd");
811        c.arg("/C").arg(cmd);
812        c
813    } else {
814        let mut c = Command::new("sh");
815        c.arg("-c").arg(cmd);
816        c
817    }
818}
819
820/// Expands Perl `\Q...\E` spans to escaped text for the Rust [`regex`] crate.
821/// Convert Perl octal escapes (`\0`, `\00`, `\000`, `\012`, etc.) to `\xHH`
822/// so the Rust `regex` crate can match them.
823/// Convert Perl octal escapes starting with `\0` (e.g. `\0`, `\012`, `\077`) to `\xHH`
824/// so the Rust regex crate can match NUL and other octal-specified bytes.
825/// Only `\0`-prefixed sequences are octal; `\1`–`\9` are backreferences.
826fn expand_perl_regex_octal_escapes(pat: &str) -> String {
827    let mut out = String::with_capacity(pat.len());
828    let mut it = pat.chars().peekable();
829    while let Some(c) = it.next() {
830        if c == '\\' {
831            if let Some(&'0') = it.peek() {
832                // Collect up to 3 octal digits starting with '0'
833                let mut oct = String::new();
834                while oct.len() < 3 {
835                    if let Some(&d) = it.peek() {
836                        if ('0'..='7').contains(&d) {
837                            oct.push(d);
838                            it.next();
839                        } else {
840                            break;
841                        }
842                    } else {
843                        break;
844                    }
845                }
846                if let Ok(val) = u8::from_str_radix(&oct, 8) {
847                    out.push_str(&format!("\\x{:02x}", val));
848                } else {
849                    out.push('\\');
850                    out.push_str(&oct);
851                }
852                continue;
853            }
854        }
855        out.push(c);
856    }
857    out
858}
859
860fn expand_perl_regex_quotemeta(pat: &str) -> String {
861    let mut out = String::with_capacity(pat.len().saturating_mul(2));
862    let mut it = pat.chars().peekable();
863    let mut in_q = false;
864    while let Some(c) = it.next() {
865        if in_q {
866            if c == '\\' && it.peek() == Some(&'E') {
867                it.next();
868                in_q = false;
869                continue;
870            }
871            out.push_str(&perl_quotemeta(&c.to_string()));
872            continue;
873        }
874        if c == '\\' && it.peek() == Some(&'Q') {
875            it.next();
876            in_q = true;
877            continue;
878        }
879        out.push(c);
880    }
881    out
882}
883
884/// Normalise Perl replacement backreferences for the Rust `regex` / `fancy_regex` crates.
885///
886/// 1. `\1`..`\9` → `${1}`..`${9}` (Perl backslash syntax).
887/// 2. `$1`..`$9`  → `${1}`..`${9}` (prevents the regex crate from treating `$1X` as the
888///    named capture group `1X` — Perl stops numeric backrefs at the first non-digit).
889pub(crate) fn normalize_replacement_backrefs(replacement: &str) -> String {
890    let mut out = String::with_capacity(replacement.len() + 8);
891    let mut it = replacement.chars().peekable();
892    while let Some(c) = it.next() {
893        if c == '\\' {
894            match it.peek() {
895                Some(&d) if d.is_ascii_digit() => {
896                    it.next();
897                    out.push_str("${");
898                    out.push(d);
899                    while let Some(&d2) = it.peek() {
900                        if !d2.is_ascii_digit() {
901                            break;
902                        }
903                        it.next();
904                        out.push(d2);
905                    }
906                    out.push('}');
907                }
908                Some(&'\\') => {
909                    it.next();
910                    out.push('\\');
911                }
912                _ => out.push('\\'),
913            }
914        } else if c == '$' {
915            match it.peek() {
916                Some(&d) if d.is_ascii_digit() => {
917                    it.next();
918                    out.push_str("${");
919                    out.push(d);
920                    while let Some(&d2) = it.peek() {
921                        if !d2.is_ascii_digit() {
922                            break;
923                        }
924                        it.next();
925                        out.push(d2);
926                    }
927                    out.push('}');
928                }
929                Some(&'{') => {
930                    // already braced — pass through as-is
931                    out.push('$');
932                }
933                _ => out.push('$'),
934            }
935        } else {
936            out.push(c);
937        }
938    }
939    out
940}
941
942/// Copy a Perl character class `[` … `]` from `chars[i]` (must be `'['`) into `out`; return index
943/// past the closing `]`.
944fn copy_regex_char_class(chars: &[char], mut i: usize, out: &mut String) -> usize {
945    debug_assert_eq!(chars.get(i), Some(&'['));
946    out.push('[');
947    i += 1;
948    if i < chars.len() && chars[i] == '^' {
949        out.push('^');
950        i += 1;
951    }
952    if i >= chars.len() {
953        return i;
954    }
955    // `]` as the first class character is literal iff another unescaped `]` closes the class
956    // (e.g. `[]]` / `[^]]`, or `[]\[^$.*/]`). Otherwise `[]` / `[^]` is an empty class closed by
957    // this `]`.
958    if chars[i] == ']' {
959        if i + 1 < chars.len() && chars[i + 1] == ']' {
960            // `[]]` / `[^]]`: literal `]` then the closing `]`.
961            out.push(']');
962            i += 1;
963        } else {
964            let mut scan = i + 1;
965            let mut found_closing = false;
966            while scan < chars.len() {
967                if chars[scan] == '\\' && scan + 1 < chars.len() {
968                    scan += 2;
969                    continue;
970                }
971                if chars[scan] == ']' {
972                    found_closing = true;
973                    break;
974                }
975                scan += 1;
976            }
977            if found_closing {
978                out.push(']');
979                i += 1;
980            } else {
981                out.push(']');
982                return i + 1;
983            }
984        }
985    }
986    while i < chars.len() && chars[i] != ']' {
987        if chars[i] == '\\' && i + 1 < chars.len() {
988            out.push(chars[i]);
989            out.push(chars[i + 1]);
990            i += 2;
991            continue;
992        }
993        out.push(chars[i]);
994        i += 1;
995    }
996    if i < chars.len() {
997        out.push(']');
998        i += 1;
999    }
1000    i
1001}
1002
1003/// Perl `$` (without `/m`) matches end-of-string **or** before a single trailing `\n`. Rust's `$`
1004/// matches only the haystack end, so rewrite bare `$` anchors to `(?:\n?\z)` (after `\Q...\E` and
1005/// outside character classes). Skips `\$`, `$1`…, `${…}`, and `$name` forms that are not end
1006/// anchors. When the `/m` flag is present, Rust `(?m)$` already matches line ends like Perl.
1007fn rewrite_perl_regex_dollar_end_anchor(pat: &str, multiline_flag: bool) -> String {
1008    if multiline_flag {
1009        return pat.to_string();
1010    }
1011    let chars: Vec<char> = pat.chars().collect();
1012    let mut out = String::with_capacity(pat.len().saturating_add(16));
1013    let mut i = 0usize;
1014    while i < chars.len() {
1015        let c = chars[i];
1016        if c == '\\' && i + 1 < chars.len() {
1017            out.push(c);
1018            out.push(chars[i + 1]);
1019            i += 2;
1020            continue;
1021        }
1022        if c == '[' {
1023            i = copy_regex_char_class(&chars, i, &mut out);
1024            continue;
1025        }
1026        if c == '$' {
1027            if let Some(&next) = chars.get(i + 1) {
1028                if next.is_ascii_digit() {
1029                    out.push(c);
1030                    i += 1;
1031                    continue;
1032                }
1033                if next == '{' {
1034                    out.push(c);
1035                    i += 1;
1036                    continue;
1037                }
1038                if next.is_ascii_alphanumeric() || next == '_' {
1039                    out.push(c);
1040                    i += 1;
1041                    continue;
1042                }
1043            }
1044            out.push_str("(?=\\n?\\z)");
1045            i += 1;
1046            continue;
1047        }
1048        out.push(c);
1049        i += 1;
1050    }
1051    out
1052}
1053
1054/// Buffered directory listing for Perl `opendir` / `readdir` (Rust `ReadDir` is single-pass).
1055#[derive(Debug, Clone)]
1056pub(crate) struct DirHandleState {
1057    pub entries: Vec<String>,
1058    pub pos: usize,
1059}
1060
1061/// Perl-style `$^O`: map Rust [`std::env::consts::OS`] to common Perl names (`linux`, `darwin`, `MSWin32`, …).
1062pub(crate) fn perl_osname() -> String {
1063    match std::env::consts::OS {
1064        "linux" => "linux".to_string(),
1065        "macos" => "darwin".to_string(),
1066        "windows" => "MSWin32".to_string(),
1067        other => other.to_string(),
1068    }
1069}
1070
1071fn perl_version_v_string() -> String {
1072    format!("v{}", env!("CARGO_PKG_VERSION"))
1073}
1074
1075fn extended_os_error_string() -> String {
1076    std::io::Error::last_os_error().to_string()
1077}
1078
1079#[cfg(unix)]
1080fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1081    unsafe {
1082        (
1083            libc::getuid() as i64,
1084            libc::geteuid() as i64,
1085            libc::getgid() as i64,
1086            libc::getegid() as i64,
1087        )
1088    }
1089}
1090
1091#[cfg(not(unix))]
1092fn unix_real_effective_ids() -> (i64, i64, i64, i64) {
1093    (0, 0, 0, 0)
1094}
1095
1096fn unix_id_for_special(name: &str) -> i64 {
1097    let (r, e, _, _) = unix_real_effective_ids();
1098    match name {
1099        "<" => r,
1100        ">" => e,
1101        _ => 0,
1102    }
1103}
1104
1105#[cfg(unix)]
1106fn unix_group_list_string(primary: libc::gid_t) -> String {
1107    let mut buf = vec![0 as libc::gid_t; 256];
1108    let n = unsafe { libc::getgroups(256, buf.as_mut_ptr()) };
1109    if n <= 0 {
1110        return format!("{}", primary);
1111    }
1112    let mut parts = vec![format!("{}", primary)];
1113    for g in buf.iter().take(n as usize) {
1114        parts.push(format!("{}", g));
1115    }
1116    parts.join(" ")
1117}
1118
1119/// Perl `$(` / `$)` — space-separated group id list (real / effective set).
1120#[cfg(unix)]
1121fn unix_group_list_for_special(name: &str) -> String {
1122    let (_, _, gid, egid) = unix_real_effective_ids();
1123    match name {
1124        "(" => unix_group_list_string(gid as libc::gid_t),
1125        ")" => unix_group_list_string(egid as libc::gid_t),
1126        _ => String::new(),
1127    }
1128}
1129
1130#[cfg(not(unix))]
1131fn unix_group_list_for_special(_name: &str) -> String {
1132    String::new()
1133}
1134
1135/// Home directory for [`getuid`](libc::getuid) when **`HOME`** is missing (OpenSSH uses it for
1136/// `~/.ssh/config` and keys).
1137#[cfg(unix)]
1138fn pw_home_dir_for_current_uid() -> Option<std::ffi::OsString> {
1139    use libc::{getpwuid_r, getuid};
1140    use std::ffi::CStr;
1141    use std::os::unix::ffi::OsStringExt;
1142    let uid = unsafe { getuid() };
1143    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1144    let mut result: *mut libc::passwd = std::ptr::null_mut();
1145    let mut buf = vec![0u8; 16_384];
1146    let rc = unsafe {
1147        getpwuid_r(
1148            uid,
1149            &mut pw,
1150            buf.as_mut_ptr().cast::<libc::c_char>(),
1151            buf.len(),
1152            &mut result,
1153        )
1154    };
1155    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1156        return None;
1157    }
1158    let bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1159    if bytes.is_empty() {
1160        return None;
1161    }
1162    Some(std::ffi::OsString::from_vec(bytes.to_vec()))
1163}
1164
1165/// Passwd home for a login name (e.g. **`SUDO_USER`** when `stryke` runs under `sudo`).
1166#[cfg(unix)]
1167fn pw_home_dir_for_login_name(login: &std::ffi::OsStr) -> Option<std::ffi::OsString> {
1168    use libc::getpwnam_r;
1169    use std::ffi::{CStr, CString};
1170    use std::os::unix::ffi::{OsStrExt, OsStringExt};
1171    let bytes = login.as_bytes();
1172    if bytes.is_empty() || bytes.contains(&0) {
1173        return None;
1174    }
1175    let cname = CString::new(bytes).ok()?;
1176    let mut pw: libc::passwd = unsafe { std::mem::zeroed() };
1177    let mut result: *mut libc::passwd = std::ptr::null_mut();
1178    let mut buf = vec![0u8; 16_384];
1179    let rc = unsafe {
1180        getpwnam_r(
1181            cname.as_ptr(),
1182            &mut pw,
1183            buf.as_mut_ptr().cast::<libc::c_char>(),
1184            buf.len(),
1185            &mut result,
1186        )
1187    };
1188    if rc != 0 || result.is_null() || pw.pw_dir.is_null() {
1189        return None;
1190    }
1191    let dir_bytes = unsafe { CStr::from_ptr(pw.pw_dir).to_bytes() };
1192    if dir_bytes.is_empty() {
1193        return None;
1194    }
1195    Some(std::ffi::OsString::from_vec(dir_bytes.to_vec()))
1196}
1197
1198impl Default for Interpreter {
1199    fn default() -> Self {
1200        Self::new()
1201    }
1202}
1203
1204/// How [`Interpreter::apply_regex_captures`] updates `@^CAPTURE_ALL`.
1205#[derive(Clone, Copy)]
1206pub(crate) enum CaptureAllMode {
1207    /// Non-`g` match: clear `@^CAPTURE_ALL` (matches Perl 5.42+ empty `@^CAPTURE_ALL` when not using `/g`).
1208    Empty,
1209    /// Scalar-context `m//g`: append one row (numbered groups) per successful iteration.
1210    Append,
1211    /// List `m//g` / `s///g` with rows already stored — do not overwrite `@^CAPTURE_ALL`.
1212    Skip,
1213}
1214
1215impl Interpreter {
1216    pub fn new() -> Self {
1217        let mut scope = Scope::new();
1218        scope.declare_array("INC", vec![PerlValue::string(".".to_string())]);
1219        scope.declare_hash("INC", IndexMap::new());
1220        scope.declare_array("ARGV", vec![]);
1221        scope.declare_array("_", vec![]);
1222        scope.declare_hash("ENV", IndexMap::new());
1223        scope.declare_hash("SIG", IndexMap::new());
1224        // Reflection hashes — populated from `build.rs`-generated tables so
1225        // they track the real parser/dispatcher/LSP without hand-maintenance.
1226        // Seven hashes; all lookups are O(1). Forward maps:
1227        //   %b  / %stryke::builtins      — name → category ("parallel", "string", …)
1228        //   %pc / %stryke::perl_compats  — subset: Perl 5 core only
1229        //   %e  / %stryke::extensions    — subset: stryke-only
1230        //   %a  / %stryke::aliases       — alias → primary
1231        //   %d  / %stryke::descriptions  — name → LSP one-liner (sparse)
1232        // Inverted indexes for constant-time reverse queries:
1233        //   %c  / %stryke::categories    — category → arrayref of names
1234        //   %p  / %stryke::primaries     — primary → arrayref of aliases
1235        //
1236        // `keys %perl_compats ∩ keys %extensions == ∅` by construction;
1237        // together they cover `keys %builtins`. Short aliases use the
1238        // hash-sigil namespace (no collision with `$a`/`$b`/`e` sub).
1239        // Reflection hashes are lazily initialized on first access
1240        // (see `ensure_reflection_hashes`). Only declare the version scalar
1241        // eagerly since it's trivial.
1242        scope.declare_scalar(
1243            "stryke::VERSION",
1244            PerlValue::string(env!("CARGO_PKG_VERSION").to_string()),
1245        );
1246        scope.declare_array("-", vec![]);
1247        scope.declare_array("+", vec![]);
1248        scope.declare_array("^CAPTURE", vec![]);
1249        scope.declare_array("^CAPTURE_ALL", vec![]);
1250        scope.declare_hash("^HOOK", IndexMap::new());
1251        scope.declare_scalar("~", PerlValue::string("STDOUT".to_string()));
1252
1253        let script_start_time = std::time::SystemTime::now()
1254            .duration_since(std::time::UNIX_EPOCH)
1255            .map(|d| d.as_secs() as i64)
1256            .unwrap_or(0);
1257
1258        let executable_path = cached_executable_path();
1259
1260        let mut special_caret_scalars: HashMap<String, PerlValue> = HashMap::new();
1261        for name in crate::special_vars::PERL5_DOCUMENTED_CARET_NAMES {
1262            special_caret_scalars.insert(format!("^{}", name), PerlValue::UNDEF);
1263        }
1264
1265        let mut s = Self {
1266            scope,
1267            subs: HashMap::new(),
1268            struct_defs: HashMap::new(),
1269            enum_defs: HashMap::new(),
1270            class_defs: HashMap::new(),
1271            trait_defs: HashMap::new(),
1272            file: "-e".to_string(),
1273            output_handles: HashMap::new(),
1274            input_handles: HashMap::new(),
1275            ofs: String::new(),
1276            ors: String::new(),
1277            irs: Some("\n".to_string()),
1278            errno: String::new(),
1279            errno_code: 0,
1280            eval_error: String::new(),
1281            eval_error_code: 0,
1282            eval_error_value: None,
1283            argv: Vec::new(),
1284            env: IndexMap::new(),
1285            env_materialized: false,
1286            program_name: "stryke".to_string(),
1287            line_number: 0,
1288            last_readline_handle: String::new(),
1289            last_stdin_die_bracket: "<STDIN>".to_string(),
1290            handle_line_numbers: HashMap::new(),
1291            flip_flop_active: Vec::new(),
1292            flip_flop_exclusive_left_line: Vec::new(),
1293            flip_flop_sequence: Vec::new(),
1294            flip_flop_last_dot: Vec::new(),
1295            flip_flop_tree: HashMap::new(),
1296            sigint_pending_caret: Cell::new(false),
1297            auto_split: false,
1298            field_separator: None,
1299            begin_blocks: Vec::new(),
1300            unit_check_blocks: Vec::new(),
1301            check_blocks: Vec::new(),
1302            init_blocks: Vec::new(),
1303            end_blocks: Vec::new(),
1304            warnings: false,
1305            output_autoflush: false,
1306            default_print_handle: "STDOUT".to_string(),
1307            suppress_stdout: false,
1308            child_exit_status: 0,
1309            last_match: String::new(),
1310            prematch: String::new(),
1311            postmatch: String::new(),
1312            last_paren_match: String::new(),
1313            list_separator: " ".to_string(),
1314            script_start_time,
1315            compile_hints: 0,
1316            warning_bits: 0,
1317            global_phase: "RUN".to_string(),
1318            subscript_sep: "\x1c".to_string(),
1319            inplace_edit: String::new(),
1320            debug_flags: 0,
1321            perl_debug_flags: 0,
1322            eval_nesting: 0,
1323            argv_current_file: String::new(),
1324            diamond_next_idx: 0,
1325            diamond_reader: None,
1326            strict_refs: false,
1327            strict_subs: false,
1328            strict_vars: false,
1329            utf8_pragma: false,
1330            open_pragma_utf8: false,
1331            // Like Perl 5.10+, `say` is enabled by default; `no feature 'say'` disables it.
1332            feature_bits: FEAT_SAY,
1333            num_threads: 0, // lazily read from rayon on first parallel op
1334            regex_cache: HashMap::new(),
1335            regex_last: None,
1336            regex_match_memo: None,
1337            regex_capture_scope_fresh: false,
1338            regex_pos: HashMap::new(),
1339            state_vars: HashMap::new(),
1340            state_bindings_stack: Vec::new(),
1341            rand_rng: StdRng::seed_from_u64(fast_rng_seed()),
1342            dir_handles: HashMap::new(),
1343            io_file_slots: HashMap::new(),
1344            pipe_children: HashMap::new(),
1345            socket_handles: HashMap::new(),
1346            wantarray_kind: WantarrayCtx::Scalar,
1347            profiler: None,
1348            module_export_lists: HashMap::new(),
1349            virtual_modules: HashMap::new(),
1350            tied_hashes: HashMap::new(),
1351            tied_scalars: HashMap::new(),
1352            tied_arrays: HashMap::new(),
1353            overload_table: HashMap::new(),
1354            format_templates: HashMap::new(),
1355            special_caret_scalars,
1356            format_page_number: 0,
1357            format_lines_per_page: 60,
1358            format_lines_left: 0,
1359            format_line_break_chars: "\n".to_string(),
1360            format_top_name: String::new(),
1361            accumulator_format: String::new(),
1362            max_system_fd: 2,
1363            emergency_memory: String::new(),
1364            last_subpattern_name: String::new(),
1365            inc_hook_index: 0,
1366            multiline_match: false,
1367            executable_path,
1368            formfeed_string: "\x0c".to_string(),
1369            glob_handle_alias: HashMap::new(),
1370            glob_restore_frames: vec![Vec::new()],
1371            special_var_restore_frames: vec![Vec::new()],
1372            reflection_hashes_ready: false,
1373            english_enabled: false,
1374            english_no_match_vars: false,
1375            english_match_vars_ever_enabled: false,
1376            english_lexical_scalars: vec![HashSet::new()],
1377            our_lexical_scalars: vec![HashSet::new()],
1378            vm_jit_enabled: !matches!(
1379                std::env::var("STRYKE_NO_JIT"),
1380                Ok(v)
1381                    if v == "1"
1382                        || v.eq_ignore_ascii_case("true")
1383                        || v.eq_ignore_ascii_case("yes")
1384            ),
1385            disasm_bytecode: false,
1386            pec_precompiled_chunk: None,
1387            pec_cache_fingerprint: None,
1388            in_generator: false,
1389            line_mode_skip_main: false,
1390            line_mode_eof_pending: false,
1391            line_mode_stdin_pending: VecDeque::new(),
1392            rate_limit_slots: Vec::new(),
1393            log_level_override: None,
1394            current_sub_stack: Vec::new(),
1395            debugger: None,
1396            debug_call_stack: Vec::new(),
1397        };
1398        s.install_overload_pragma_stubs();
1399        crate::list_util::install_scalar_util(&mut s);
1400        crate::list_util::install_sub_util(&mut s);
1401        s.install_utf8_unicode_to_native_stub();
1402        s
1403    }
1404
1405    /// `utf8::unicode_to_native` — core XS in perl; JSON::PP calls it from BEGIN before utf8_heavy.
1406    fn install_utf8_unicode_to_native_stub(&mut self) {
1407        let empty: Block = vec![];
1408        let key = "utf8::unicode_to_native".to_string();
1409        self.subs.insert(
1410            key.clone(),
1411            Arc::new(PerlSub {
1412                name: key,
1413                params: vec![],
1414                body: empty,
1415                prototype: None,
1416                closure_env: None,
1417                fib_like: None,
1418            }),
1419        );
1420    }
1421
1422    /// Lazily populate the reflection hashes (`%b`, `%stryke::builtins`, etc.)
1423    /// on first access. This avoids building ~12k hash entries on startup for
1424    /// one-liners that never touch introspection.
1425    pub(crate) fn ensure_reflection_hashes(&mut self) {
1426        if self.reflection_hashes_ready {
1427            return;
1428        }
1429        self.reflection_hashes_ready = true;
1430        let builtins_map = crate::builtins::builtins_hash_map();
1431        let perl_compats_map = crate::builtins::perl_compats_hash_map();
1432        let extensions_map = crate::builtins::extensions_hash_map();
1433        let aliases_map = crate::builtins::aliases_hash_map();
1434        let descriptions_map = crate::builtins::descriptions_hash_map();
1435        let categories_map = crate::builtins::categories_hash_map();
1436        let primaries_map = crate::builtins::primaries_hash_map();
1437        let all_map = crate::builtins::all_hash_map();
1438        self.scope
1439            .declare_hash_global("stryke::builtins", builtins_map.clone());
1440        self.scope
1441            .declare_hash_global("stryke::perl_compats", perl_compats_map.clone());
1442        self.scope
1443            .declare_hash_global("stryke::extensions", extensions_map.clone());
1444        self.scope
1445            .declare_hash_global("stryke::aliases", aliases_map.clone());
1446        self.scope
1447            .declare_hash_global("stryke::descriptions", descriptions_map.clone());
1448        self.scope
1449            .declare_hash_global("stryke::categories", categories_map.clone());
1450        self.scope
1451            .declare_hash_global("stryke::primaries", primaries_map.clone());
1452        self.scope
1453            .declare_hash_global("stryke::all", all_map.clone());
1454        // Short aliases: only declare if no user-declared hash with that name
1455        // exists, to avoid overwriting `my %e` etc.
1456        for (name, val) in [
1457            ("b", builtins_map),
1458            ("pc", perl_compats_map),
1459            ("e", extensions_map),
1460            ("a", aliases_map),
1461            ("d", descriptions_map),
1462            ("c", categories_map),
1463            ("p", primaries_map),
1464            ("all", all_map),
1465        ] {
1466            if !self.scope.any_frame_has_hash(name) {
1467                self.scope.declare_hash_global(name, val);
1468            }
1469        }
1470    }
1471
1472    /// `overload::import` / `overload::unimport` — core stubs used by CPAN modules (e.g.
1473    /// `JSON::PP::Boolean`) before real `overload.pm` is modeled. Empty bodies are enough for
1474    /// strict subs and to satisfy `use overload ();` call sites.
1475    fn install_overload_pragma_stubs(&mut self) {
1476        let empty: Block = vec![];
1477        for key in ["overload::import", "overload::unimport"] {
1478            let name = key.to_string();
1479            self.subs.insert(
1480                name.clone(),
1481                Arc::new(PerlSub {
1482                    name,
1483                    params: vec![],
1484                    body: empty.clone(),
1485                    prototype: None,
1486                    closure_env: None,
1487                    fib_like: None,
1488                }),
1489            );
1490        }
1491    }
1492
1493    /// Fork interpreter state for `-n`/`-p` over multiple `@ARGV` files in parallel (rayon).
1494    /// Clears file descriptors and I/O handles (each worker only runs the line loop).
1495    pub fn line_mode_worker_clone(&self) -> Interpreter {
1496        Interpreter {
1497            scope: self.scope.clone(),
1498            subs: self.subs.clone(),
1499            struct_defs: self.struct_defs.clone(),
1500            enum_defs: self.enum_defs.clone(),
1501            class_defs: self.class_defs.clone(),
1502            trait_defs: self.trait_defs.clone(),
1503            file: self.file.clone(),
1504            output_handles: HashMap::new(),
1505            input_handles: HashMap::new(),
1506            ofs: self.ofs.clone(),
1507            ors: self.ors.clone(),
1508            irs: self.irs.clone(),
1509            errno: self.errno.clone(),
1510            errno_code: self.errno_code,
1511            eval_error: self.eval_error.clone(),
1512            eval_error_code: self.eval_error_code,
1513            eval_error_value: self.eval_error_value.clone(),
1514            argv: self.argv.clone(),
1515            env: self.env.clone(),
1516            env_materialized: self.env_materialized,
1517            program_name: self.program_name.clone(),
1518            line_number: 0,
1519            last_readline_handle: String::new(),
1520            last_stdin_die_bracket: "<STDIN>".to_string(),
1521            handle_line_numbers: HashMap::new(),
1522            flip_flop_active: Vec::new(),
1523            flip_flop_exclusive_left_line: Vec::new(),
1524            flip_flop_sequence: Vec::new(),
1525            flip_flop_last_dot: Vec::new(),
1526            flip_flop_tree: HashMap::new(),
1527            sigint_pending_caret: Cell::new(false),
1528            auto_split: self.auto_split,
1529            field_separator: self.field_separator.clone(),
1530            begin_blocks: self.begin_blocks.clone(),
1531            unit_check_blocks: self.unit_check_blocks.clone(),
1532            check_blocks: self.check_blocks.clone(),
1533            init_blocks: self.init_blocks.clone(),
1534            end_blocks: self.end_blocks.clone(),
1535            warnings: self.warnings,
1536            output_autoflush: self.output_autoflush,
1537            default_print_handle: self.default_print_handle.clone(),
1538            suppress_stdout: self.suppress_stdout,
1539            child_exit_status: self.child_exit_status,
1540            last_match: self.last_match.clone(),
1541            prematch: self.prematch.clone(),
1542            postmatch: self.postmatch.clone(),
1543            last_paren_match: self.last_paren_match.clone(),
1544            list_separator: self.list_separator.clone(),
1545            script_start_time: self.script_start_time,
1546            compile_hints: self.compile_hints,
1547            warning_bits: self.warning_bits,
1548            global_phase: self.global_phase.clone(),
1549            subscript_sep: self.subscript_sep.clone(),
1550            inplace_edit: self.inplace_edit.clone(),
1551            debug_flags: self.debug_flags,
1552            perl_debug_flags: self.perl_debug_flags,
1553            eval_nesting: self.eval_nesting,
1554            argv_current_file: String::new(),
1555            diamond_next_idx: 0,
1556            diamond_reader: None,
1557            strict_refs: self.strict_refs,
1558            strict_subs: self.strict_subs,
1559            strict_vars: self.strict_vars,
1560            utf8_pragma: self.utf8_pragma,
1561            open_pragma_utf8: self.open_pragma_utf8,
1562            feature_bits: self.feature_bits,
1563            num_threads: 0,
1564            regex_cache: self.regex_cache.clone(),
1565            regex_last: self.regex_last.clone(),
1566            regex_match_memo: self.regex_match_memo.clone(),
1567            regex_capture_scope_fresh: false,
1568            regex_pos: self.regex_pos.clone(),
1569            state_vars: self.state_vars.clone(),
1570            state_bindings_stack: Vec::new(),
1571            rand_rng: self.rand_rng.clone(),
1572            dir_handles: HashMap::new(),
1573            io_file_slots: HashMap::new(),
1574            pipe_children: HashMap::new(),
1575            socket_handles: HashMap::new(),
1576            wantarray_kind: self.wantarray_kind,
1577            profiler: None,
1578            module_export_lists: self.module_export_lists.clone(),
1579            virtual_modules: self.virtual_modules.clone(),
1580            tied_hashes: self.tied_hashes.clone(),
1581            tied_scalars: self.tied_scalars.clone(),
1582            tied_arrays: self.tied_arrays.clone(),
1583            overload_table: self.overload_table.clone(),
1584            format_templates: self.format_templates.clone(),
1585            special_caret_scalars: self.special_caret_scalars.clone(),
1586            format_page_number: self.format_page_number,
1587            format_lines_per_page: self.format_lines_per_page,
1588            format_lines_left: self.format_lines_left,
1589            format_line_break_chars: self.format_line_break_chars.clone(),
1590            format_top_name: self.format_top_name.clone(),
1591            accumulator_format: self.accumulator_format.clone(),
1592            max_system_fd: self.max_system_fd,
1593            emergency_memory: self.emergency_memory.clone(),
1594            last_subpattern_name: self.last_subpattern_name.clone(),
1595            inc_hook_index: self.inc_hook_index,
1596            multiline_match: self.multiline_match,
1597            executable_path: self.executable_path.clone(),
1598            formfeed_string: self.formfeed_string.clone(),
1599            glob_handle_alias: self.glob_handle_alias.clone(),
1600            glob_restore_frames: self.glob_restore_frames.clone(),
1601            special_var_restore_frames: self.special_var_restore_frames.clone(),
1602            reflection_hashes_ready: self.reflection_hashes_ready,
1603            english_enabled: self.english_enabled,
1604            english_no_match_vars: self.english_no_match_vars,
1605            english_match_vars_ever_enabled: self.english_match_vars_ever_enabled,
1606            english_lexical_scalars: self.english_lexical_scalars.clone(),
1607            our_lexical_scalars: self.our_lexical_scalars.clone(),
1608            vm_jit_enabled: self.vm_jit_enabled,
1609            disasm_bytecode: self.disasm_bytecode,
1610            // Sideband cache fields belong to the top-level driver, not line-mode workers.
1611            pec_precompiled_chunk: None,
1612            pec_cache_fingerprint: None,
1613            in_generator: false,
1614            line_mode_skip_main: false,
1615            line_mode_eof_pending: false,
1616            line_mode_stdin_pending: VecDeque::new(),
1617            rate_limit_slots: Vec::new(),
1618            log_level_override: self.log_level_override,
1619            current_sub_stack: Vec::new(),
1620            debugger: None,
1621            debug_call_stack: Vec::new(),
1622        }
1623    }
1624
1625    /// Rayon pool size (`stryke -j`); lazily initialized from `rayon::current_num_threads()`.
1626    pub(crate) fn parallel_thread_count(&mut self) -> usize {
1627        if self.num_threads == 0 {
1628            self.num_threads = rayon::current_num_threads();
1629        }
1630        self.num_threads
1631    }
1632
1633    /// `puniq` / `pfirst` / `pany` — parallel list builtins ([`crate::par_list`]).
1634    pub(crate) fn eval_par_list_call(
1635        &mut self,
1636        name: &str,
1637        args: &[PerlValue],
1638        ctx: WantarrayCtx,
1639        line: usize,
1640    ) -> PerlResult<PerlValue> {
1641        match name {
1642            "puniq" => {
1643                let (list_src, show_prog) = match args.len() {
1644                    0 => return Err(PerlError::runtime("puniq: expected LIST", line)),
1645                    1 => (&args[0], false),
1646                    2 => (&args[0], args[1].is_true()),
1647                    _ => {
1648                        return Err(PerlError::runtime(
1649                            "puniq: expected LIST [, progress => EXPR]",
1650                            line,
1651                        ));
1652                    }
1653                };
1654                let list = list_src.to_list();
1655                let n_threads = self.parallel_thread_count();
1656                let pmap_progress = PmapProgress::new(show_prog, list.len());
1657                let out = crate::par_list::puniq_run(list, n_threads, &pmap_progress);
1658                pmap_progress.finish();
1659                if ctx == WantarrayCtx::List {
1660                    Ok(PerlValue::array(out))
1661                } else {
1662                    Ok(PerlValue::integer(out.len() as i64))
1663                }
1664            }
1665            "pfirst" => {
1666                let (code_val, list_src, show_prog) = match args.len() {
1667                    2 => (&args[0], &args[1], false),
1668                    3 => (&args[0], &args[1], args[2].is_true()),
1669                    _ => {
1670                        return Err(PerlError::runtime(
1671                            "pfirst: expected BLOCK, LIST [, progress => EXPR]",
1672                            line,
1673                        ));
1674                    }
1675                };
1676                let Some(sub) = code_val.as_code_ref() else {
1677                    return Err(PerlError::runtime(
1678                        "pfirst: first argument must be a code reference",
1679                        line,
1680                    ));
1681                };
1682                let sub = sub.clone();
1683                let list = list_src.to_list();
1684                if list.is_empty() {
1685                    return Ok(PerlValue::UNDEF);
1686                }
1687                let pmap_progress = PmapProgress::new(show_prog, list.len());
1688                let subs = self.subs.clone();
1689                let (scope_capture, atomic_arrays, atomic_hashes) =
1690                    self.scope.capture_with_atomics();
1691                let out = crate::par_list::pfirst_run(list, &pmap_progress, |item| {
1692                    let mut local_interp = Interpreter::new();
1693                    local_interp.subs = subs.clone();
1694                    local_interp.scope.restore_capture(&scope_capture);
1695                    local_interp
1696                        .scope
1697                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1698                    local_interp.enable_parallel_guard();
1699                    local_interp.scope.set_topic(item);
1700                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1701                        Ok(v) => v.is_true(),
1702                        Err(_) => false,
1703                    }
1704                });
1705                pmap_progress.finish();
1706                Ok(out.unwrap_or(PerlValue::UNDEF))
1707            }
1708            "pany" => {
1709                let (code_val, list_src, show_prog) = match args.len() {
1710                    2 => (&args[0], &args[1], false),
1711                    3 => (&args[0], &args[1], args[2].is_true()),
1712                    _ => {
1713                        return Err(PerlError::runtime(
1714                            "pany: expected BLOCK, LIST [, progress => EXPR]",
1715                            line,
1716                        ));
1717                    }
1718                };
1719                let Some(sub) = code_val.as_code_ref() else {
1720                    return Err(PerlError::runtime(
1721                        "pany: first argument must be a code reference",
1722                        line,
1723                    ));
1724                };
1725                let sub = sub.clone();
1726                let list = list_src.to_list();
1727                let pmap_progress = PmapProgress::new(show_prog, list.len());
1728                let subs = self.subs.clone();
1729                let (scope_capture, atomic_arrays, atomic_hashes) =
1730                    self.scope.capture_with_atomics();
1731                let b = crate::par_list::pany_run(list, &pmap_progress, |item| {
1732                    let mut local_interp = Interpreter::new();
1733                    local_interp.subs = subs.clone();
1734                    local_interp.scope.restore_capture(&scope_capture);
1735                    local_interp
1736                        .scope
1737                        .restore_atomics(&atomic_arrays, &atomic_hashes);
1738                    local_interp.enable_parallel_guard();
1739                    local_interp.scope.set_topic(item);
1740                    match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Scalar, line) {
1741                        Ok(v) => v.is_true(),
1742                        Err(_) => false,
1743                    }
1744                });
1745                pmap_progress.finish();
1746                Ok(PerlValue::integer(if b { 1 } else { 0 }))
1747            }
1748            _ => Err(PerlError::runtime(
1749                format!("internal: unknown par_list builtin {name}"),
1750                line,
1751            )),
1752        }
1753    }
1754
1755    fn encode_exit_status(&self, s: std::process::ExitStatus) -> i64 {
1756        #[cfg(unix)]
1757        if let Some(sig) = s.signal() {
1758            return sig as i64 & 0x7f;
1759        }
1760        let code = s.code().unwrap_or(0) as i64;
1761        code << 8
1762    }
1763
1764    pub(crate) fn record_child_exit_status(&mut self, s: std::process::ExitStatus) {
1765        self.child_exit_status = self.encode_exit_status(s);
1766    }
1767
1768    /// Update `$!` / `errno_code` from a [`std::io::Error`] (dualvar numeric + string).
1769    pub(crate) fn apply_io_error_to_errno(&mut self, e: &std::io::Error) {
1770        self.errno = e.to_string();
1771        self.errno_code = e.raw_os_error().unwrap_or(0);
1772    }
1773
1774    /// `ssh LIST` — run the real `ssh` binary with `LIST` as argv (no `sh -c`).
1775    ///
1776    /// **`Host` aliases in `~/.ssh/config`** are honored by OpenSSH like in a normal shell (same
1777    /// binary, inherited env). **Shell** `alias` / functions are not applied (no `sh -c`). If
1778    /// **`HOME`** is unset, on Unix we set it from the passwd DB so config and keys resolve.
1779    ///
1780    /// **`sudo`:** the child `ssh` normally sees **`HOME=/root`**, so it reads **`/root/.ssh/config`**
1781    /// and host aliases in *your* config are missing. When **`SUDO_USER`** is set and the effective
1782    /// uid is **0**, we set **`HOME`** for this subprocess to **`SUDO_USER`'s** passwd home so your
1783    /// `~/.ssh/config` and keys apply.
1784    pub(crate) fn ssh_builtin_execute(&mut self, args: &[PerlValue]) -> PerlResult<PerlValue> {
1785        use std::process::Command;
1786        let mut cmd = Command::new("ssh");
1787        #[cfg(unix)]
1788        {
1789            use libc::geteuid;
1790            let home_for_ssh = if unsafe { geteuid() } == 0 {
1791                std::env::var_os("SUDO_USER").and_then(|u| pw_home_dir_for_login_name(&u))
1792            } else {
1793                None
1794            };
1795            if let Some(h) = home_for_ssh {
1796                cmd.env("HOME", h);
1797            } else if std::env::var_os("HOME").is_none() {
1798                if let Some(h) = pw_home_dir_for_current_uid() {
1799                    cmd.env("HOME", h);
1800                }
1801            }
1802        }
1803        for a in args {
1804            cmd.arg(a.to_string());
1805        }
1806        match cmd.status() {
1807            Ok(s) => {
1808                self.record_child_exit_status(s);
1809                Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
1810            }
1811            Err(e) => {
1812                self.apply_io_error_to_errno(&e);
1813                Ok(PerlValue::integer(-1))
1814            }
1815        }
1816    }
1817
1818    /// Set `$@` message; numeric side is `0` if empty, else `1`.
1819    pub(crate) fn set_eval_error(&mut self, msg: String) {
1820        self.eval_error = msg;
1821        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
1822        self.eval_error_value = None;
1823    }
1824
1825    pub(crate) fn set_eval_error_from_perl_error(&mut self, e: &PerlError) {
1826        self.eval_error = e.to_string();
1827        self.eval_error_code = if self.eval_error.is_empty() { 0 } else { 1 };
1828        self.eval_error_value = e.die_value.clone();
1829    }
1830
1831    pub(crate) fn clear_eval_error(&mut self) {
1832        self.eval_error = String::new();
1833        self.eval_error_code = 0;
1834        self.eval_error_value = None;
1835    }
1836
1837    /// Advance `$.` bookkeeping for the handle that produced the last `readline` line.
1838    fn bump_line_for_handle(&mut self, handle_key: &str) {
1839        self.last_readline_handle = handle_key.to_string();
1840        *self
1841            .handle_line_numbers
1842            .entry(handle_key.to_string())
1843            .or_insert(0) += 1;
1844    }
1845
1846    /// `@ISA` / `@EXPORT` storage uses `Pkg::NAME` outside `main`.
1847    pub(crate) fn stash_array_name_for_package(&self, name: &str) -> String {
1848        if name.starts_with('^') {
1849            return name.to_string();
1850        }
1851        if matches!(name, "ISA" | "EXPORT" | "EXPORT_OK") {
1852            let pkg = self.current_package();
1853            if !pkg.is_empty() && pkg != "main" {
1854                return format!("{}::{}", pkg, name);
1855            }
1856        }
1857        name.to_string()
1858    }
1859
1860    /// Package stash key for `our $name` (same rule as [`Compiler::qualify_stash_scalar_name`]).
1861    pub(crate) fn stash_scalar_name_for_package(&self, name: &str) -> String {
1862        if name.contains("::") {
1863            return name.to_string();
1864        }
1865        let pkg = self.current_package();
1866        if pkg.is_empty() || pkg == "main" {
1867            format!("main::{}", name)
1868        } else {
1869            format!("{}::{}", pkg, name)
1870        }
1871    }
1872
1873    /// Tree-walker: bare `$x` after `our $x` reads the package stash scalar (`main::x` / `Pkg::x`).
1874    pub(crate) fn tree_scalar_storage_name(&self, name: &str) -> String {
1875        if name.contains("::") {
1876            return name.to_string();
1877        }
1878        for (lex, our) in self
1879            .english_lexical_scalars
1880            .iter()
1881            .zip(self.our_lexical_scalars.iter())
1882            .rev()
1883        {
1884            if lex.contains(name) {
1885                if our.contains(name) {
1886                    return self.stash_scalar_name_for_package(name);
1887                }
1888                return name.to_string();
1889            }
1890        }
1891        name.to_string()
1892    }
1893
1894    /// Shared by tree `StmtKind::Tie` and bytecode [`crate::bytecode::Op::Tie`].
1895    pub(crate) fn tie_execute(
1896        &mut self,
1897        target_kind: u8,
1898        target_name: &str,
1899        class_and_args: Vec<PerlValue>,
1900        line: usize,
1901    ) -> PerlResult<PerlValue> {
1902        let mut it = class_and_args.into_iter();
1903        let class = it.next().unwrap_or(PerlValue::UNDEF);
1904        let pkg = class.to_string();
1905        let pkg = pkg.trim_matches(|c| c == '\'' || c == '"').to_string();
1906        let tie_ctor = match target_kind {
1907            0 => "TIESCALAR",
1908            1 => "TIEARRAY",
1909            2 => "TIEHASH",
1910            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
1911        };
1912        let tie_fn = format!("{}::{}", pkg, tie_ctor);
1913        let sub = self
1914            .subs
1915            .get(&tie_fn)
1916            .cloned()
1917            .ok_or_else(|| PerlError::runtime(format!("tie: cannot find &{}", tie_fn), line))?;
1918        let mut call_args = vec![PerlValue::string(pkg.clone())];
1919        call_args.extend(it);
1920        let obj = match self.call_sub(&sub, call_args, WantarrayCtx::Scalar, line) {
1921            Ok(v) => v,
1922            Err(FlowOrError::Flow(_)) => PerlValue::UNDEF,
1923            Err(FlowOrError::Error(e)) => return Err(e),
1924        };
1925        match target_kind {
1926            0 => {
1927                self.tied_scalars.insert(target_name.to_string(), obj);
1928            }
1929            1 => {
1930                let key = self.stash_array_name_for_package(target_name);
1931                self.tied_arrays.insert(key, obj);
1932            }
1933            2 => {
1934                self.tied_hashes.insert(target_name.to_string(), obj);
1935            }
1936            _ => return Err(PerlError::runtime("tie: invalid target kind", line)),
1937        }
1938        Ok(PerlValue::UNDEF)
1939    }
1940
1941    /// Immediate parents from live `@Class::ISA` (no cached MRO — changes take effect on next method lookup).
1942    pub(crate) fn parents_of_class(&self, class: &str) -> Vec<String> {
1943        let key = format!("{}::ISA", class);
1944        self.scope
1945            .get_array(&key)
1946            .into_iter()
1947            .map(|v| v.to_string())
1948            .collect()
1949    }
1950
1951    pub(crate) fn mro_linearize(&self, class: &str) -> Vec<String> {
1952        let p = |c: &str| self.parents_of_class(c);
1953        linearize_c3(class, &p, 0)
1954    }
1955
1956    /// Returns fully qualified sub name for [`Self::subs`], or a candidate for [`Self::try_autoload_call`].
1957    pub(crate) fn resolve_method_full_name(
1958        &self,
1959        invocant_class: &str,
1960        method: &str,
1961        super_mode: bool,
1962    ) -> Option<String> {
1963        let mro = self.mro_linearize(invocant_class);
1964        // SUPER:: — skip the invocant's class in C3 order (same as Perl: start at the parent of
1965        // the blessed class). Do not use `__PACKAGE__` here: it may be `main` after `package main`
1966        // even when running `C::meth`.
1967        let start = if super_mode {
1968            mro.iter()
1969                .position(|p| p == invocant_class)
1970                .map(|i| i + 1)
1971                // If the class string does not appear in MRO (should be rare), skip the first
1972                // entry so we still search parents before giving up.
1973                .unwrap_or(1)
1974        } else {
1975            0
1976        };
1977        for pkg in mro.iter().skip(start) {
1978            if pkg == "UNIVERSAL" {
1979                continue;
1980            }
1981            let fq = format!("{}::{}", pkg, method);
1982            if self.subs.contains_key(&fq) {
1983                return Some(fq);
1984            }
1985        }
1986        mro.iter()
1987            .skip(start)
1988            .find(|p| *p != "UNIVERSAL")
1989            .map(|pkg| format!("{}::{}", pkg, method))
1990    }
1991
1992    pub(crate) fn resolve_io_handle_name(&self, name: &str) -> String {
1993        if let Some(alias) = self.glob_handle_alias.get(name) {
1994            return alias.clone();
1995        }
1996        // `print $fh …` stores the handle as "$varname"; resolve it by
1997        // reading the scalar variable which holds the IO handle name.
1998        if let Some(var_name) = name.strip_prefix('$') {
1999            let val = self.scope.get_scalar(var_name);
2000            let s = val.to_string();
2001            if !s.is_empty() {
2002                return self.resolve_io_handle_name(&s);
2003            }
2004        }
2005        name.to_string()
2006    }
2007
2008    /// Stash key for `sub name` / `&name` when `name` is a typeglob basename (`*foo`, `*Pkg::foo`).
2009    pub(crate) fn qualify_typeglob_sub_key(&self, name: &str) -> String {
2010        if name.contains("::") {
2011            name.to_string()
2012        } else {
2013            self.qualify_sub_key(name)
2014        }
2015    }
2016
2017    /// `*lhs = *rhs` — copy subroutine, scalar, array, hash, and IO-handle alias slots (Perl-style).
2018    pub(crate) fn copy_typeglob_slots(
2019        &mut self,
2020        lhs: &str,
2021        rhs: &str,
2022        line: usize,
2023    ) -> PerlResult<()> {
2024        let lhs_sub = self.qualify_typeglob_sub_key(lhs);
2025        let rhs_sub = self.qualify_typeglob_sub_key(rhs);
2026        match self.subs.get(&rhs_sub).cloned() {
2027            Some(s) => {
2028                self.subs.insert(lhs_sub, s);
2029            }
2030            None => {
2031                self.subs.remove(&lhs_sub);
2032            }
2033        }
2034        let sv = self.scope.get_scalar(rhs);
2035        self.scope
2036            .set_scalar(lhs, sv.clone())
2037            .map_err(|e| e.at_line(line))?;
2038        let lhs_an = self.stash_array_name_for_package(lhs);
2039        let rhs_an = self.stash_array_name_for_package(rhs);
2040        let av = self.scope.get_array(&rhs_an);
2041        self.scope
2042            .set_array(&lhs_an, av.clone())
2043            .map_err(|e| e.at_line(line))?;
2044        let hv = self.scope.get_hash(rhs);
2045        self.scope
2046            .set_hash(lhs, hv.clone())
2047            .map_err(|e| e.at_line(line))?;
2048        match self.glob_handle_alias.get(rhs).cloned() {
2049            Some(t) => {
2050                self.glob_handle_alias.insert(lhs.to_string(), t);
2051            }
2052            None => {
2053                self.glob_handle_alias.remove(lhs);
2054            }
2055        }
2056        Ok(())
2057    }
2058
2059    /// `format NAME =` … — register under `current_package::NAME` (VM [`crate::bytecode::Op::FormatDecl`] and tree).
2060    pub(crate) fn install_format_decl(
2061        &mut self,
2062        basename: &str,
2063        lines: &[String],
2064        line: usize,
2065    ) -> PerlResult<()> {
2066        let pkg = self.current_package();
2067        let key = format!("{}::{}", pkg, basename);
2068        let tmpl = crate::format::parse_format_template(lines).map_err(|e| e.at_line(line))?;
2069        self.format_templates.insert(key, Arc::new(tmpl));
2070        Ok(())
2071    }
2072
2073    /// `use overload` — merge pairs into [`Self::overload_table`] for [`Self::current_package`].
2074    pub(crate) fn install_use_overload_pairs(&mut self, pairs: &[(String, String)]) {
2075        let pkg = self.current_package();
2076        let ent = self.overload_table.entry(pkg).or_default();
2077        for (k, v) in pairs {
2078            ent.insert(k.clone(), v.clone());
2079        }
2080    }
2081
2082    /// `local *LHS` / `local *LHS = *RHS` — save/restore [`Self::glob_handle_alias`] like the tree
2083    /// [`StmtKind::Local`] / [`StmtKind::LocalExpr`] paths.
2084    pub(crate) fn local_declare_typeglob(
2085        &mut self,
2086        lhs: &str,
2087        rhs: Option<&str>,
2088        line: usize,
2089    ) -> PerlResult<()> {
2090        let old = self.glob_handle_alias.remove(lhs);
2091        let Some(frame) = self.glob_restore_frames.last_mut() else {
2092            return Err(PerlError::runtime(
2093                "internal: no glob restore frame for local *GLOB",
2094                line,
2095            ));
2096        };
2097        frame.push((lhs.to_string(), old));
2098        if let Some(r) = rhs {
2099            self.glob_handle_alias
2100                .insert(lhs.to_string(), r.to_string());
2101        }
2102        Ok(())
2103    }
2104
2105    pub(crate) fn scope_push_hook(&mut self) {
2106        self.scope.push_frame();
2107        self.glob_restore_frames.push(Vec::new());
2108        self.special_var_restore_frames.push(Vec::new());
2109        self.english_lexical_scalars.push(HashSet::new());
2110        self.our_lexical_scalars.push(HashSet::new());
2111        self.state_bindings_stack.push(Vec::new());
2112    }
2113
2114    #[inline]
2115    pub(crate) fn english_note_lexical_scalar(&mut self, name: &str) {
2116        if let Some(s) = self.english_lexical_scalars.last_mut() {
2117            s.insert(name.to_string());
2118        }
2119    }
2120
2121    #[inline]
2122    fn note_our_scalar(&mut self, bare_name: &str) {
2123        if let Some(s) = self.our_lexical_scalars.last_mut() {
2124            s.insert(bare_name.to_string());
2125        }
2126    }
2127
2128    pub(crate) fn scope_pop_hook(&mut self) {
2129        if !self.scope.can_pop_frame() {
2130            return;
2131        }
2132        // Execute deferred blocks in LIFO order before popping the frame.
2133        // Important: defer blocks run in the CURRENT scope (not a new frame),
2134        // so they can modify variables in the enclosing scope.
2135        let defers = self.scope.take_defers();
2136        for coderef in defers {
2137            if let Some(sub) = coderef.as_code_ref() {
2138                // Execute the defer block body directly in the current scope,
2139                // without creating a new frame or restoring closure captures.
2140                // This allows defer { $x = 100 } to modify the outer $x.
2141                let saved_wa = self.wantarray_kind;
2142                self.wantarray_kind = WantarrayCtx::Void;
2143                let _ = self.exec_block_no_scope(&sub.body);
2144                self.wantarray_kind = saved_wa;
2145            }
2146        }
2147        // Save state variable values back before popping the frame
2148        if let Some(bindings) = self.state_bindings_stack.pop() {
2149            for (var_name, state_key) in &bindings {
2150                let val = self.scope.get_scalar(var_name).clone();
2151                self.state_vars.insert(state_key.clone(), val);
2152            }
2153        }
2154        // `local $/` / `$\` / `$,` / `$"` etc. — restore each special-var backing field
2155        // BEFORE the scope frame is popped, since `set_special_var` may consult `self.scope`.
2156        if let Some(entries) = self.special_var_restore_frames.pop() {
2157            for (name, old) in entries.into_iter().rev() {
2158                let _ = self.set_special_var(&name, &old);
2159            }
2160        }
2161        if let Some(entries) = self.glob_restore_frames.pop() {
2162            for (name, old) in entries.into_iter().rev() {
2163                match old {
2164                    Some(s) => {
2165                        self.glob_handle_alias.insert(name, s);
2166                    }
2167                    None => {
2168                        self.glob_handle_alias.remove(&name);
2169                    }
2170                }
2171            }
2172        }
2173        self.scope.pop_frame();
2174        let _ = self.english_lexical_scalars.pop();
2175        let _ = self.our_lexical_scalars.pop();
2176    }
2177
2178    /// After [`Scope::restore_capture`] / [`Scope::restore_atomics`] on a parallel or async worker,
2179    /// reject writes to non-`mysync` outer captured lexicals (block locals use `scope_push_hook`).
2180    #[inline]
2181    pub(crate) fn enable_parallel_guard(&mut self) {
2182        self.scope.set_parallel_guard(true);
2183    }
2184
2185    /// BEGIN/END are lowered into the VM chunk; clear interpreter queues so a later tree-walker
2186    /// run does not execute them again.
2187    pub(crate) fn clear_begin_end_blocks_after_vm_compile(&mut self) {
2188        self.begin_blocks.clear();
2189        self.unit_check_blocks.clear();
2190        self.check_blocks.clear();
2191        self.init_blocks.clear();
2192        self.end_blocks.clear();
2193    }
2194
2195    /// Pop scope frames until [`Scope::depth`] == `target_depth`, running [`Self::scope_pop_hook`]
2196    /// each time so `glob_restore_frames` / `english_lexical_scalars` stay aligned with
2197    /// [`Self::scope_push_hook`]. The bytecode VM must use this after [`Op::Call`] /
2198    /// [`Op::PushFrame`] (which call `scope_push_hook`); [`Scope::pop_to_depth`] alone is wrong
2199    /// there because it only calls [`Scope::pop_frame`].
2200    pub(crate) fn pop_scope_to_depth(&mut self, target_depth: usize) {
2201        while self.scope.depth() > target_depth && self.scope.can_pop_frame() {
2202            self.scope_pop_hook();
2203        }
2204    }
2205
2206    /// `%SIG` hook — code refs run between statements (`perl_signal` module).
2207    ///
2208    /// Unset `%SIG` entries and the string **`DEFAULT`** mean **POSIX default** for that signal (not
2209    /// IGNORE). That matters for `SIGINT` / `SIGTERM` / `SIGALRM`, where default is terminate — so
2210    /// Ctrl+C is not “trapped” when no handler is installed (including parallel `pmap` / `progress`
2211    /// workers that call `perl_signal::poll`).
2212    pub(crate) fn invoke_sig_handler(&mut self, sig: &str) -> PerlResult<()> {
2213        self.touch_env_hash("SIG");
2214        let v = self.scope.get_hash_element("SIG", sig);
2215        if v.is_undef() {
2216            return Self::default_sig_action(sig);
2217        }
2218        if let Some(s) = v.as_str() {
2219            if s == "IGNORE" {
2220                return Ok(());
2221            }
2222            if s == "DEFAULT" {
2223                return Self::default_sig_action(sig);
2224            }
2225        }
2226        if let Some(sub) = v.as_code_ref() {
2227            match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, 0) {
2228                Ok(_) => Ok(()),
2229                Err(FlowOrError::Flow(_)) => Ok(()),
2230                Err(FlowOrError::Error(e)) => Err(e),
2231            }
2232        } else {
2233            Self::default_sig_action(sig)
2234        }
2235    }
2236
2237    /// POSIX default for signals we deliver via `perl_signal::poll` (Unix).
2238    #[inline]
2239    fn default_sig_action(sig: &str) -> PerlResult<()> {
2240        match sig {
2241            // 128 + signal number (common shell convention)
2242            "INT" => std::process::exit(130),
2243            "TERM" => std::process::exit(143),
2244            "ALRM" => std::process::exit(142),
2245            // Default for SIGCHLD is ignore
2246            "CHLD" => Ok(()),
2247            _ => Ok(()),
2248        }
2249    }
2250
2251    /// Populate [`Self::env`] and the `%ENV` hash from [`std::env::vars`] once.
2252    /// Deferred from [`Self::new`] to reduce interpreter startup when `%ENV` is unused.
2253    pub fn materialize_env_if_needed(&mut self) {
2254        if self.env_materialized {
2255            return;
2256        }
2257        self.env = std::env::vars()
2258            .map(|(k, v)| (k, PerlValue::string(v)))
2259            .collect();
2260        self.scope
2261            .set_hash("ENV", self.env.clone())
2262            .expect("set %ENV");
2263        self.env_materialized = true;
2264    }
2265
2266    /// Effective minimum log level (`log_level()` override, else `$ENV{LOG_LEVEL}`, else `info`).
2267    pub(crate) fn log_filter_effective(&mut self) -> LogLevelFilter {
2268        self.materialize_env_if_needed();
2269        if let Some(x) = self.log_level_override {
2270            return x;
2271        }
2272        let s = self.scope.get_hash_element("ENV", "LOG_LEVEL").to_string();
2273        LogLevelFilter::parse(&s).unwrap_or(LogLevelFilter::Info)
2274    }
2275
2276    /// <https://no-color.org/> — non-empty `$ENV{NO_COLOR}` disables ANSI in `log_*`.
2277    pub(crate) fn no_color_effective(&mut self) -> bool {
2278        self.materialize_env_if_needed();
2279        let v = self.scope.get_hash_element("ENV", "NO_COLOR");
2280        if v.is_undef() {
2281            return false;
2282        }
2283        !v.to_string().is_empty()
2284    }
2285
2286    #[inline]
2287    pub(crate) fn touch_env_hash(&mut self, hash_name: &str) {
2288        if hash_name == "ENV" {
2289            self.materialize_env_if_needed();
2290        } else if !self.reflection_hashes_ready && !self.scope.has_lexical_hash(hash_name) {
2291            match hash_name {
2292                "b"
2293                | "pc"
2294                | "e"
2295                | "a"
2296                | "d"
2297                | "c"
2298                | "p"
2299                | "all"
2300                | "stryke::builtins"
2301                | "stryke::perl_compats"
2302                | "stryke::extensions"
2303                | "stryke::aliases"
2304                | "stryke::descriptions"
2305                | "stryke::categories"
2306                | "stryke::primaries"
2307                | "stryke::all" => {
2308                    self.ensure_reflection_hashes();
2309                }
2310                _ => {}
2311            }
2312        }
2313    }
2314
2315    /// `exists $href->{k}` / `exists $obj->{k}` — container is a hash ref or blessed hash-like value.
2316    pub(crate) fn exists_arrow_hash_element(
2317        &self,
2318        container: PerlValue,
2319        key: &str,
2320        line: usize,
2321    ) -> PerlResult<bool> {
2322        if let Some(r) = container.as_hash_ref() {
2323            return Ok(r.read().contains_key(key));
2324        }
2325        if let Some(b) = container.as_blessed_ref() {
2326            let data = b.data.read();
2327            if let Some(r) = data.as_hash_ref() {
2328                return Ok(r.read().contains_key(key));
2329            }
2330            if let Some(hm) = data.as_hash_map() {
2331                return Ok(hm.contains_key(key));
2332            }
2333            return Err(PerlError::runtime(
2334                "exists argument is not a HASH reference",
2335                line,
2336            ));
2337        }
2338        Err(PerlError::runtime(
2339            "exists argument is not a HASH reference",
2340            line,
2341        ))
2342    }
2343
2344    /// `delete $href->{k}` / `delete $obj->{k}` — same container rules as [`Self::exists_arrow_hash_element`].
2345    pub(crate) fn delete_arrow_hash_element(
2346        &self,
2347        container: PerlValue,
2348        key: &str,
2349        line: usize,
2350    ) -> PerlResult<PerlValue> {
2351        if let Some(r) = container.as_hash_ref() {
2352            return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2353        }
2354        if let Some(b) = container.as_blessed_ref() {
2355            let mut data = b.data.write();
2356            if let Some(r) = data.as_hash_ref() {
2357                return Ok(r.write().shift_remove(key).unwrap_or(PerlValue::UNDEF));
2358            }
2359            if let Some(mut map) = data.as_hash_map() {
2360                let v = map.shift_remove(key).unwrap_or(PerlValue::UNDEF);
2361                *data = PerlValue::hash(map);
2362                return Ok(v);
2363            }
2364            return Err(PerlError::runtime(
2365                "delete argument is not a HASH reference",
2366                line,
2367            ));
2368        }
2369        Err(PerlError::runtime(
2370            "delete argument is not a HASH reference",
2371            line,
2372        ))
2373    }
2374
2375    /// `exists $aref->[$i]` — plain array ref only (same index rules as [`Self::read_arrow_array_element`]).
2376    pub(crate) fn exists_arrow_array_element(
2377        &self,
2378        container: PerlValue,
2379        idx: i64,
2380        line: usize,
2381    ) -> PerlResult<bool> {
2382        if let Some(a) = container.as_array_ref() {
2383            let arr = a.read();
2384            let i = if idx < 0 {
2385                (arr.len() as i64 + idx) as usize
2386            } else {
2387                idx as usize
2388            };
2389            return Ok(i < arr.len());
2390        }
2391        Err(PerlError::runtime(
2392            "exists argument is not an ARRAY reference",
2393            line,
2394        ))
2395    }
2396
2397    /// `delete $aref->[$i]` — sets element to undef, returns previous value (Perl array `delete`).
2398    pub(crate) fn delete_arrow_array_element(
2399        &self,
2400        container: PerlValue,
2401        idx: i64,
2402        line: usize,
2403    ) -> PerlResult<PerlValue> {
2404        if let Some(a) = container.as_array_ref() {
2405            let mut arr = a.write();
2406            let i = if idx < 0 {
2407                (arr.len() as i64 + idx) as usize
2408            } else {
2409                idx as usize
2410            };
2411            if i >= arr.len() {
2412                return Ok(PerlValue::UNDEF);
2413            }
2414            let old = arr.get(i).cloned().unwrap_or(PerlValue::UNDEF);
2415            arr[i] = PerlValue::UNDEF;
2416            return Ok(old);
2417        }
2418        Err(PerlError::runtime(
2419            "delete argument is not an ARRAY reference",
2420            line,
2421        ))
2422    }
2423
2424    /// Paths from `@INC` for `require` / `use` (non-empty; defaults to `.` if unset).
2425    pub(crate) fn inc_directories(&self) -> Vec<String> {
2426        let mut v: Vec<String> = self
2427            .scope
2428            .get_array("INC")
2429            .into_iter()
2430            .map(|x| x.to_string())
2431            .filter(|s| !s.is_empty())
2432            .collect();
2433        if v.is_empty() {
2434            v.push(".".to_string());
2435        }
2436        v
2437    }
2438
2439    #[inline]
2440    pub(crate) fn strict_scalar_exempt(name: &str) -> bool {
2441        matches!(
2442            name,
2443            "_" | "0"
2444                | "!"
2445                | "@"
2446                | "/"
2447                | "\\"
2448                | ","
2449                | "."
2450                | "__PACKAGE__"
2451                | "$$"
2452                | "|"
2453                | "?"
2454                | "\""
2455                | "&"
2456                | "`"
2457                | "'"
2458                | "+"
2459                | "<"
2460                | ">"
2461                | "("
2462                | ")"
2463                | "]"
2464                | ";"
2465                | "ARGV"
2466                | "%"
2467                | "="
2468                | "-"
2469                | ":"
2470                | "*"
2471                | "INC"
2472        ) || name.chars().all(|c| c.is_ascii_digit())
2473            || name.starts_with('^')
2474            || (name.starts_with('#') && name.len() > 1)
2475    }
2476
2477    fn check_strict_scalar_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2478        if !self.strict_vars
2479            || Self::strict_scalar_exempt(name)
2480            || name.contains("::")
2481            || self.scope.scalar_binding_exists(name)
2482        {
2483            return Ok(());
2484        }
2485        Err(PerlError::runtime(
2486            format!(
2487                "Global symbol \"${}\" requires explicit package name (did you forget to declare \"my ${}\"?)",
2488                name, name
2489            ),
2490            line,
2491        )
2492        .into())
2493    }
2494
2495    fn check_strict_array_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2496        if !self.strict_vars || name.contains("::") || self.scope.array_binding_exists(name) {
2497            return Ok(());
2498        }
2499        Err(PerlError::runtime(
2500            format!(
2501                "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
2502                name, name
2503            ),
2504            line,
2505        )
2506        .into())
2507    }
2508
2509    fn check_strict_hash_var(&self, name: &str, line: usize) -> Result<(), FlowOrError> {
2510        // `%+`, `%-`, `%ENV`, `%SIG` etc. are special hashes, not subject to strict.
2511        if !self.strict_vars
2512            || name.contains("::")
2513            || self.scope.hash_binding_exists(name)
2514            || matches!(name, "+" | "-" | "ENV" | "SIG" | "!" | "^H")
2515        {
2516            return Ok(());
2517        }
2518        Err(PerlError::runtime(
2519            format!(
2520                "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
2521                name, name
2522            ),
2523            line,
2524        )
2525        .into())
2526    }
2527
2528    fn looks_like_version_only(spec: &str) -> bool {
2529        let t = spec.trim();
2530        !t.is_empty()
2531            && !t.contains('/')
2532            && !t.contains('\\')
2533            && !t.contains("::")
2534            && t.chars()
2535                .all(|c| c.is_ascii_digit() || c == '.' || c == '_' || c == 'v')
2536            && t.chars().any(|c| c.is_ascii_digit())
2537    }
2538
2539    fn module_spec_to_relpath(spec: &str) -> String {
2540        let t = spec.trim();
2541        if t.contains("::") {
2542            format!("{}.pm", t.replace("::", "/"))
2543        } else if t.ends_with(".pm") || t.ends_with(".pl") || t.contains('/') {
2544            t.replace('\\', "/")
2545        } else {
2546            format!("{}.pm", t)
2547        }
2548    }
2549
2550    /// `sub name` in `package P` → stash key `P::name` (otherwise `name` in `main`).
2551    /// `sub Q::name { }` is already fully qualified — do not prepend the current package.
2552    pub(crate) fn qualify_sub_key(&self, name: &str) -> String {
2553        if name.contains("::") {
2554            return name.to_string();
2555        }
2556        let pkg = self.current_package();
2557        if pkg.is_empty() || pkg == "main" {
2558            name.to_string()
2559        } else {
2560            format!("{}::{}", pkg, name)
2561        }
2562    }
2563
2564    /// `Undefined subroutine &name` (bare calls) with optional `strict subs` hint.
2565    pub(crate) fn undefined_subroutine_call_message(&self, name: &str) -> String {
2566        let mut msg = format!("Undefined subroutine &{}", name);
2567        if self.strict_subs {
2568            msg.push_str(
2569                " (strict subs: declare the sub or use a fully qualified name before calling)",
2570            );
2571        }
2572        msg
2573    }
2574
2575    /// `Undefined subroutine pkg::name` (coderef resolution) with optional `strict subs` hint.
2576    pub(crate) fn undefined_subroutine_resolve_message(&self, name: &str) -> String {
2577        let mut msg = format!("Undefined subroutine {}", self.qualify_sub_key(name));
2578        if self.strict_subs {
2579            msg.push_str(
2580                " (strict subs: declare the sub or use a fully qualified name before calling)",
2581            );
2582        }
2583        msg
2584    }
2585
2586    /// Where `use` imports a symbol: `main` → short name; otherwise `Pkg::sym`.
2587    fn import_alias_key(&self, short: &str) -> String {
2588        self.qualify_sub_key(short)
2589    }
2590
2591    /// `use Module qw()` / `use Module ()` — explicit empty list (not the same as `use Module`).
2592    fn is_explicit_empty_import_list(imports: &[Expr]) -> bool {
2593        if imports.len() == 1 {
2594            match &imports[0].kind {
2595                ExprKind::QW(ws) => return ws.is_empty(),
2596                // Parser: `use Carp ()` → one import that is an empty `List` (see `parse_use`).
2597                ExprKind::List(xs) => return xs.is_empty(),
2598                _ => {}
2599            }
2600        }
2601        false
2602    }
2603
2604    /// After `require`, copy `Module::export` → caller stash per `use` list.
2605    fn apply_module_import(
2606        &mut self,
2607        module: &str,
2608        imports: &[Expr],
2609        line: usize,
2610    ) -> PerlResult<()> {
2611        if imports.is_empty() {
2612            return self.import_all_from_module(module, line);
2613        }
2614        if Self::is_explicit_empty_import_list(imports) {
2615            return Ok(());
2616        }
2617        let names = Self::pragma_import_strings(imports, line)?;
2618        if names.is_empty() {
2619            return Ok(());
2620        }
2621        for name in names {
2622            self.import_one_symbol(module, &name, line)?;
2623        }
2624        Ok(())
2625    }
2626
2627    fn import_all_from_module(&mut self, module: &str, line: usize) -> PerlResult<()> {
2628        if module == "List::Util" {
2629            crate::list_util::ensure_list_util(self);
2630        }
2631        if let Some(lists) = self.module_export_lists.get(module) {
2632            let export: Vec<String> = lists.export.clone();
2633            for short in export {
2634                self.import_named_sub(module, &short, line)?;
2635            }
2636            return Ok(());
2637        }
2638        // No `our @EXPORT` recorded (legacy): import every top-level sub in the package.
2639        let prefix = format!("{}::", module);
2640        let keys: Vec<String> = self
2641            .subs
2642            .keys()
2643            .filter(|k| k.starts_with(&prefix) && !k[prefix.len()..].contains("::"))
2644            .cloned()
2645            .collect();
2646        for k in keys {
2647            let short = k[prefix.len()..].to_string();
2648            if let Some(sub) = self.subs.get(&k).cloned() {
2649                let alias = self.import_alias_key(&short);
2650                self.subs.insert(alias, sub);
2651            }
2652        }
2653        Ok(())
2654    }
2655
2656    /// Copy `Module::name` into the caller stash (`name` must exist as a sub).
2657    fn import_named_sub(&mut self, module: &str, short: &str, line: usize) -> PerlResult<()> {
2658        if module == "List::Util" {
2659            crate::list_util::ensure_list_util(self);
2660        }
2661        let qual = format!("{}::{}", module, short);
2662        let sub = self.subs.get(&qual).cloned().ok_or_else(|| {
2663            PerlError::runtime(
2664                format!(
2665                    "`{}` is not defined in module `{}` (expected `{}`)",
2666                    short, module, qual
2667                ),
2668                line,
2669            )
2670        })?;
2671        let alias = self.import_alias_key(short);
2672        self.subs.insert(alias, sub);
2673        Ok(())
2674    }
2675
2676    fn import_one_symbol(&mut self, module: &str, export: &str, line: usize) -> PerlResult<()> {
2677        if let Some(lists) = self.module_export_lists.get(module) {
2678            let allowed: HashSet<&str> = lists
2679                .export
2680                .iter()
2681                .map(|s| s.as_str())
2682                .chain(lists.export_ok.iter().map(|s| s.as_str()))
2683                .collect();
2684            if !allowed.contains(export) {
2685                return Err(PerlError::runtime(
2686                    format!(
2687                        "`{}` is not exported by `{}` (not in @EXPORT or @EXPORT_OK)",
2688                        export, module
2689                    ),
2690                    line,
2691                ));
2692            }
2693        }
2694        self.import_named_sub(module, export, line)
2695    }
2696
2697    /// After `our @EXPORT` / `our @EXPORT_OK` in a package, record lists for `use`.
2698    fn record_exporter_our_array_name(&mut self, name: &str, items: &[PerlValue]) {
2699        if name != "EXPORT" && name != "EXPORT_OK" {
2700            return;
2701        }
2702        let pkg = self.current_package();
2703        if pkg.is_empty() || pkg == "main" {
2704            return;
2705        }
2706        let names: Vec<String> = items.iter().map(|v| v.to_string()).collect();
2707        let ent = self.module_export_lists.entry(pkg).or_default();
2708        if name == "EXPORT" {
2709            ent.export = names;
2710        } else {
2711            ent.export_ok = names;
2712        }
2713    }
2714
2715    /// Resolve `foo` or `Foo::bar` against the subroutine stash (package-aware).
2716    /// Refresh [`PerlSub::closure_env`] for `name` from [`Scope::capture`] at the current stack
2717    /// (top-level `sub` at runtime and [`Op::BindSubClosure`] after preceding `my`/etc.).
2718    pub(crate) fn rebind_sub_closure(&mut self, name: &str) {
2719        let key = self.qualify_sub_key(name);
2720        let Some(sub) = self.subs.get(&key).cloned() else {
2721            return;
2722        };
2723        let captured = self.scope.capture();
2724        let closure_env = if captured.is_empty() {
2725            None
2726        } else {
2727            Some(captured)
2728        };
2729        let mut new_sub = (*sub).clone();
2730        new_sub.closure_env = closure_env;
2731        new_sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&new_sub);
2732        self.subs.insert(key, Arc::new(new_sub));
2733    }
2734
2735    pub(crate) fn resolve_sub_by_name(&self, name: &str) -> Option<Arc<PerlSub>> {
2736        if let Some(s) = self.subs.get(name) {
2737            return Some(s.clone());
2738        }
2739        if !name.contains("::") {
2740            let pkg = self.current_package();
2741            if !pkg.is_empty() && pkg != "main" {
2742                let mut q = String::with_capacity(pkg.len() + 2 + name.len());
2743                q.push_str(&pkg);
2744                q.push_str("::");
2745                q.push_str(name);
2746                return self.subs.get(&q).cloned();
2747            }
2748        }
2749        None
2750    }
2751
2752    /// `use Module VERSION LIST` — numeric `VERSION` is not part of the import list (Perl strips it
2753    /// before calling `import`).
2754    fn imports_after_leading_use_version(imports: &[Expr]) -> &[Expr] {
2755        if let Some(first) = imports.first() {
2756            if matches!(first.kind, ExprKind::Integer(_) | ExprKind::Float(_)) {
2757                return &imports[1..];
2758            }
2759        }
2760        imports
2761    }
2762
2763    /// Compile-time pragma import list (`'refs'`, `qw(refs subs)`, version integers).
2764    fn pragma_import_strings(imports: &[Expr], default_line: usize) -> PerlResult<Vec<String>> {
2765        let mut out = Vec::new();
2766        for e in imports {
2767            match &e.kind {
2768                ExprKind::String(s) => out.push(s.clone()),
2769                ExprKind::QW(ws) => out.extend(ws.iter().cloned()),
2770                ExprKind::Integer(n) => out.push(n.to_string()),
2771                // `use Env "@PATH"` / `use Env "$HOME"` — double-quoted string containing
2772                // a single interpolated variable.  Reconstruct the sigil+name form.
2773                ExprKind::InterpolatedString(parts) => {
2774                    let mut s = String::new();
2775                    for p in parts {
2776                        match p {
2777                            StringPart::Literal(l) => s.push_str(l),
2778                            StringPart::ScalarVar(v) => {
2779                                s.push('$');
2780                                s.push_str(v);
2781                            }
2782                            StringPart::ArrayVar(v) => {
2783                                s.push('@');
2784                                s.push_str(v);
2785                            }
2786                            _ => {
2787                                return Err(PerlError::runtime(
2788                                    "pragma import must be a compile-time string, qw(), or integer",
2789                                    e.line.max(default_line),
2790                                ));
2791                            }
2792                        }
2793                    }
2794                    out.push(s);
2795                }
2796                _ => {
2797                    return Err(PerlError::runtime(
2798                        "pragma import must be a compile-time string, qw(), or integer",
2799                        e.line.max(default_line),
2800                    ));
2801                }
2802            }
2803        }
2804        Ok(out)
2805    }
2806
2807    fn apply_use_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2808        if imports.is_empty() {
2809            self.strict_refs = true;
2810            self.strict_subs = true;
2811            self.strict_vars = true;
2812            return Ok(());
2813        }
2814        let names = Self::pragma_import_strings(imports, line)?;
2815        for name in names {
2816            match name.as_str() {
2817                "refs" => self.strict_refs = true,
2818                "subs" => self.strict_subs = true,
2819                "vars" => self.strict_vars = true,
2820                _ => {
2821                    return Err(PerlError::runtime(
2822                        format!("Unknown strict mode `{}`", name),
2823                        line,
2824                    ));
2825                }
2826            }
2827        }
2828        Ok(())
2829    }
2830
2831    fn apply_no_strict(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2832        if imports.is_empty() {
2833            self.strict_refs = false;
2834            self.strict_subs = false;
2835            self.strict_vars = false;
2836            return Ok(());
2837        }
2838        let names = Self::pragma_import_strings(imports, line)?;
2839        for name in names {
2840            match name.as_str() {
2841                "refs" => self.strict_refs = false,
2842                "subs" => self.strict_subs = false,
2843                "vars" => self.strict_vars = false,
2844                _ => {
2845                    return Err(PerlError::runtime(
2846                        format!("Unknown strict mode `{}`", name),
2847                        line,
2848                    ));
2849                }
2850            }
2851        }
2852        Ok(())
2853    }
2854
2855    fn apply_use_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2856        let items = Self::pragma_import_strings(imports, line)?;
2857        if items.is_empty() {
2858            return Err(PerlError::runtime(
2859                "use feature requires a feature name or bundle (e.g. qw(say) or :5.10)",
2860                line,
2861            ));
2862        }
2863        for item in items {
2864            let s = item.trim();
2865            if let Some(rest) = s.strip_prefix(':') {
2866                self.apply_feature_bundle(rest, line)?;
2867            } else {
2868                self.apply_feature_name(s, true, line)?;
2869            }
2870        }
2871        Ok(())
2872    }
2873
2874    fn apply_no_feature(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
2875        if imports.is_empty() {
2876            self.feature_bits = 0;
2877            return Ok(());
2878        }
2879        let items = Self::pragma_import_strings(imports, line)?;
2880        for item in items {
2881            let s = item.trim();
2882            if let Some(rest) = s.strip_prefix(':') {
2883                self.clear_feature_bundle(rest);
2884            } else {
2885                self.apply_feature_name(s, false, line)?;
2886            }
2887        }
2888        Ok(())
2889    }
2890
2891    fn apply_feature_bundle(&mut self, v: &str, line: usize) -> PerlResult<()> {
2892        let key = v.trim();
2893        match key {
2894            "5.10" | "5.010" | "5.10.0" => {
2895                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
2896            }
2897            "5.12" | "5.012" | "5.12.0" => {
2898                self.feature_bits |= FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS;
2899            }
2900            _ => {
2901                return Err(PerlError::runtime(
2902                    format!("unsupported feature bundle :{}", key),
2903                    line,
2904                ));
2905            }
2906        }
2907        Ok(())
2908    }
2909
2910    fn clear_feature_bundle(&mut self, v: &str) {
2911        let key = v.trim();
2912        if matches!(
2913            key,
2914            "5.10" | "5.010" | "5.10.0" | "5.12" | "5.012" | "5.12.0"
2915        ) {
2916            self.feature_bits &= !(FEAT_SAY | FEAT_SWITCH | FEAT_STATE | FEAT_UNICODE_STRINGS);
2917        }
2918    }
2919
2920    fn apply_feature_name(&mut self, name: &str, enable: bool, line: usize) -> PerlResult<()> {
2921        let bit = match name {
2922            "say" => FEAT_SAY,
2923            "state" => FEAT_STATE,
2924            "switch" => FEAT_SWITCH,
2925            "unicode_strings" => FEAT_UNICODE_STRINGS,
2926            // Features that stryke accepts as known but tracks no separate bit for —
2927            // either always-on, always-off, or syntactic sugar already enabled.
2928            // Keeps `use feature 'X'` from erroring on common Perl 5.20+ pragmas.
2929            "postderef"
2930            | "postderef_qq"
2931            | "evalbytes"
2932            | "current_sub"
2933            | "fc"
2934            | "lexical_subs"
2935            | "signatures"
2936            | "refaliasing"
2937            | "bitwise"
2938            | "isa"
2939            | "indirect"
2940            | "multidimensional"
2941            | "bareword_filehandles"
2942            | "try"
2943            | "defer"
2944            | "extra_paired_delimiters"
2945            | "module_true"
2946            | "class"
2947            | "array_base" => return Ok(()),
2948            _ => {
2949                return Err(PerlError::runtime(
2950                    format!("unknown feature `{}`", name),
2951                    line,
2952                ));
2953            }
2954        };
2955        if enable {
2956            self.feature_bits |= bit;
2957        } else {
2958            self.feature_bits &= !bit;
2959        }
2960        Ok(())
2961    }
2962
2963    /// `require EXPR` — load once, record `%INC`, return `1` on success.
2964    pub(crate) fn require_execute(&mut self, spec: &str, line: usize) -> PerlResult<PerlValue> {
2965        let t = spec.trim();
2966        if t.is_empty() {
2967            return Err(PerlError::runtime("require: empty argument", line));
2968        }
2969        match t {
2970            "strict" => {
2971                self.apply_use_strict(&[], line)?;
2972                return Ok(PerlValue::integer(1));
2973            }
2974            "utf8" => {
2975                self.utf8_pragma = true;
2976                return Ok(PerlValue::integer(1));
2977            }
2978            "feature" | "v5" => {
2979                return Ok(PerlValue::integer(1));
2980            }
2981            "warnings" => {
2982                self.warnings = true;
2983                return Ok(PerlValue::integer(1));
2984            }
2985            "threads" | "Thread::Pool" | "Parallel::ForkManager" => {
2986                return Ok(PerlValue::integer(1));
2987            }
2988            _ => {}
2989        }
2990        let p = Path::new(t);
2991        if p.is_absolute() {
2992            return self.require_absolute_path(p, line);
2993        }
2994        if t.starts_with("./") || t.starts_with("../") {
2995            return self.require_relative_path(p, line);
2996        }
2997        if Self::looks_like_version_only(t) {
2998            return Ok(PerlValue::integer(1));
2999        }
3000        let relpath = Self::module_spec_to_relpath(t);
3001        self.require_from_inc(&relpath, line)
3002    }
3003
3004    /// `%^HOOK` entries `require__before` / `require__after` (Perl 5.37+): coderef `(filename)`.
3005    fn invoke_require_hook(&mut self, key: &str, path: &str, line: usize) -> PerlResult<()> {
3006        let v = self.scope.get_hash_element("^HOOK", key);
3007        if v.is_undef() {
3008            return Ok(());
3009        }
3010        let Some(sub) = v.as_code_ref() else {
3011            return Ok(());
3012        };
3013        let r = self.call_sub(
3014            sub.as_ref(),
3015            vec![PerlValue::string(path.to_string())],
3016            WantarrayCtx::Scalar,
3017            line,
3018        );
3019        match r {
3020            Ok(_) => Ok(()),
3021            Err(FlowOrError::Error(e)) => Err(e),
3022            Err(FlowOrError::Flow(Flow::Return(_))) => Ok(()),
3023            Err(FlowOrError::Flow(other)) => Err(PerlError::runtime(
3024                format!(
3025                    "require hook {:?} returned unexpected control flow: {:?}",
3026                    key, other
3027                ),
3028                line,
3029            )),
3030        }
3031    }
3032
3033    fn require_absolute_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3034        let canon = path.canonicalize().unwrap_or_else(|_| path.to_path_buf());
3035        let key = canon.to_string_lossy().into_owned();
3036        if self.scope.exists_hash_element("INC", &key) {
3037            return Ok(PerlValue::integer(1));
3038        }
3039        self.invoke_require_hook("require__before", &key, line)?;
3040        let code = read_file_text_perl_compat(&canon).map_err(|e| {
3041            PerlError::runtime(
3042                format!("Can't open {} for reading: {}", canon.display(), e),
3043                line,
3044            )
3045        })?;
3046        let code = crate::data_section::strip_perl_end_marker(&code);
3047        self.scope
3048            .set_hash_element("INC", &key, PerlValue::string(key.clone()))?;
3049        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3050        let r = crate::parse_and_run_string_in_file(code, self, &key);
3051        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3052        r?;
3053        self.invoke_require_hook("require__after", &key, line)?;
3054        Ok(PerlValue::integer(1))
3055    }
3056
3057    fn require_relative_path(&mut self, path: &Path, line: usize) -> PerlResult<PerlValue> {
3058        if !path.exists() {
3059            return Err(PerlError::runtime(
3060                format!(
3061                    "Can't locate {} (relative path does not exist)",
3062                    path.display()
3063                ),
3064                line,
3065            ));
3066        }
3067        self.require_absolute_path(path, line)
3068    }
3069
3070    fn require_from_inc(&mut self, relpath: &str, line: usize) -> PerlResult<PerlValue> {
3071        if self.scope.exists_hash_element("INC", relpath) {
3072            return Ok(PerlValue::integer(1));
3073        }
3074        self.invoke_require_hook("require__before", relpath, line)?;
3075
3076        // Check virtual modules first (AOT bundles).
3077        if let Some(code) = self.virtual_modules.get(relpath).cloned() {
3078            let code = crate::data_section::strip_perl_end_marker(&code);
3079            self.scope.set_hash_element(
3080                "INC",
3081                relpath,
3082                PerlValue::string(format!("(virtual)/{}", relpath)),
3083            )?;
3084            let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3085            let r = crate::parse_and_run_string_in_file(code, self, relpath);
3086            let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3087            r?;
3088            self.invoke_require_hook("require__after", relpath, line)?;
3089            return Ok(PerlValue::integer(1));
3090        }
3091
3092        for dir in self.inc_directories() {
3093            let full = Path::new(&dir).join(relpath);
3094            if full.is_file() {
3095                let code = read_file_text_perl_compat(&full).map_err(|e| {
3096                    PerlError::runtime(
3097                        format!("Can't open {} for reading: {}", full.display(), e),
3098                        line,
3099                    )
3100                })?;
3101                let code = crate::data_section::strip_perl_end_marker(&code);
3102                let abs = full.canonicalize().unwrap_or(full);
3103                let abs_s = abs.to_string_lossy().into_owned();
3104                self.scope
3105                    .set_hash_element("INC", relpath, PerlValue::string(abs_s.clone()))?;
3106                let saved_pkg = self.scope.get_scalar("__PACKAGE__");
3107                let r = crate::parse_and_run_string_in_file(code, self, &abs_s);
3108                let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
3109                r?;
3110                self.invoke_require_hook("require__after", relpath, line)?;
3111                return Ok(PerlValue::integer(1));
3112            }
3113        }
3114        Err(PerlError::runtime(
3115            format!(
3116                "Can't locate {} in @INC (push paths onto @INC or use -I DIR)",
3117                relpath
3118            ),
3119            line,
3120        ))
3121    }
3122
3123    /// Register a virtual module (for AOT bundles). Path should be relative like "lib/foo.stk".
3124    pub fn register_virtual_module(&mut self, path: String, source: String) {
3125        self.virtual_modules.insert(path, source);
3126    }
3127
3128    /// Pragmas (`use strict 'refs'`, `use feature`) or load a `.pm` file (`use Foo::Bar`).
3129    pub(crate) fn exec_use_stmt(
3130        &mut self,
3131        module: &str,
3132        imports: &[Expr],
3133        line: usize,
3134    ) -> PerlResult<()> {
3135        match module {
3136            "strict" => self.apply_use_strict(imports, line),
3137            "utf8" => {
3138                if !imports.is_empty() {
3139                    return Err(PerlError::runtime("use utf8 takes no arguments", line));
3140                }
3141                self.utf8_pragma = true;
3142                Ok(())
3143            }
3144            "feature" => self.apply_use_feature(imports, line),
3145            "v5" => Ok(()),
3146            "warnings" => {
3147                self.warnings = true;
3148                Ok(())
3149            }
3150            "English" => {
3151                self.english_enabled = true;
3152                let args = Self::pragma_import_strings(imports, line)?;
3153                let no_match = args.iter().any(|a| a == "-no_match_vars");
3154                // Once match vars are exported (use English without -no_match_vars),
3155                // they stay available for the rest of the program — Perl exports them
3156                // into the caller's namespace and later pragmas cannot un-export them.
3157                if !no_match {
3158                    self.english_match_vars_ever_enabled = true;
3159                }
3160                self.english_no_match_vars = no_match && !self.english_match_vars_ever_enabled;
3161                Ok(())
3162            }
3163            "Env" => self.apply_use_env(imports, line),
3164            "open" => self.apply_use_open(imports, line),
3165            "constant" => self.apply_use_constant(imports, line),
3166            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3167            _ => {
3168                self.require_execute(module, line)?;
3169                let imports = Self::imports_after_leading_use_version(imports);
3170                self.apply_module_import(module, imports, line)?;
3171                Ok(())
3172            }
3173        }
3174    }
3175
3176    /// `no strict 'refs'`, `no warnings`, `no feature`, …
3177    pub(crate) fn exec_no_stmt(
3178        &mut self,
3179        module: &str,
3180        imports: &[Expr],
3181        line: usize,
3182    ) -> PerlResult<()> {
3183        match module {
3184            "strict" => self.apply_no_strict(imports, line),
3185            "utf8" => {
3186                if !imports.is_empty() {
3187                    return Err(PerlError::runtime("no utf8 takes no arguments", line));
3188                }
3189                self.utf8_pragma = false;
3190                Ok(())
3191            }
3192            "feature" => self.apply_no_feature(imports, line),
3193            "v5" => Ok(()),
3194            "warnings" => {
3195                self.warnings = false;
3196                Ok(())
3197            }
3198            "English" => {
3199                self.english_enabled = false;
3200                // Don't reset no_match_vars here — if match vars were ever enabled,
3201                // they persist (Perl's export cannot be un-exported).
3202                if !self.english_match_vars_ever_enabled {
3203                    self.english_no_match_vars = false;
3204                }
3205                Ok(())
3206            }
3207            "open" => {
3208                self.open_pragma_utf8 = false;
3209                Ok(())
3210            }
3211            "threads" | "Thread::Pool" | "Parallel::ForkManager" => Ok(()),
3212            _ => Ok(()),
3213        }
3214    }
3215
3216    /// `use Env qw(@PATH)` / `use Env '@PATH'` — populate `%ENV`-style paths from the process environment.
3217    fn apply_use_env(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3218        let names = Self::pragma_import_strings(imports, line)?;
3219        for n in names {
3220            let key = n.trim_start_matches('@');
3221            if key.eq_ignore_ascii_case("PATH") {
3222                let path_env = std::env::var("PATH").unwrap_or_default();
3223                let path_vec: Vec<PerlValue> = std::env::split_paths(&path_env)
3224                    .map(|p| PerlValue::string(p.to_string_lossy().into_owned()))
3225                    .collect();
3226                let aname = self.stash_array_name_for_package("PATH");
3227                self.scope.declare_array(&aname, path_vec);
3228            }
3229        }
3230        Ok(())
3231    }
3232
3233    /// `use open ':encoding(UTF-8)'`, `qw(:std :encoding(UTF-8))`, `:utf8`, etc.
3234    fn apply_use_open(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3235        let items = Self::pragma_import_strings(imports, line)?;
3236        for item in items {
3237            let s = item.trim();
3238            if s.eq_ignore_ascii_case(":utf8") || s == ":std" || s.eq_ignore_ascii_case("std") {
3239                self.open_pragma_utf8 = true;
3240                continue;
3241            }
3242            if let Some(rest) = s.strip_prefix(":encoding(") {
3243                if let Some(inner) = rest.strip_suffix(')') {
3244                    if inner.eq_ignore_ascii_case("UTF-8") || inner.eq_ignore_ascii_case("utf8") {
3245                        self.open_pragma_utf8 = true;
3246                    }
3247                }
3248            }
3249        }
3250        Ok(())
3251    }
3252
3253    /// `use constant NAME => EXPR` / `use constant 1.03` — do not load core `constant.pm` (it uses syntax we do not parse yet).
3254    fn apply_use_constant(&mut self, imports: &[Expr], line: usize) -> PerlResult<()> {
3255        if imports.is_empty() {
3256            return Ok(());
3257        }
3258        // `use constant 1.03;` — version check only (ignored here).
3259        if imports.len() == 1 {
3260            match &imports[0].kind {
3261                ExprKind::Float(_) | ExprKind::Integer(_) => return Ok(()),
3262                _ => {}
3263            }
3264        }
3265        for imp in imports {
3266            match &imp.kind {
3267                ExprKind::List(items) => {
3268                    if items.len() % 2 != 0 {
3269                        return Err(PerlError::runtime(
3270                            format!(
3271                                "use constant: expected even-length list of NAME => VALUE pairs, got {}",
3272                                items.len()
3273                            ),
3274                            line,
3275                        ));
3276                    }
3277                    let mut i = 0;
3278                    while i < items.len() {
3279                        let name = match &items[i].kind {
3280                            ExprKind::String(s) => s.clone(),
3281                            _ => {
3282                                return Err(PerlError::runtime(
3283                                    "use constant: constant name must be a string literal",
3284                                    line,
3285                                ));
3286                            }
3287                        };
3288                        let val = match self.eval_expr(&items[i + 1]) {
3289                            Ok(v) => v,
3290                            Err(FlowOrError::Error(e)) => return Err(e),
3291                            Err(FlowOrError::Flow(_)) => {
3292                                return Err(PerlError::runtime(
3293                                    "use constant: unexpected control flow in initializer",
3294                                    line,
3295                                ));
3296                            }
3297                        };
3298                        self.install_constant_sub(&name, &val, line)?;
3299                        i += 2;
3300                    }
3301                }
3302                _ => {
3303                    return Err(PerlError::runtime(
3304                        "use constant: expected list of NAME => VALUE pairs",
3305                        line,
3306                    ));
3307                }
3308            }
3309        }
3310        Ok(())
3311    }
3312
3313    fn install_constant_sub(&mut self, name: &str, val: &PerlValue, line: usize) -> PerlResult<()> {
3314        let key = self.qualify_sub_key(name);
3315        let ret_expr = self.perl_value_to_const_literal_expr(val, line)?;
3316        let body = vec![Statement {
3317            label: None,
3318            kind: StmtKind::Return(Some(ret_expr)),
3319            line,
3320        }];
3321        self.subs.insert(
3322            key.clone(),
3323            Arc::new(PerlSub {
3324                name: key,
3325                params: vec![],
3326                body,
3327                prototype: None,
3328                closure_env: None,
3329                fib_like: None,
3330            }),
3331        );
3332        Ok(())
3333    }
3334
3335    /// Build a literal expression for `return EXPR` in a constant sub (scalar/aggregate only).
3336    fn perl_value_to_const_literal_expr(&self, v: &PerlValue, line: usize) -> PerlResult<Expr> {
3337        if v.is_undef() {
3338            return Ok(Expr {
3339                kind: ExprKind::Undef,
3340                line,
3341            });
3342        }
3343        if let Some(n) = v.as_integer() {
3344            return Ok(Expr {
3345                kind: ExprKind::Integer(n),
3346                line,
3347            });
3348        }
3349        if let Some(f) = v.as_float() {
3350            return Ok(Expr {
3351                kind: ExprKind::Float(f),
3352                line,
3353            });
3354        }
3355        if let Some(s) = v.as_str() {
3356            return Ok(Expr {
3357                kind: ExprKind::String(s),
3358                line,
3359            });
3360        }
3361        if let Some(arr) = v.as_array_vec() {
3362            let mut elems = Vec::with_capacity(arr.len());
3363            for e in &arr {
3364                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3365            }
3366            return Ok(Expr {
3367                kind: ExprKind::ArrayRef(elems),
3368                line,
3369            });
3370        }
3371        if let Some(h) = v.as_hash_map() {
3372            let mut pairs = Vec::with_capacity(h.len());
3373            for (k, vv) in h.iter() {
3374                pairs.push((
3375                    Expr {
3376                        kind: ExprKind::String(k.clone()),
3377                        line,
3378                    },
3379                    self.perl_value_to_const_literal_expr(vv, line)?,
3380                ));
3381            }
3382            return Ok(Expr {
3383                kind: ExprKind::HashRef(pairs),
3384                line,
3385            });
3386        }
3387        if let Some(aref) = v.as_array_ref() {
3388            let arr = aref.read();
3389            let mut elems = Vec::with_capacity(arr.len());
3390            for e in arr.iter() {
3391                elems.push(self.perl_value_to_const_literal_expr(e, line)?);
3392            }
3393            return Ok(Expr {
3394                kind: ExprKind::ArrayRef(elems),
3395                line,
3396            });
3397        }
3398        if let Some(href) = v.as_hash_ref() {
3399            let h = href.read();
3400            let mut pairs = Vec::with_capacity(h.len());
3401            for (k, vv) in h.iter() {
3402                pairs.push((
3403                    Expr {
3404                        kind: ExprKind::String(k.clone()),
3405                        line,
3406                    },
3407                    self.perl_value_to_const_literal_expr(vv, line)?,
3408                ));
3409            }
3410            return Ok(Expr {
3411                kind: ExprKind::HashRef(pairs),
3412                line,
3413            });
3414        }
3415        Err(PerlError::runtime(
3416            format!("use constant: unsupported value type ({v:?})"),
3417            line,
3418        ))
3419    }
3420
3421    /// Register subs, run `use` in source order, collect `BEGIN`/`END` (before `BEGIN` execution).
3422    pub(crate) fn prepare_program_top_level(&mut self, program: &Program) -> PerlResult<()> {
3423        if crate::list_util::program_needs_list_util(program) {
3424            crate::list_util::ensure_list_util(self);
3425        }
3426        for stmt in &program.statements {
3427            match &stmt.kind {
3428                StmtKind::Package { name } => {
3429                    let _ = self
3430                        .scope
3431                        .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
3432                }
3433                StmtKind::SubDecl {
3434                    name,
3435                    params,
3436                    body,
3437                    prototype,
3438                } => {
3439                    let key = self.qualify_sub_key(name);
3440                    let mut sub = PerlSub {
3441                        name: name.clone(),
3442                        params: params.clone(),
3443                        body: body.clone(),
3444                        closure_env: None,
3445                        prototype: prototype.clone(),
3446                        fib_like: None,
3447                    };
3448                    sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
3449                    self.subs.insert(key, Arc::new(sub));
3450                }
3451                StmtKind::UsePerlVersion { .. } => {}
3452                StmtKind::Use { module, imports } => {
3453                    self.exec_use_stmt(module, imports, stmt.line)?;
3454                }
3455                StmtKind::UseOverload { pairs } => {
3456                    self.install_use_overload_pairs(pairs);
3457                }
3458                StmtKind::FormatDecl { name, lines } => {
3459                    self.install_format_decl(name, lines, stmt.line)?;
3460                }
3461                StmtKind::No { module, imports } => {
3462                    self.exec_no_stmt(module, imports, stmt.line)?;
3463                }
3464                StmtKind::Begin(block) => self.begin_blocks.push(block.clone()),
3465                StmtKind::UnitCheck(block) => self.unit_check_blocks.push(block.clone()),
3466                StmtKind::Check(block) => self.check_blocks.push(block.clone()),
3467                StmtKind::Init(block) => self.init_blocks.push(block.clone()),
3468                StmtKind::End(block) => self.end_blocks.push(block.clone()),
3469                _ => {}
3470            }
3471        }
3472        Ok(())
3473    }
3474
3475    /// Install the `DATA` handle from a script `__DATA__` section (bytes after the marker line).
3476    pub fn install_data_handle(&mut self, data: Vec<u8>) {
3477        self.input_handles.insert(
3478            "DATA".to_string(),
3479            BufReader::new(Box::new(Cursor::new(data)) as Box<dyn Read + Send>),
3480        );
3481    }
3482
3483    /// `open` and VM `BuiltinId::Open`. `file_opt` is the evaluated third argument when present.
3484    ///
3485    /// Two-arg `open $fh, EXPR` with a single string: Perl treats a leading `|` as pipe-to-command
3486    /// (`|-`) and a trailing `|` as pipe-from-command (`-|`), both via `sh -c` / `cmd /C` (see
3487    /// [`piped_shell_command`]).
3488    pub(crate) fn open_builtin_execute(
3489        &mut self,
3490        handle_name: String,
3491        mode_s: String,
3492        file_opt: Option<String>,
3493        line: usize,
3494    ) -> PerlResult<PerlValue> {
3495        // Perl two-arg `open $fh, EXPR` when EXPR is a single string:
3496        // - leading `|`  → pipe to command (write to child's stdin)
3497        // - trailing `|` → pipe from command (read child's stdout)
3498        // (Must run before `<` / `>` so `"| cmd"` is not treated as a filename.)
3499        let (actual_mode, path) = if let Some(f) = file_opt {
3500            (mode_s, f)
3501        } else {
3502            let trimmed = mode_s.trim();
3503            if let Some(rest) = trimmed.strip_prefix('|') {
3504                ("|-".to_string(), rest.trim_start().to_string())
3505            } else if trimmed.ends_with('|') {
3506                let mut cmd = trimmed.to_string();
3507                cmd.pop(); // trailing `|` that selects pipe-from-command
3508                ("-|".to_string(), cmd.trim_end().to_string())
3509            } else if let Some(rest) = trimmed.strip_prefix(">>") {
3510                (">>".to_string(), rest.trim().to_string())
3511            } else if let Some(rest) = trimmed.strip_prefix('>') {
3512                (">".to_string(), rest.trim().to_string())
3513            } else if let Some(rest) = trimmed.strip_prefix('<') {
3514                ("<".to_string(), rest.trim().to_string())
3515            } else {
3516                ("<".to_string(), trimmed.to_string())
3517            }
3518        };
3519        let handle_return = handle_name.clone();
3520        match actual_mode.as_str() {
3521            "-|" => {
3522                let mut cmd = piped_shell_command(&path);
3523                cmd.stdout(Stdio::piped());
3524                let mut child = cmd.spawn().map_err(|e| {
3525                    self.apply_io_error_to_errno(&e);
3526                    PerlError::runtime(format!("Can't open pipe from command: {}", e), line)
3527                })?;
3528                let stdout = child
3529                    .stdout
3530                    .take()
3531                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdout", line))?;
3532                self.input_handles
3533                    .insert(handle_name.clone(), BufReader::new(Box::new(stdout)));
3534                self.pipe_children.insert(handle_name, child);
3535            }
3536            "|-" => {
3537                let mut cmd = piped_shell_command(&path);
3538                cmd.stdin(Stdio::piped());
3539                let mut child = cmd.spawn().map_err(|e| {
3540                    self.apply_io_error_to_errno(&e);
3541                    PerlError::runtime(format!("Can't open pipe to command: {}", e), line)
3542                })?;
3543                let stdin = child
3544                    .stdin
3545                    .take()
3546                    .ok_or_else(|| PerlError::runtime("pipe: child has no stdin", line))?;
3547                self.output_handles
3548                    .insert(handle_name.clone(), Box::new(stdin));
3549                self.pipe_children.insert(handle_name, child);
3550            }
3551            "<" => {
3552                let file = std::fs::File::open(&path).map_err(|e| {
3553                    self.apply_io_error_to_errno(&e);
3554                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3555                })?;
3556                let shared = Arc::new(Mutex::new(file));
3557                self.io_file_slots
3558                    .insert(handle_name.clone(), Arc::clone(&shared));
3559                self.input_handles.insert(
3560                    handle_name.clone(),
3561                    BufReader::new(Box::new(IoSharedFile(Arc::clone(&shared)))),
3562                );
3563            }
3564            ">" => {
3565                let file = std::fs::File::create(&path).map_err(|e| {
3566                    self.apply_io_error_to_errno(&e);
3567                    PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3568                })?;
3569                let shared = Arc::new(Mutex::new(file));
3570                self.io_file_slots
3571                    .insert(handle_name.clone(), Arc::clone(&shared));
3572                self.output_handles.insert(
3573                    handle_name.clone(),
3574                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3575                );
3576            }
3577            ">>" => {
3578                let file = std::fs::OpenOptions::new()
3579                    .append(true)
3580                    .create(true)
3581                    .open(&path)
3582                    .map_err(|e| {
3583                        self.apply_io_error_to_errno(&e);
3584                        PerlError::runtime(format!("Can't open '{}': {}", path, e), line)
3585                    })?;
3586                let shared = Arc::new(Mutex::new(file));
3587                self.io_file_slots
3588                    .insert(handle_name.clone(), Arc::clone(&shared));
3589                self.output_handles.insert(
3590                    handle_name.clone(),
3591                    Box::new(IoSharedFileWrite(Arc::clone(&shared))),
3592                );
3593            }
3594            _ => {
3595                return Err(PerlError::runtime(
3596                    format!("Unknown open mode '{}'", actual_mode),
3597                    line,
3598                ));
3599            }
3600        }
3601        Ok(PerlValue::io_handle(handle_return))
3602    }
3603
3604    /// `group_by` / `chunk_by` — consecutive runs where the key (block or `EXPR` with `$_`)
3605    /// matches the previous key under [`PerlValue::str_eq`]. Returns a list of arrayrefs
3606    /// (same outer shape as `chunked`).
3607    pub(crate) fn eval_chunk_by_builtin(
3608        &mut self,
3609        key_spec: &Expr,
3610        list_expr: &Expr,
3611        ctx: WantarrayCtx,
3612        line: usize,
3613    ) -> ExecResult {
3614        let list = self.eval_expr_ctx(list_expr, WantarrayCtx::List)?.to_list();
3615        let chunks = match &key_spec.kind {
3616            ExprKind::CodeRef { .. } => {
3617                let cr = self.eval_expr(key_spec)?;
3618                let Some(sub) = cr.as_code_ref() else {
3619                    return Err(PerlError::runtime(
3620                        "group_by/chunk_by: first argument must be { BLOCK }",
3621                        line,
3622                    )
3623                    .into());
3624                };
3625                let sub = sub.clone();
3626                let mut chunks: Vec<PerlValue> = Vec::new();
3627                let mut run: Vec<PerlValue> = Vec::new();
3628                let mut prev_key: Option<PerlValue> = None;
3629                for item in list {
3630                    self.scope.set_topic(item.clone());
3631                    let key = match self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line) {
3632                        Ok(k) => k,
3633                        Err(FlowOrError::Error(e)) => return Err(FlowOrError::Error(e)),
3634                        Err(FlowOrError::Flow(Flow::Return(v))) => v,
3635                        Err(_) => PerlValue::UNDEF,
3636                    };
3637                    match &prev_key {
3638                        None => {
3639                            run.push(item);
3640                            prev_key = Some(key);
3641                        }
3642                        Some(pk) => {
3643                            if key.str_eq(pk) {
3644                                run.push(item);
3645                            } else {
3646                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3647                                    std::mem::take(&mut run),
3648                                ))));
3649                                run.push(item);
3650                                prev_key = Some(key);
3651                            }
3652                        }
3653                    }
3654                }
3655                if !run.is_empty() {
3656                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3657                }
3658                chunks
3659            }
3660            _ => {
3661                let mut chunks: Vec<PerlValue> = Vec::new();
3662                let mut run: Vec<PerlValue> = Vec::new();
3663                let mut prev_key: Option<PerlValue> = None;
3664                for item in list {
3665                    self.scope.set_topic(item.clone());
3666                    let key = self.eval_expr_ctx(key_spec, WantarrayCtx::Scalar)?;
3667                    match &prev_key {
3668                        None => {
3669                            run.push(item);
3670                            prev_key = Some(key);
3671                        }
3672                        Some(pk) => {
3673                            if key.str_eq(pk) {
3674                                run.push(item);
3675                            } else {
3676                                chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(
3677                                    std::mem::take(&mut run),
3678                                ))));
3679                                run.push(item);
3680                                prev_key = Some(key);
3681                            }
3682                        }
3683                    }
3684                }
3685                if !run.is_empty() {
3686                    chunks.push(PerlValue::array_ref(Arc::new(RwLock::new(run))));
3687                }
3688                chunks
3689            }
3690        };
3691        Ok(match ctx {
3692            WantarrayCtx::List => PerlValue::array(chunks),
3693            WantarrayCtx::Scalar => PerlValue::integer(chunks.len() as i64),
3694            WantarrayCtx::Void => PerlValue::UNDEF,
3695        })
3696    }
3697
3698    /// `take_while` / `drop_while` / `tap` / `peek` — block + list as [`ExprKind::FuncCall`].
3699    pub(crate) fn list_higher_order_block_builtin(
3700        &mut self,
3701        name: &str,
3702        args: &[PerlValue],
3703        line: usize,
3704    ) -> PerlResult<PerlValue> {
3705        match self.list_higher_order_block_builtin_exec(name, args, line) {
3706            Ok(v) => Ok(v),
3707            Err(FlowOrError::Error(e)) => Err(e),
3708            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
3709            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
3710                format!("{name}: unsupported control flow in block"),
3711                line,
3712            )),
3713        }
3714    }
3715
3716    fn list_higher_order_block_builtin_exec(
3717        &mut self,
3718        name: &str,
3719        args: &[PerlValue],
3720        line: usize,
3721    ) -> ExecResult {
3722        if args.is_empty() {
3723            return Err(
3724                PerlError::runtime(format!("{name}: expected {{ BLOCK }}, LIST"), line).into(),
3725            );
3726        }
3727        let Some(sub) = args[0].as_code_ref() else {
3728            return Err(PerlError::runtime(
3729                format!("{name}: first argument must be {{ BLOCK }}"),
3730                line,
3731            )
3732            .into());
3733        };
3734        let sub = sub.clone();
3735        let items: Vec<PerlValue> = args[1..].to_vec();
3736        if matches!(name, "tap" | "peek") && items.len() == 1 {
3737            if let Some(p) = items[0].as_pipeline() {
3738                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
3739                return Ok(PerlValue::pipeline(Arc::clone(&p)));
3740            }
3741            let v = &items[0];
3742            if v.is_iterator() || v.as_array_vec().is_some() {
3743                let source = crate::map_stream::into_pull_iter(v.clone());
3744                let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
3745                return Ok(PerlValue::iterator(Arc::new(
3746                    crate::map_stream::TapIterator::new(
3747                        source,
3748                        sub,
3749                        self.subs.clone(),
3750                        capture,
3751                        atomic_arrays,
3752                        atomic_hashes,
3753                    ),
3754                )));
3755            }
3756        }
3757        // Streaming optimization disabled for these functions because the pre-captured
3758        // coderef from args[0] has its closure_env populated at parse time, which causes
3759        // $_ to get stale values on subsequent calls. These functions work correctly in
3760        // the non-streaming eager path below.
3761        let wa = self.wantarray_kind;
3762        match name {
3763            "take_while" => {
3764                let mut out = Vec::new();
3765                for item in items {
3766                    self.scope_push_hook();
3767                    self.scope.set_topic(item.clone());
3768                    let pred = self.exec_block(&sub.body)?;
3769                    self.scope_pop_hook();
3770                    if !pred.is_true() {
3771                        break;
3772                    }
3773                    out.push(item);
3774                }
3775                Ok(match wa {
3776                    WantarrayCtx::List => PerlValue::array(out),
3777                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
3778                    WantarrayCtx::Void => PerlValue::UNDEF,
3779                })
3780            }
3781            "drop_while" | "skip_while" => {
3782                let mut i = 0usize;
3783                while i < items.len() {
3784                    self.scope_push_hook();
3785                    self.scope.set_topic(items[i].clone());
3786                    let pred = self.exec_block(&sub.body)?;
3787                    self.scope_pop_hook();
3788                    if !pred.is_true() {
3789                        break;
3790                    }
3791                    i += 1;
3792                }
3793                let rest = items[i..].to_vec();
3794                Ok(match wa {
3795                    WantarrayCtx::List => PerlValue::array(rest),
3796                    WantarrayCtx::Scalar => PerlValue::integer(rest.len() as i64),
3797                    WantarrayCtx::Void => PerlValue::UNDEF,
3798                })
3799            }
3800            "reject" => {
3801                let mut out = Vec::new();
3802                for item in items {
3803                    self.scope_push_hook();
3804                    self.scope.set_topic(item.clone());
3805                    let pred = self.exec_block(&sub.body)?;
3806                    self.scope_pop_hook();
3807                    if !pred.is_true() {
3808                        out.push(item);
3809                    }
3810                }
3811                Ok(match wa {
3812                    WantarrayCtx::List => PerlValue::array(out),
3813                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
3814                    WantarrayCtx::Void => PerlValue::UNDEF,
3815                })
3816            }
3817            "tap" | "peek" => {
3818                let _ = self.call_sub(&sub, items.clone(), WantarrayCtx::Void, line)?;
3819                Ok(match wa {
3820                    WantarrayCtx::List => PerlValue::array(items),
3821                    WantarrayCtx::Scalar => PerlValue::integer(items.len() as i64),
3822                    WantarrayCtx::Void => PerlValue::UNDEF,
3823                })
3824            }
3825            "partition" => {
3826                let mut yes = Vec::new();
3827                let mut no = Vec::new();
3828                for item in items {
3829                    self.scope.set_topic(item.clone());
3830                    let pred = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3831                    if pred.is_true() {
3832                        yes.push(item);
3833                    } else {
3834                        no.push(item);
3835                    }
3836                }
3837                let yes_ref = PerlValue::array_ref(Arc::new(RwLock::new(yes)));
3838                let no_ref = PerlValue::array_ref(Arc::new(RwLock::new(no)));
3839                Ok(match wa {
3840                    WantarrayCtx::List => PerlValue::array(vec![yes_ref, no_ref]),
3841                    WantarrayCtx::Scalar => PerlValue::integer(2),
3842                    WantarrayCtx::Void => PerlValue::UNDEF,
3843                })
3844            }
3845            "min_by" => {
3846                let mut best: Option<(PerlValue, PerlValue)> = None;
3847                for item in items {
3848                    self.scope.set_topic(item.clone());
3849                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3850                    best = Some(match best {
3851                        None => (item, key),
3852                        Some((bv, bk)) => {
3853                            if key.num_cmp(&bk) == std::cmp::Ordering::Less {
3854                                (item, key)
3855                            } else {
3856                                (bv, bk)
3857                            }
3858                        }
3859                    });
3860                }
3861                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
3862            }
3863            "max_by" => {
3864                let mut best: Option<(PerlValue, PerlValue)> = None;
3865                for item in items {
3866                    self.scope.set_topic(item.clone());
3867                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3868                    best = Some(match best {
3869                        None => (item, key),
3870                        Some((bv, bk)) => {
3871                            if key.num_cmp(&bk) == std::cmp::Ordering::Greater {
3872                                (item, key)
3873                            } else {
3874                                (bv, bk)
3875                            }
3876                        }
3877                    });
3878                }
3879                Ok(best.map(|(v, _)| v).unwrap_or(PerlValue::UNDEF))
3880            }
3881            "zip_with" => {
3882                // zip_with { BLOCK } \@a, \@b — apply block to paired elements
3883                // Flatten items, then treat each array ref/binding as a separate list.
3884                let flat: Vec<PerlValue> = items.into_iter().flat_map(|a| a.to_list()).collect();
3885                let refs: Vec<Vec<PerlValue>> = flat
3886                    .iter()
3887                    .map(|el| {
3888                        if let Some(ar) = el.as_array_ref() {
3889                            ar.read().clone()
3890                        } else if let Some(name) = el.as_array_binding_name() {
3891                            self.scope.get_array(&name)
3892                        } else {
3893                            vec![el.clone()]
3894                        }
3895                    })
3896                    .collect();
3897                let max_len = refs.iter().map(|l| l.len()).max().unwrap_or(0);
3898                let mut out = Vec::with_capacity(max_len);
3899                for i in 0..max_len {
3900                    let pair: Vec<PerlValue> = refs
3901                        .iter()
3902                        .map(|l| l.get(i).cloned().unwrap_or(PerlValue::UNDEF))
3903                        .collect();
3904                    let result = self.call_sub(&sub, pair, WantarrayCtx::Scalar, line)?;
3905                    out.push(result);
3906                }
3907                Ok(match wa {
3908                    WantarrayCtx::List => PerlValue::array(out),
3909                    WantarrayCtx::Scalar => PerlValue::integer(out.len() as i64),
3910                    WantarrayCtx::Void => PerlValue::UNDEF,
3911                })
3912            }
3913            "count_by" => {
3914                let mut counts = indexmap::IndexMap::new();
3915                for item in items {
3916                    self.scope.set_topic(item.clone());
3917                    let key = self.call_sub(&sub, vec![], WantarrayCtx::Scalar, line)?;
3918                    let k = key.to_string();
3919                    let entry = counts.entry(k).or_insert(PerlValue::integer(0));
3920                    *entry = PerlValue::integer(entry.to_int() + 1);
3921                }
3922                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(counts))))
3923            }
3924            _ => Err(PerlError::runtime(
3925                format!("internal: unknown list block builtin `{name}`"),
3926                line,
3927            )
3928            .into()),
3929        }
3930    }
3931
3932    /// `rmdir LIST` — remove empty directories; returns count removed.
3933    pub(crate) fn builtin_rmdir_execute(
3934        &mut self,
3935        args: &[PerlValue],
3936        _line: usize,
3937    ) -> PerlResult<PerlValue> {
3938        let mut count = 0i64;
3939        for a in args {
3940            let p = a.to_string();
3941            if p.is_empty() {
3942                continue;
3943            }
3944            if std::fs::remove_dir(&p).is_ok() {
3945                count += 1;
3946            }
3947        }
3948        Ok(PerlValue::integer(count))
3949    }
3950
3951    /// `touch FILE, ...` — create if absent, update timestamps to now.
3952    pub(crate) fn builtin_touch_execute(
3953        &mut self,
3954        args: &[PerlValue],
3955        _line: usize,
3956    ) -> PerlResult<PerlValue> {
3957        let paths: Vec<String> = args.iter().map(|v| v.to_string()).collect();
3958        Ok(PerlValue::integer(crate::perl_fs::touch_paths(&paths)))
3959    }
3960
3961    /// `utime ATIME, MTIME, LIST`
3962    pub(crate) fn builtin_utime_execute(
3963        &mut self,
3964        args: &[PerlValue],
3965        line: usize,
3966    ) -> PerlResult<PerlValue> {
3967        if args.len() < 3 {
3968            return Err(PerlError::runtime(
3969                "utime requires at least three arguments (atime, mtime, files...)",
3970                line,
3971            ));
3972        }
3973        let at = args[0].to_int();
3974        let mt = args[1].to_int();
3975        let paths: Vec<String> = args.iter().skip(2).map(|v| v.to_string()).collect();
3976        let n = crate::perl_fs::utime_paths(at, mt, &paths);
3977        #[cfg(not(unix))]
3978        if !paths.is_empty() && n == 0 {
3979            return Err(PerlError::runtime(
3980                "utime is not supported on this platform",
3981                line,
3982            ));
3983        }
3984        Ok(PerlValue::integer(n))
3985    }
3986
3987    /// `umask EXPR` / `umask()` — returns previous mask when setting; current mask when called with no arguments.
3988    pub(crate) fn builtin_umask_execute(
3989        &mut self,
3990        args: &[PerlValue],
3991        line: usize,
3992    ) -> PerlResult<PerlValue> {
3993        #[cfg(unix)]
3994        {
3995            let _ = line;
3996            if args.is_empty() {
3997                let cur = unsafe { libc::umask(0) };
3998                unsafe { libc::umask(cur) };
3999                return Ok(PerlValue::integer(cur as i64));
4000            }
4001            let new_m = args[0].to_int() as libc::mode_t;
4002            let old = unsafe { libc::umask(new_m) };
4003            Ok(PerlValue::integer(old as i64))
4004        }
4005        #[cfg(not(unix))]
4006        {
4007            let _ = args;
4008            Err(PerlError::runtime(
4009                "umask is not supported on this platform",
4010                line,
4011            ))
4012        }
4013    }
4014
4015    /// `getcwd` — current directory or undef on failure.
4016    pub(crate) fn builtin_getcwd_execute(
4017        &mut self,
4018        args: &[PerlValue],
4019        line: usize,
4020    ) -> PerlResult<PerlValue> {
4021        if !args.is_empty() {
4022            return Err(PerlError::runtime("getcwd takes no arguments", line));
4023        }
4024        match std::env::current_dir() {
4025            Ok(p) => Ok(PerlValue::string(p.to_string_lossy().into_owned())),
4026            Err(e) => {
4027                self.apply_io_error_to_errno(&e);
4028                Ok(PerlValue::UNDEF)
4029            }
4030        }
4031    }
4032
4033    /// `realpath PATH` — [`std::fs::canonicalize`]; sets `$!` / errno on failure, returns undef.
4034    pub(crate) fn builtin_realpath_execute(
4035        &mut self,
4036        args: &[PerlValue],
4037        line: usize,
4038    ) -> PerlResult<PerlValue> {
4039        let path = args
4040            .first()
4041            .ok_or_else(|| PerlError::runtime("realpath: need path", line))?
4042            .to_string();
4043        if path.is_empty() {
4044            return Err(PerlError::runtime("realpath: need path", line));
4045        }
4046        match crate::perl_fs::realpath_resolved(&path) {
4047            Ok(s) => Ok(PerlValue::string(s)),
4048            Err(e) => {
4049                self.apply_io_error_to_errno(&e);
4050                Ok(PerlValue::UNDEF)
4051            }
4052        }
4053    }
4054
4055    /// `pipe READHANDLE, WRITEHANDLE` — install OS pipe ends as buffered read / write handles (Unix).
4056    pub(crate) fn builtin_pipe_execute(
4057        &mut self,
4058        args: &[PerlValue],
4059        line: usize,
4060    ) -> PerlResult<PerlValue> {
4061        if args.len() != 2 {
4062            return Err(PerlError::runtime(
4063                "pipe requires exactly two arguments",
4064                line,
4065            ));
4066        }
4067        #[cfg(unix)]
4068        {
4069            use std::fs::File;
4070            use std::os::unix::io::FromRawFd;
4071
4072            let read_name = args[0].to_string();
4073            let write_name = args[1].to_string();
4074            if read_name.is_empty() || write_name.is_empty() {
4075                return Err(PerlError::runtime("pipe: invalid handle name", line));
4076            }
4077            let mut fds = [0i32; 2];
4078            if unsafe { libc::pipe(fds.as_mut_ptr()) } != 0 {
4079                let e = std::io::Error::last_os_error();
4080                self.apply_io_error_to_errno(&e);
4081                return Ok(PerlValue::integer(0));
4082            }
4083            let read_file = unsafe { File::from_raw_fd(fds[0]) };
4084            let write_file = unsafe { File::from_raw_fd(fds[1]) };
4085
4086            let read_shared = Arc::new(Mutex::new(read_file));
4087            let write_shared = Arc::new(Mutex::new(write_file));
4088
4089            self.close_builtin_execute(read_name.clone()).ok();
4090            self.close_builtin_execute(write_name.clone()).ok();
4091
4092            self.io_file_slots
4093                .insert(read_name.clone(), Arc::clone(&read_shared));
4094            self.input_handles.insert(
4095                read_name,
4096                BufReader::new(Box::new(IoSharedFile(Arc::clone(&read_shared)))),
4097            );
4098
4099            self.io_file_slots
4100                .insert(write_name.clone(), Arc::clone(&write_shared));
4101            self.output_handles
4102                .insert(write_name, Box::new(IoSharedFileWrite(write_shared)));
4103
4104            Ok(PerlValue::integer(1))
4105        }
4106        #[cfg(not(unix))]
4107        {
4108            let _ = args;
4109            Err(PerlError::runtime(
4110                "pipe is not supported on this platform",
4111                line,
4112            ))
4113        }
4114    }
4115
4116    pub(crate) fn close_builtin_execute(&mut self, name: String) -> PerlResult<PerlValue> {
4117        self.output_handles.remove(&name);
4118        self.input_handles.remove(&name);
4119        self.io_file_slots.remove(&name);
4120        if let Some(mut child) = self.pipe_children.remove(&name) {
4121            if let Ok(st) = child.wait() {
4122                self.record_child_exit_status(st);
4123            }
4124        }
4125        Ok(PerlValue::integer(1))
4126    }
4127
4128    pub(crate) fn has_input_handle(&self, name: &str) -> bool {
4129        self.input_handles.contains_key(name)
4130    }
4131
4132    /// `eof` with no arguments: true while processing the last line from the current `-n`/`-p` input
4133    /// source (see [`Self::line_mode_eof_pending`]). Other contexts still return false until
4134    /// readline-level EOF tracking exists.
4135    pub(crate) fn eof_without_arg_is_true(&self) -> bool {
4136        self.line_mode_eof_pending
4137    }
4138
4139    /// `eof` / `eof()` / `eof FH` — shared by the tree walker, [`crate::vm::VM`], and
4140    /// [`crate::builtins::try_builtin`] (`CORE::eof`, `builtin::eof`, which parse as [`ExprKind::FuncCall`],
4141    /// not [`ExprKind::Eof`]).
4142    pub(crate) fn eof_builtin_execute(
4143        &self,
4144        args: &[PerlValue],
4145        line: usize,
4146    ) -> PerlResult<PerlValue> {
4147        match args.len() {
4148            0 => Ok(PerlValue::integer(if self.eof_without_arg_is_true() {
4149                1
4150            } else {
4151                0
4152            })),
4153            1 => {
4154                let name = args[0].to_string();
4155                let at_eof = !self.has_input_handle(&name);
4156                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
4157            }
4158            _ => Err(PerlError::runtime("eof: too many arguments", line)),
4159        }
4160    }
4161
4162    /// `study EXPR` — Perl returns `1` for non-empty strings and a defined empty value (numifies to
4163    /// `0`, stringifies to `""`) for `""`.
4164    pub(crate) fn study_return_value(s: &str) -> PerlValue {
4165        if s.is_empty() {
4166            PerlValue::string(String::new())
4167        } else {
4168            PerlValue::integer(1)
4169        }
4170    }
4171
4172    pub(crate) fn readline_builtin_execute(
4173        &mut self,
4174        handle: Option<&str>,
4175    ) -> PerlResult<PerlValue> {
4176        // `<>` / `readline` with no handle: iterate `@ARGV` files, else stdin.
4177        if handle.is_none() {
4178            let argv = self.scope.get_array("ARGV");
4179            if !argv.is_empty() {
4180                loop {
4181                    if self.diamond_reader.is_none() {
4182                        while self.diamond_next_idx < argv.len() {
4183                            let path = argv[self.diamond_next_idx].to_string();
4184                            self.diamond_next_idx += 1;
4185                            match File::open(&path) {
4186                                Ok(f) => {
4187                                    self.argv_current_file = path;
4188                                    self.diamond_reader = Some(BufReader::new(f));
4189                                    break;
4190                                }
4191                                Err(e) => {
4192                                    self.apply_io_error_to_errno(&e);
4193                                }
4194                            }
4195                        }
4196                        if self.diamond_reader.is_none() {
4197                            return Ok(PerlValue::UNDEF);
4198                        }
4199                    }
4200                    let mut line_str = String::new();
4201                    let read_result: Result<usize, io::Error> =
4202                        if let Some(reader) = self.diamond_reader.as_mut() {
4203                            if self.open_pragma_utf8 {
4204                                let mut buf = Vec::new();
4205                                reader.read_until(b'\n', &mut buf).inspect(|n| {
4206                                    if *n > 0 {
4207                                        line_str = String::from_utf8_lossy(&buf).into_owned();
4208                                    }
4209                                })
4210                            } else {
4211                                let mut buf = Vec::new();
4212                                match reader.read_until(b'\n', &mut buf) {
4213                                    Ok(n) => {
4214                                        if n > 0 {
4215                                            line_str =
4216                                            crate::perl_decode::decode_utf8_or_latin1_read_until(
4217                                                &buf,
4218                                            );
4219                                        }
4220                                        Ok(n)
4221                                    }
4222                                    Err(e) => Err(e),
4223                                }
4224                            }
4225                        } else {
4226                            unreachable!()
4227                        };
4228                    match read_result {
4229                        Ok(0) => {
4230                            self.diamond_reader = None;
4231                            continue;
4232                        }
4233                        Ok(_) => {
4234                            self.bump_line_for_handle(&self.argv_current_file.clone());
4235                            return Ok(PerlValue::string(line_str));
4236                        }
4237                        Err(e) => {
4238                            self.apply_io_error_to_errno(&e);
4239                            self.diamond_reader = None;
4240                            continue;
4241                        }
4242                    }
4243                }
4244            } else {
4245                self.argv_current_file.clear();
4246            }
4247        }
4248
4249        let handle_name = handle.unwrap_or("STDIN");
4250        let mut line_str = String::new();
4251        if handle_name == "STDIN" {
4252            if let Some(queued) = self.line_mode_stdin_pending.pop_front() {
4253                self.last_stdin_die_bracket = if handle.is_none() {
4254                    "<>".to_string()
4255                } else {
4256                    "<STDIN>".to_string()
4257                };
4258                self.bump_line_for_handle("STDIN");
4259                return Ok(PerlValue::string(queued));
4260            }
4261            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4262                let mut buf = Vec::new();
4263                io::stdin().lock().read_until(b'\n', &mut buf).inspect(|n| {
4264                    if *n > 0 {
4265                        line_str = String::from_utf8_lossy(&buf).into_owned();
4266                    }
4267                })
4268            } else {
4269                let mut buf = Vec::new();
4270                let mut lock = io::stdin().lock();
4271                match lock.read_until(b'\n', &mut buf) {
4272                    Ok(n) => {
4273                        if n > 0 {
4274                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4275                        }
4276                        Ok(n)
4277                    }
4278                    Err(e) => Err(e),
4279                }
4280            };
4281            match r {
4282                Ok(0) => Ok(PerlValue::UNDEF),
4283                Ok(_) => {
4284                    self.last_stdin_die_bracket = if handle.is_none() {
4285                        "<>".to_string()
4286                    } else {
4287                        "<STDIN>".to_string()
4288                    };
4289                    self.bump_line_for_handle("STDIN");
4290                    Ok(PerlValue::string(line_str))
4291                }
4292                Err(e) => {
4293                    self.apply_io_error_to_errno(&e);
4294                    Ok(PerlValue::UNDEF)
4295                }
4296            }
4297        } else if let Some(reader) = self.input_handles.get_mut(handle_name) {
4298            let r: Result<usize, io::Error> = if self.open_pragma_utf8 {
4299                let mut buf = Vec::new();
4300                reader.read_until(b'\n', &mut buf).inspect(|n| {
4301                    if *n > 0 {
4302                        line_str = String::from_utf8_lossy(&buf).into_owned();
4303                    }
4304                })
4305            } else {
4306                let mut buf = Vec::new();
4307                match reader.read_until(b'\n', &mut buf) {
4308                    Ok(n) => {
4309                        if n > 0 {
4310                            line_str = crate::perl_decode::decode_utf8_or_latin1_read_until(&buf);
4311                        }
4312                        Ok(n)
4313                    }
4314                    Err(e) => Err(e),
4315                }
4316            };
4317            match r {
4318                Ok(0) => Ok(PerlValue::UNDEF),
4319                Ok(_) => {
4320                    self.bump_line_for_handle(handle_name);
4321                    Ok(PerlValue::string(line_str))
4322                }
4323                Err(e) => {
4324                    self.apply_io_error_to_errno(&e);
4325                    Ok(PerlValue::UNDEF)
4326                }
4327            }
4328        } else {
4329            Ok(PerlValue::UNDEF)
4330        }
4331    }
4332
4333    /// `<HANDLE>` / `readline` in **list** context: all lines until EOF (same as repeated scalar readline).
4334    pub(crate) fn readline_builtin_execute_list(
4335        &mut self,
4336        handle: Option<&str>,
4337    ) -> PerlResult<PerlValue> {
4338        let mut lines = Vec::new();
4339        loop {
4340            let v = self.readline_builtin_execute(handle)?;
4341            if v.is_undef() {
4342                break;
4343            }
4344            lines.push(v);
4345        }
4346        Ok(PerlValue::array(lines))
4347    }
4348
4349    pub(crate) fn opendir_handle(&mut self, handle: &str, path: &str) -> PerlValue {
4350        match std::fs::read_dir(path) {
4351            Ok(rd) => {
4352                let entries: Vec<String> = rd
4353                    .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().into_owned()))
4354                    .collect();
4355                self.dir_handles
4356                    .insert(handle.to_string(), DirHandleState { entries, pos: 0 });
4357                PerlValue::integer(1)
4358            }
4359            Err(e) => {
4360                self.apply_io_error_to_errno(&e);
4361                PerlValue::integer(0)
4362            }
4363        }
4364    }
4365
4366    pub(crate) fn readdir_handle(&mut self, handle: &str) -> PerlValue {
4367        if let Some(dh) = self.dir_handles.get_mut(handle) {
4368            if dh.pos < dh.entries.len() {
4369                let s = dh.entries[dh.pos].clone();
4370                dh.pos += 1;
4371                PerlValue::string(s)
4372            } else {
4373                PerlValue::UNDEF
4374            }
4375        } else {
4376            PerlValue::UNDEF
4377        }
4378    }
4379
4380    /// List-context `readdir`: all directory entries not yet consumed (advances cursor to end).
4381    pub(crate) fn readdir_handle_list(&mut self, handle: &str) -> PerlValue {
4382        if let Some(dh) = self.dir_handles.get_mut(handle) {
4383            let rest: Vec<PerlValue> = dh.entries[dh.pos..]
4384                .iter()
4385                .cloned()
4386                .map(PerlValue::string)
4387                .collect();
4388            dh.pos = dh.entries.len();
4389            PerlValue::array(rest)
4390        } else {
4391            PerlValue::array(Vec::new())
4392        }
4393    }
4394
4395    pub(crate) fn closedir_handle(&mut self, handle: &str) -> PerlValue {
4396        PerlValue::integer(if self.dir_handles.remove(handle).is_some() {
4397            1
4398        } else {
4399            0
4400        })
4401    }
4402
4403    pub(crate) fn rewinddir_handle(&mut self, handle: &str) -> PerlValue {
4404        if let Some(dh) = self.dir_handles.get_mut(handle) {
4405            dh.pos = 0;
4406            PerlValue::integer(1)
4407        } else {
4408            PerlValue::integer(0)
4409        }
4410    }
4411
4412    pub(crate) fn telldir_handle(&mut self, handle: &str) -> PerlValue {
4413        self.dir_handles
4414            .get(handle)
4415            .map(|dh| PerlValue::integer(dh.pos as i64))
4416            .unwrap_or(PerlValue::UNDEF)
4417    }
4418
4419    pub(crate) fn seekdir_handle(&mut self, handle: &str, pos: usize) -> PerlValue {
4420        if let Some(dh) = self.dir_handles.get_mut(handle) {
4421            dh.pos = pos.min(dh.entries.len());
4422            PerlValue::integer(1)
4423        } else {
4424            PerlValue::integer(0)
4425        }
4426    }
4427
4428    /// Set `$&`, `` $` ``, `$'`, `$+`, `$1`…`$n`, `@-`, `@+`, `%+`, and `${^MATCH}` / … fields from a successful match.
4429    /// Scalar name names a regex capture variable (`$&`, `` $` ``, `$'`, `$+`, `$-`, `$1`..`$N`).
4430    /// Writing to any of these from non-regex code must invalidate [`Self::regex_capture_scope_fresh`]
4431    /// so the [`Self::regex_match_memo`] fast path re-applies `apply_regex_captures` on the next hit.
4432    #[inline]
4433    pub(crate) fn is_regex_capture_scope_var(name: &str) -> bool {
4434        crate::special_vars::is_regex_match_scalar_name(name)
4435    }
4436
4437    /// Invalidate the capture-variable side of [`Self::regex_match_memo`]. Call from name-based
4438    /// scope writes (e.g. `Op::SetScalar`) so the next memoized regex match replays
4439    /// `apply_regex_captures` instead of short-circuiting.
4440    #[inline]
4441    pub(crate) fn maybe_invalidate_regex_capture_memo(&mut self, name: &str) {
4442        if self.regex_capture_scope_fresh && Self::is_regex_capture_scope_var(name) {
4443            self.regex_capture_scope_fresh = false;
4444        }
4445    }
4446
4447    pub(crate) fn apply_regex_captures(
4448        &mut self,
4449        haystack: &str,
4450        offset: usize,
4451        re: &PerlCompiledRegex,
4452        caps: &PerlCaptures<'_>,
4453        capture_all: CaptureAllMode,
4454    ) -> Result<(), FlowOrError> {
4455        let m0 = caps.get(0).expect("regex capture 0");
4456        let s0 = offset + m0.start;
4457        let e0 = offset + m0.end;
4458        self.last_match = haystack.get(s0..e0).unwrap_or("").to_string();
4459        self.prematch = haystack.get(..s0).unwrap_or("").to_string();
4460        self.postmatch = haystack.get(e0..).unwrap_or("").to_string();
4461        let mut last_paren = String::new();
4462        for i in 1..caps.len() {
4463            if let Some(m) = caps.get(i) {
4464                last_paren = m.text.to_string();
4465            }
4466        }
4467        self.last_paren_match = last_paren;
4468        self.last_subpattern_name = String::new();
4469        for n in re.capture_names().flatten() {
4470            if caps.name(n).is_some() {
4471                self.last_subpattern_name = n.to_string();
4472            }
4473        }
4474        self.scope
4475            .set_scalar("&", PerlValue::string(self.last_match.clone()))?;
4476        self.scope
4477            .set_scalar("`", PerlValue::string(self.prematch.clone()))?;
4478        self.scope
4479            .set_scalar("'", PerlValue::string(self.postmatch.clone()))?;
4480        self.scope
4481            .set_scalar("+", PerlValue::string(self.last_paren_match.clone()))?;
4482        for i in 1..caps.len() {
4483            if let Some(m) = caps.get(i) {
4484                self.scope
4485                    .set_scalar(&i.to_string(), PerlValue::string(m.text.to_string()))?;
4486            }
4487        }
4488        let mut start_arr = vec![PerlValue::integer(s0 as i64)];
4489        let mut end_arr = vec![PerlValue::integer(e0 as i64)];
4490        for i in 1..caps.len() {
4491            if let Some(m) = caps.get(i) {
4492                start_arr.push(PerlValue::integer((offset + m.start) as i64));
4493                end_arr.push(PerlValue::integer((offset + m.end) as i64));
4494            } else {
4495                start_arr.push(PerlValue::integer(-1));
4496                end_arr.push(PerlValue::integer(-1));
4497            }
4498        }
4499        self.scope.set_array("-", start_arr)?;
4500        self.scope.set_array("+", end_arr)?;
4501        let mut named = IndexMap::new();
4502        for name in re.capture_names().flatten() {
4503            if let Some(m) = caps.name(name) {
4504                named.insert(name.to_string(), PerlValue::string(m.text.to_string()));
4505            }
4506        }
4507        self.scope.set_hash("+", named.clone())?;
4508        // `%-` maps each named capture to an arrayref of values (for multiple matches of the same name).
4509        let mut named_minus = IndexMap::new();
4510        for (name, val) in &named {
4511            named_minus.insert(
4512                name.clone(),
4513                PerlValue::array_ref(Arc::new(RwLock::new(vec![val.clone()]))),
4514            );
4515        }
4516        self.scope.set_hash("-", named_minus)?;
4517        let cap_flat = crate::perl_regex::numbered_capture_flat(caps);
4518        self.scope.set_array("^CAPTURE", cap_flat.clone())?;
4519        match capture_all {
4520            CaptureAllMode::Empty => {
4521                self.scope.set_array("^CAPTURE_ALL", vec![])?;
4522            }
4523            CaptureAllMode::Append => {
4524                let mut rows = self.scope.get_array("^CAPTURE_ALL");
4525                rows.push(PerlValue::array(cap_flat));
4526                self.scope.set_array("^CAPTURE_ALL", rows)?;
4527            }
4528            CaptureAllMode::Skip => {}
4529        }
4530        Ok(())
4531    }
4532
4533    pub(crate) fn clear_flip_flop_state(&mut self) {
4534        self.flip_flop_active.clear();
4535        self.flip_flop_exclusive_left_line.clear();
4536        self.flip_flop_sequence.clear();
4537        self.flip_flop_last_dot.clear();
4538        self.flip_flop_tree.clear();
4539    }
4540
4541    pub(crate) fn prepare_flip_flop_vm_slots(&mut self, slots: u16) {
4542        self.flip_flop_active.resize(slots as usize, false);
4543        self.flip_flop_active.fill(false);
4544        self.flip_flop_exclusive_left_line
4545            .resize(slots as usize, None);
4546        self.flip_flop_exclusive_left_line.fill(None);
4547        self.flip_flop_sequence.resize(slots as usize, 0);
4548        self.flip_flop_sequence.fill(0);
4549        self.flip_flop_last_dot.resize(slots as usize, None);
4550        self.flip_flop_last_dot.fill(None);
4551    }
4552
4553    /// Input line number used by scalar `..` flip-flop — matches Perl `$.` (`-n`/`-p` use
4554    /// [`Self::line_number`]; [`Self::readline_builtin_execute`] updates `$.` via
4555    /// [`Self::handle_line_numbers`]).
4556    #[inline]
4557    pub(crate) fn scalar_flipflop_dot_line(&self) -> i64 {
4558        if self.last_readline_handle.is_empty() {
4559            self.line_number
4560        } else {
4561            *self
4562                .handle_line_numbers
4563                .get(&self.last_readline_handle)
4564                .unwrap_or(&0)
4565        }
4566    }
4567
4568    /// Scalar `..` / `...` flip-flop vs `$.` (numeric bounds). `exclusive` matches Perl `...` (do not
4569    /// treat the right bound as satisfied on the same `$.` line as the left match; see `perlop`).
4570    ///
4571    /// Perl `pp_flop` stringifies the false state as `""` (not `0`) so `my $x = 1..5; print "[$x]"`
4572    /// prints `[]` when `$.` hasn't reached the left bound. True values are sequence numbers
4573    /// starting at `1`; the result on the closing line of an exclusive `...` has `E0` appended
4574    /// (represented here as the string `"<n>E0"`). Callers that need the numeric form still
4575    /// get `0` / `N` from [`PerlValue::to_int`].
4576    pub(crate) fn scalar_flip_flop_eval(
4577        &mut self,
4578        left: i64,
4579        right: i64,
4580        slot: usize,
4581        exclusive: bool,
4582    ) -> PerlResult<PerlValue> {
4583        if self.flip_flop_active.len() <= slot {
4584            self.flip_flop_active.resize(slot + 1, false);
4585        }
4586        if self.flip_flop_exclusive_left_line.len() <= slot {
4587            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4588        }
4589        if self.flip_flop_sequence.len() <= slot {
4590            self.flip_flop_sequence.resize(slot + 1, 0);
4591        }
4592        if self.flip_flop_last_dot.len() <= slot {
4593            self.flip_flop_last_dot.resize(slot + 1, None);
4594        }
4595        let dot = self.scalar_flipflop_dot_line();
4596        let active = &mut self.flip_flop_active[slot];
4597        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4598        let seq = &mut self.flip_flop_sequence[slot];
4599        let last_dot = &mut self.flip_flop_last_dot[slot];
4600        if !*active {
4601            if dot == left {
4602                *active = true;
4603                *seq = 1;
4604                *last_dot = Some(dot);
4605                if exclusive {
4606                    *excl_left = Some(dot);
4607                } else {
4608                    *excl_left = None;
4609                    if dot == right {
4610                        *active = false;
4611                        return Ok(PerlValue::string(format!("{}E0", *seq)));
4612                    }
4613                }
4614                return Ok(PerlValue::string(seq.to_string()));
4615            }
4616            *last_dot = Some(dot);
4617            return Ok(PerlValue::string(String::new()));
4618        }
4619        // Already active: increment the sequence once per new `$.`, so a second evaluation on
4620        // the same line reads the same number (matches Perl `pp_flop`).
4621        if *last_dot != Some(dot) {
4622            *seq += 1;
4623            *last_dot = Some(dot);
4624        }
4625        let cur_seq = *seq;
4626        if let Some(ll) = *excl_left {
4627            if dot == right && dot > ll {
4628                *active = false;
4629                *excl_left = None;
4630                *seq = 0;
4631                return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4632            }
4633        } else if dot == right {
4634            *active = false;
4635            *seq = 0;
4636            return Ok(PerlValue::string(format!("{}E0", cur_seq)));
4637        }
4638        Ok(PerlValue::string(cur_seq.to_string()))
4639    }
4640
4641    fn regex_flip_flop_transition(
4642        active: &mut bool,
4643        excl_left: &mut Option<i64>,
4644        exclusive: bool,
4645        dot: i64,
4646        left_m: bool,
4647        right_m: bool,
4648    ) -> i64 {
4649        if !*active {
4650            if left_m {
4651                *active = true;
4652                if exclusive {
4653                    *excl_left = Some(dot);
4654                } else {
4655                    *excl_left = None;
4656                    if right_m {
4657                        *active = false;
4658                    }
4659                }
4660                return 1;
4661            }
4662            return 0;
4663        }
4664        if let Some(ll) = *excl_left {
4665            if right_m && dot > ll {
4666                *active = false;
4667                *excl_left = None;
4668            }
4669        } else if right_m {
4670            *active = false;
4671        }
4672        1
4673    }
4674
4675    /// Scalar `..` / `...` when both operands are regex literals: match against `$_`; `$.`
4676    /// ([`Self::scalar_flipflop_dot_line`]) drives exclusive `...` (right not tested on the same line as
4677    /// left until `$.` advances), mirroring [`Self::scalar_flip_flop_eval`].
4678    #[allow(clippy::too_many_arguments)] // left/right pattern + flags + VM state is inherently eight params
4679    pub(crate) fn regex_flip_flop_eval(
4680        &mut self,
4681        left_pat: &str,
4682        left_flags: &str,
4683        right_pat: &str,
4684        right_flags: &str,
4685        slot: usize,
4686        exclusive: bool,
4687        line: usize,
4688    ) -> PerlResult<PerlValue> {
4689        let dot = self.scalar_flipflop_dot_line();
4690        let subject = self.scope.get_scalar("_").to_string();
4691        let left_re = self
4692            .compile_regex(left_pat, left_flags, line)
4693            .map_err(|e| match e {
4694                FlowOrError::Error(err) => err,
4695                FlowOrError::Flow(_) => {
4696                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4697                }
4698            })?;
4699        let right_re = self
4700            .compile_regex(right_pat, right_flags, line)
4701            .map_err(|e| match e {
4702                FlowOrError::Error(err) => err,
4703                FlowOrError::Flow(_) => {
4704                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4705                }
4706            })?;
4707        let left_m = left_re.is_match(&subject);
4708        let right_m = right_re.is_match(&subject);
4709        if self.flip_flop_active.len() <= slot {
4710            self.flip_flop_active.resize(slot + 1, false);
4711        }
4712        if self.flip_flop_exclusive_left_line.len() <= slot {
4713            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4714        }
4715        let active = &mut self.flip_flop_active[slot];
4716        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4717        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4718            active, excl_left, exclusive, dot, left_m, right_m,
4719        )))
4720    }
4721
4722    /// Regex `..` / `...` with a dynamic right operand (evaluated in boolean context vs `$_` / `eof` / etc.).
4723    pub(crate) fn regex_flip_flop_eval_dynamic_right(
4724        &mut self,
4725        left_pat: &str,
4726        left_flags: &str,
4727        slot: usize,
4728        exclusive: bool,
4729        line: usize,
4730        right_m: bool,
4731    ) -> PerlResult<PerlValue> {
4732        let dot = self.scalar_flipflop_dot_line();
4733        let subject = self.scope.get_scalar("_").to_string();
4734        let left_re = self
4735            .compile_regex(left_pat, left_flags, line)
4736            .map_err(|e| match e {
4737                FlowOrError::Error(err) => err,
4738                FlowOrError::Flow(_) => {
4739                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4740                }
4741            })?;
4742        let left_m = left_re.is_match(&subject);
4743        if self.flip_flop_active.len() <= slot {
4744            self.flip_flop_active.resize(slot + 1, false);
4745        }
4746        if self.flip_flop_exclusive_left_line.len() <= slot {
4747            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4748        }
4749        let active = &mut self.flip_flop_active[slot];
4750        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4751        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4752            active, excl_left, exclusive, dot, left_m, right_m,
4753        )))
4754    }
4755
4756    /// Regex left bound vs `$_`; right bound is a fixed `$.` line (Perl `m/a/...N`).
4757    pub(crate) fn regex_flip_flop_eval_dot_line_rhs(
4758        &mut self,
4759        left_pat: &str,
4760        left_flags: &str,
4761        slot: usize,
4762        exclusive: bool,
4763        line: usize,
4764        rhs_line: i64,
4765    ) -> PerlResult<PerlValue> {
4766        let dot = self.scalar_flipflop_dot_line();
4767        let subject = self.scope.get_scalar("_").to_string();
4768        let left_re = self
4769            .compile_regex(left_pat, left_flags, line)
4770            .map_err(|e| match e {
4771                FlowOrError::Error(err) => err,
4772                FlowOrError::Flow(_) => {
4773                    PerlError::runtime("unexpected flow in regex flip-flop", line)
4774                }
4775            })?;
4776        let left_m = left_re.is_match(&subject);
4777        let right_m = dot == rhs_line;
4778        if self.flip_flop_active.len() <= slot {
4779            self.flip_flop_active.resize(slot + 1, false);
4780        }
4781        if self.flip_flop_exclusive_left_line.len() <= slot {
4782            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4783        }
4784        let active = &mut self.flip_flop_active[slot];
4785        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4786        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4787            active, excl_left, exclusive, dot, left_m, right_m,
4788        )))
4789    }
4790
4791    /// Regex `..` / `...` flip-flop when the right operand is bare `eof` (Perl: right side is `eof`, not a
4792    /// pattern). Uses [`Self::eof_without_arg_is_true`] like `eof` in `-n`/`-p`; exclusive `...` defers the
4793    /// right test until `$.` is strictly past the line where the left regex matched (same as
4794    /// [`Self::regex_flip_flop_eval`]).
4795    pub(crate) fn regex_eof_flip_flop_eval(
4796        &mut self,
4797        left_pat: &str,
4798        left_flags: &str,
4799        slot: usize,
4800        exclusive: bool,
4801        line: usize,
4802    ) -> PerlResult<PerlValue> {
4803        let dot = self.scalar_flipflop_dot_line();
4804        let subject = self.scope.get_scalar("_").to_string();
4805        let left_re = self
4806            .compile_regex(left_pat, left_flags, line)
4807            .map_err(|e| match e {
4808                FlowOrError::Error(err) => err,
4809                FlowOrError::Flow(_) => {
4810                    PerlError::runtime("unexpected flow in regex/eof flip-flop", line)
4811                }
4812            })?;
4813        let left_m = left_re.is_match(&subject);
4814        let right_m = self.eof_without_arg_is_true();
4815        if self.flip_flop_active.len() <= slot {
4816            self.flip_flop_active.resize(slot + 1, false);
4817        }
4818        if self.flip_flop_exclusive_left_line.len() <= slot {
4819            self.flip_flop_exclusive_left_line.resize(slot + 1, None);
4820        }
4821        let active = &mut self.flip_flop_active[slot];
4822        let excl_left = &mut self.flip_flop_exclusive_left_line[slot];
4823        Ok(PerlValue::integer(Self::regex_flip_flop_transition(
4824            active, excl_left, exclusive, dot, left_m, right_m,
4825        )))
4826    }
4827
4828    /// Shared `chomp` for tree-walker and VM (mutates `target`).
4829    pub(crate) fn chomp_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
4830        let mut s = val.to_string();
4831        let removed = if s.ends_with('\n') {
4832            s.pop();
4833            1i64
4834        } else {
4835            0i64
4836        };
4837        self.assign_value(target, PerlValue::string(s))?;
4838        Ok(PerlValue::integer(removed))
4839    }
4840
4841    /// Shared `chop` for tree-walker and VM (mutates `target`).
4842    pub(crate) fn chop_inplace_execute(&mut self, val: PerlValue, target: &Expr) -> ExecResult {
4843        let mut s = val.to_string();
4844        let chopped = s
4845            .pop()
4846            .map(|c| PerlValue::string(c.to_string()))
4847            .unwrap_or(PerlValue::UNDEF);
4848        self.assign_value(target, PerlValue::string(s))?;
4849        Ok(chopped)
4850    }
4851
4852    /// Shared regex match for tree-walker and VM (`pos` is updated for scalar `/g`).
4853    pub(crate) fn regex_match_execute(
4854        &mut self,
4855        s: String,
4856        pattern: &str,
4857        flags: &str,
4858        scalar_g: bool,
4859        pos_key: &str,
4860        line: usize,
4861    ) -> ExecResult {
4862        // Fast path: identical inputs to the previous non-`g` match → reuse the cached result.
4863        // Only safe for the non-`g`/non-`scalar_g` branch; `g` matches mutate `$&`/`@+`/etc. and
4864        // also keep per-pattern `pos()` state that the memo doesn't track.
4865        //
4866        // On hit AND `regex_capture_scope_fresh == true`, skip `apply_regex_captures` entirely:
4867        // the scope's `$&`/`$1`/... still reflect the memoized match. `regex_capture_scope_fresh`
4868        // is cleared by any scope write to a capture variable (see `invalidate_regex_capture_scope`).
4869        if !flags.contains('g') && !scalar_g {
4870            let memo_hit = {
4871                if let Some(ref mem) = self.regex_match_memo {
4872                    mem.pattern == pattern
4873                        && mem.flags == flags
4874                        && mem.multiline == self.multiline_match
4875                        && mem.haystack == s
4876                } else {
4877                    false
4878                }
4879            };
4880            if memo_hit {
4881                if self.regex_capture_scope_fresh {
4882                    return Ok(self.regex_match_memo.as_ref().expect("memo").result.clone());
4883                }
4884                // Memo hit but scope side effects were invalidated. Re-apply captures
4885                // from the memoized haystack + a fresh compiled regex.
4886                let (memo_s, memo_result) = {
4887                    let mem = self.regex_match_memo.as_ref().expect("memo");
4888                    (mem.haystack.clone(), mem.result.clone())
4889                };
4890                let re = self.compile_regex(pattern, flags, line)?;
4891                if let Some(caps) = re.captures(&memo_s) {
4892                    self.apply_regex_captures(&memo_s, 0, &re, &caps, CaptureAllMode::Empty)?;
4893                }
4894                self.regex_capture_scope_fresh = true;
4895                return Ok(memo_result);
4896            }
4897        }
4898        let re = self.compile_regex(pattern, flags, line)?;
4899        if flags.contains('g') && scalar_g {
4900            let key = pos_key.to_string();
4901            let start = self.regex_pos.get(&key).copied().flatten().unwrap_or(0);
4902            if start == 0 {
4903                self.scope.set_array("^CAPTURE_ALL", vec![])?;
4904            }
4905            if start > s.len() {
4906                self.regex_pos.insert(key, None);
4907                return Ok(PerlValue::integer(0));
4908            }
4909            let sub = s.get(start..).unwrap_or("");
4910            if let Some(caps) = re.captures(sub) {
4911                let overall = caps.get(0).expect("capture 0");
4912                let abs_end = start + overall.end;
4913                self.regex_pos.insert(key, Some(abs_end));
4914                self.apply_regex_captures(&s, start, &re, &caps, CaptureAllMode::Append)?;
4915                Ok(PerlValue::integer(1))
4916            } else {
4917                self.regex_pos.insert(key, None);
4918                Ok(PerlValue::integer(0))
4919            }
4920        } else if flags.contains('g') {
4921            let mut rows = Vec::new();
4922            let mut last_caps: Option<PerlCaptures<'_>> = None;
4923            for caps in re.captures_iter(&s) {
4924                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
4925                    &caps,
4926                )));
4927                last_caps = Some(caps);
4928            }
4929            self.scope.set_array("^CAPTURE_ALL", rows)?;
4930            let matches: Vec<PerlValue> = match &*re {
4931                PerlCompiledRegex::Rust(r) => r
4932                    .find_iter(&s)
4933                    .map(|m| PerlValue::string(m.as_str().to_string()))
4934                    .collect(),
4935                PerlCompiledRegex::Fancy(r) => r
4936                    .find_iter(&s)
4937                    .filter_map(|m| m.ok())
4938                    .map(|m| PerlValue::string(m.as_str().to_string()))
4939                    .collect(),
4940                PerlCompiledRegex::Pcre2(r) => r
4941                    .find_iter(s.as_bytes())
4942                    .filter_map(|m| m.ok())
4943                    .map(|m| {
4944                        let t = s.get(m.start()..m.end()).unwrap_or("");
4945                        PerlValue::string(t.to_string())
4946                    })
4947                    .collect(),
4948            };
4949            if matches.is_empty() {
4950                Ok(PerlValue::integer(0))
4951            } else {
4952                if let Some(caps) = last_caps {
4953                    self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Skip)?;
4954                }
4955                Ok(PerlValue::array(matches))
4956            }
4957        } else if let Some(caps) = re.captures(&s) {
4958            self.apply_regex_captures(&s, 0, &re, &caps, CaptureAllMode::Empty)?;
4959            let result = PerlValue::integer(1);
4960            self.regex_match_memo = Some(RegexMatchMemo {
4961                pattern: pattern.to_string(),
4962                flags: flags.to_string(),
4963                multiline: self.multiline_match,
4964                haystack: s,
4965                result: result.clone(),
4966            });
4967            self.regex_capture_scope_fresh = true;
4968            Ok(result)
4969        } else {
4970            let result = PerlValue::integer(0);
4971            // Memoize negative results too — they don't set capture vars, so scope_fresh stays true.
4972            self.regex_match_memo = Some(RegexMatchMemo {
4973                pattern: pattern.to_string(),
4974                flags: flags.to_string(),
4975                multiline: self.multiline_match,
4976                haystack: s,
4977                result: result.clone(),
4978            });
4979            // A no-match leaves `$&` / `$1` as they were, which is still "fresh" from whatever
4980            // the last successful match (if any) set them to. Don't flip the flag.
4981            Ok(result)
4982        }
4983    }
4984
4985    /// Expand `$ENV{KEY}` in an `s///` pattern or replacement string (Perl treats these like
4986    /// double-quoted interpolations; required for `s@$ENV{HOME}@~@` and for replacements like
4987    /// `"$ENV{HOME}$2"` before the regex engine sees the pattern).
4988    pub(crate) fn expand_env_braces_in_subst(
4989        &mut self,
4990        raw: &str,
4991        line: usize,
4992    ) -> PerlResult<String> {
4993        self.materialize_env_if_needed();
4994        let mut out = String::new();
4995        let mut rest = raw;
4996        while let Some(idx) = rest.find("$ENV{") {
4997            out.push_str(&rest[..idx]);
4998            let after = &rest[idx + 5..];
4999            let end = after
5000                .find('}')
5001                .ok_or_else(|| PerlError::runtime("Unclosed $ENV{...} in s///", line))?;
5002            let key = &after[..end];
5003            let val = self.scope.get_hash_element("ENV", key);
5004            out.push_str(&val.to_string());
5005            rest = &after[end + 1..];
5006        }
5007        out.push_str(rest);
5008        Ok(out)
5009    }
5010
5011    /// Shared `s///` for tree-walker and VM.
5012    ///
5013    /// Perl replacement strings accept both `\1` and `$1` for back-references.
5014    /// The Rust `regex` / `fancy_regex` crates (and our PCRE2 shim) only
5015    /// understand `$N`, so we normalise here.
5016    pub(crate) fn regex_subst_execute(
5017        &mut self,
5018        s: String,
5019        pattern: &str,
5020        replacement: &str,
5021        flags: &str,
5022        target: &Expr,
5023        line: usize,
5024    ) -> ExecResult {
5025        let re_flags: String = flags.chars().filter(|c| *c != 'e').collect();
5026        let pattern = self.expand_env_braces_in_subst(pattern, line)?;
5027        let re = self.compile_regex(&pattern, &re_flags, line)?;
5028        if flags.contains('e') {
5029            return self.regex_subst_execute_eval(s, re.as_ref(), replacement, flags, target, line);
5030        }
5031        let replacement = self.expand_env_braces_in_subst(replacement, line)?;
5032        let replacement = self.interpolate_replacement_string(&replacement);
5033        let replacement = normalize_replacement_backrefs(&replacement);
5034        let last_caps = if flags.contains('g') {
5035            let mut rows = Vec::new();
5036            let mut last = None;
5037            for caps in re.captures_iter(&s) {
5038                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5039                    &caps,
5040                )));
5041                last = Some(caps);
5042            }
5043            self.scope.set_array("^CAPTURE_ALL", rows)?;
5044            last
5045        } else {
5046            re.captures(&s)
5047        };
5048        if let Some(caps) = last_caps {
5049            let mode = if flags.contains('g') {
5050                CaptureAllMode::Skip
5051            } else {
5052                CaptureAllMode::Empty
5053            };
5054            self.apply_regex_captures(&s, 0, &re, &caps, mode)?;
5055        }
5056        let (new_s, count) = if flags.contains('g') {
5057            let count = re.find_iter_count(&s);
5058            (re.replace_all(&s, replacement.as_str()), count)
5059        } else {
5060            let count = if re.is_match(&s) { 1 } else { 0 };
5061            (re.replace(&s, replacement.as_str()), count)
5062        };
5063        if flags.contains('r') {
5064            // /r — non-destructive: return the modified string, leave target unchanged
5065            Ok(PerlValue::string(new_s))
5066        } else {
5067            self.assign_value(target, PerlValue::string(new_s))?;
5068            Ok(PerlValue::integer(count as i64))
5069        }
5070    }
5071
5072    /// Run the `s///…e…` replacement side: `e_count` stacked `eval`s like Perl (each round parses
5073    /// and executes the string; the next round uses [`PerlValue::to_string`] of the prior value).
5074    fn regex_subst_run_eval_rounds(&mut self, replacement: &str, e_count: usize) -> ExecResult {
5075        let prep_source = |raw: &str| -> String {
5076            let mut code = raw.trim().to_string();
5077            if !code.ends_with(';') {
5078                code.push(';');
5079            }
5080            code
5081        };
5082        let mut cur = prep_source(replacement);
5083        let mut last = PerlValue::UNDEF;
5084        for round in 0..e_count {
5085            last = crate::parse_and_run_string(&cur, self)?;
5086            if round + 1 < e_count {
5087                cur = prep_source(&last.to_string());
5088            }
5089        }
5090        Ok(last)
5091    }
5092
5093    fn regex_subst_execute_eval(
5094        &mut self,
5095        s: String,
5096        re: &PerlCompiledRegex,
5097        replacement: &str,
5098        flags: &str,
5099        target: &Expr,
5100        line: usize,
5101    ) -> ExecResult {
5102        let e_count = flags.chars().filter(|c| *c == 'e').count();
5103        if e_count == 0 {
5104            return Err(PerlError::runtime("s///e: internal error (no e flag)", line).into());
5105        }
5106
5107        if flags.contains('g') {
5108            let mut rows = Vec::new();
5109            let mut out = String::new();
5110            let mut last = 0usize;
5111            let mut count = 0usize;
5112            for caps in re.captures_iter(&s) {
5113                let m0 = caps.get(0).expect("regex capture 0");
5114                out.push_str(&s[last..m0.start]);
5115                self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5116                let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5117                out.push_str(&repl_val.to_string());
5118                last = m0.end;
5119                count += 1;
5120                rows.push(PerlValue::array(crate::perl_regex::numbered_capture_flat(
5121                    &caps,
5122                )));
5123            }
5124            self.scope.set_array("^CAPTURE_ALL", rows)?;
5125            out.push_str(&s[last..]);
5126            if flags.contains('r') {
5127                return Ok(PerlValue::string(out));
5128            }
5129            self.assign_value(target, PerlValue::string(out))?;
5130            return Ok(PerlValue::integer(count as i64));
5131        }
5132        if let Some(caps) = re.captures(&s) {
5133            let m0 = caps.get(0).expect("regex capture 0");
5134            self.apply_regex_captures(&s, 0, re, &caps, CaptureAllMode::Empty)?;
5135            let repl_val = self.regex_subst_run_eval_rounds(replacement, e_count)?;
5136            let mut out = String::new();
5137            out.push_str(&s[..m0.start]);
5138            out.push_str(&repl_val.to_string());
5139            out.push_str(&s[m0.end..]);
5140            if flags.contains('r') {
5141                return Ok(PerlValue::string(out));
5142            }
5143            self.assign_value(target, PerlValue::string(out))?;
5144            return Ok(PerlValue::integer(1));
5145        }
5146        if flags.contains('r') {
5147            return Ok(PerlValue::string(s));
5148        }
5149        self.assign_value(target, PerlValue::string(s))?;
5150        Ok(PerlValue::integer(0))
5151    }
5152
5153    /// Shared `tr///` for tree-walker and VM.
5154    pub(crate) fn regex_transliterate_execute(
5155        &mut self,
5156        s: String,
5157        from: &str,
5158        to: &str,
5159        flags: &str,
5160        target: &Expr,
5161        line: usize,
5162    ) -> ExecResult {
5163        let _ = line;
5164        let from_chars = Self::tr_expand_ranges(from);
5165        let to_chars = Self::tr_expand_ranges(to);
5166        let delete_mode = flags.contains('d');
5167        let mut count = 0i64;
5168        let new_s: String = s
5169            .chars()
5170            .filter_map(|c| {
5171                if let Some(pos) = from_chars.iter().position(|&fc| fc == c) {
5172                    count += 1;
5173                    if delete_mode {
5174                        // /d — delete characters that match but have no replacement
5175                        if pos < to_chars.len() {
5176                            Some(to_chars[pos])
5177                        } else {
5178                            None // delete this character
5179                        }
5180                    } else {
5181                        // Normal mode: use last char in to_chars if pos exceeds, or keep original
5182                        Some(to_chars.get(pos).or(to_chars.last()).copied().unwrap_or(c))
5183                    }
5184                } else {
5185                    Some(c)
5186                }
5187            })
5188            .collect();
5189        if flags.contains('r') {
5190            // /r — non-destructive: return the modified string, leave target unchanged
5191            Ok(PerlValue::string(new_s))
5192        } else {
5193            self.assign_value(target, PerlValue::string(new_s))?;
5194            Ok(PerlValue::integer(count))
5195        }
5196    }
5197
5198    /// Expand Perl `tr///` range notation: `a-z` → `a`, `b`, …, `z`.
5199    /// A literal `-` at the start or end of the spec is kept as-is.
5200    pub(crate) fn tr_expand_ranges(spec: &str) -> Vec<char> {
5201        let raw: Vec<char> = spec.chars().collect();
5202        let mut out = Vec::with_capacity(raw.len());
5203        let mut i = 0;
5204        while i < raw.len() {
5205            if i + 2 < raw.len() && raw[i + 1] == '-' && raw[i] <= raw[i + 2] {
5206                let start = raw[i] as u32;
5207                let end = raw[i + 2] as u32;
5208                for code in start..=end {
5209                    if let Some(c) = char::from_u32(code) {
5210                        out.push(c);
5211                    }
5212                }
5213                i += 3;
5214            } else {
5215                out.push(raw[i]);
5216                i += 1;
5217            }
5218        }
5219        out
5220    }
5221
5222    /// `splice @array, offset, length, LIST` — used by the VM `CallBuiltin(Splice)` path.
5223    pub(crate) fn splice_builtin_execute(
5224        &mut self,
5225        args: &[PerlValue],
5226        line: usize,
5227    ) -> PerlResult<PerlValue> {
5228        if args.is_empty() {
5229            return Err(PerlError::runtime("splice: missing array", line));
5230        }
5231        let arr_name = args[0].to_string();
5232        let arr_len = self.scope.array_len(&arr_name);
5233        let offset_val = args
5234            .get(1)
5235            .cloned()
5236            .unwrap_or_else(|| PerlValue::integer(0));
5237        let length_val = match args.get(2) {
5238            None => PerlValue::UNDEF,
5239            Some(v) => v.clone(),
5240        };
5241        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
5242        let rep_vals: Vec<PerlValue> = args.iter().skip(3).cloned().collect();
5243        let arr = self.scope.get_array_mut(&arr_name)?;
5244        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
5245        for (i, v) in rep_vals.into_iter().enumerate() {
5246            arr.insert(off + i, v);
5247        }
5248        Ok(match self.wantarray_kind {
5249            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
5250            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
5251        })
5252    }
5253
5254    /// `unshift @array, LIST` — VM `CallBuiltin(Unshift)`.
5255    pub(crate) fn unshift_builtin_execute(
5256        &mut self,
5257        args: &[PerlValue],
5258        line: usize,
5259    ) -> PerlResult<PerlValue> {
5260        if args.is_empty() {
5261            return Err(PerlError::runtime("unshift: missing array", line));
5262        }
5263        let arr_name = args[0].to_string();
5264        let mut flat_vals: Vec<PerlValue> = Vec::new();
5265        for a in args.iter().skip(1) {
5266            if let Some(items) = a.as_array_vec() {
5267                flat_vals.extend(items);
5268            } else {
5269                flat_vals.push(a.clone());
5270            }
5271        }
5272        let arr = self.scope.get_array_mut(&arr_name)?;
5273        for (i, v) in flat_vals.into_iter().enumerate() {
5274            arr.insert(i, v);
5275        }
5276        Ok(PerlValue::integer(arr.len() as i64))
5277    }
5278
5279    /// Random fractional value like Perl `rand`: `[0, upper)` when `upper > 0`,
5280    /// `(upper, 0]` when `upper < 0`, and `[0, 1)` when `upper == 0`.
5281    pub(crate) fn perl_rand(&mut self, upper: f64) -> f64 {
5282        if upper == 0.0 {
5283            self.rand_rng.gen_range(0.0..1.0)
5284        } else if upper > 0.0 {
5285            self.rand_rng.gen_range(0.0..upper)
5286        } else {
5287            self.rand_rng.gen_range(upper..0.0)
5288        }
5289    }
5290
5291    /// Seed the PRNG; returns the seed Perl would report (truncated integer / time).
5292    pub(crate) fn perl_srand(&mut self, seed: Option<f64>) -> i64 {
5293        let n = if let Some(s) = seed {
5294            s as i64
5295        } else {
5296            std::time::SystemTime::now()
5297                .duration_since(std::time::UNIX_EPOCH)
5298                .map(|d| d.as_secs() as i64)
5299                .unwrap_or(1)
5300        };
5301        let mag = n.unsigned_abs();
5302        self.rand_rng = StdRng::seed_from_u64(mag);
5303        n.abs()
5304    }
5305
5306    pub fn set_file(&mut self, file: &str) {
5307        self.file = file.to_string();
5308    }
5309
5310    /// Keywords, builtins, lexical names, and subroutine names for REPL tab-completion.
5311    pub fn repl_completion_names(&self) -> Vec<String> {
5312        let mut v = self.scope.repl_binding_names();
5313        v.extend(self.subs.keys().cloned());
5314        v.sort();
5315        v.dedup();
5316        v
5317    }
5318
5319    /// Subroutine keys, blessed scalar classes, and `@ISA` edges for REPL `$obj->` completion.
5320    pub fn repl_completion_snapshot(&self) -> ReplCompletionSnapshot {
5321        let mut subs: Vec<String> = self.subs.keys().cloned().collect();
5322        subs.sort();
5323        let mut classes: HashSet<String> = HashSet::new();
5324        for k in &subs {
5325            if let Some((pkg, rest)) = k.split_once("::") {
5326                if !rest.contains("::") {
5327                    classes.insert(pkg.to_string());
5328                }
5329            }
5330        }
5331        let mut blessed_scalars: HashMap<String, String> = HashMap::new();
5332        for bn in self.scope.repl_binding_names() {
5333            if let Some(r) = bn.strip_prefix('$') {
5334                let v = self.scope.get_scalar(r);
5335                if let Some(b) = v.as_blessed_ref() {
5336                    blessed_scalars.insert(r.to_string(), b.class.clone());
5337                    classes.insert(b.class.clone());
5338                }
5339            }
5340        }
5341        let mut isa_for_class: HashMap<String, Vec<String>> = HashMap::new();
5342        for c in classes {
5343            isa_for_class.insert(c.clone(), self.parents_of_class(&c));
5344        }
5345        ReplCompletionSnapshot {
5346            subs,
5347            blessed_scalars,
5348            isa_for_class,
5349        }
5350    }
5351
5352    pub(crate) fn run_bench_block(&mut self, body: &Block, n: usize, line: usize) -> ExecResult {
5353        if n == 0 {
5354            return Err(FlowOrError::Error(PerlError::runtime(
5355                "bench: iteration count must be positive",
5356                line,
5357            )));
5358        }
5359        let warmup = (n / 10).clamp(1, 10);
5360        for _ in 0..warmup {
5361            self.exec_block(body)?;
5362        }
5363        let mut samples = Vec::with_capacity(n);
5364        for _ in 0..n {
5365            let start = std::time::Instant::now();
5366            self.exec_block(body)?;
5367            samples.push(start.elapsed().as_secs_f64() * 1000.0);
5368        }
5369        let mut sorted = samples.clone();
5370        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
5371        let min_ms = sorted[0];
5372        let mean = samples.iter().sum::<f64>() / n as f64;
5373        let p99_idx = ((n as f64 * 0.99).ceil() as usize)
5374            .saturating_sub(1)
5375            .min(n - 1);
5376        let p99_ms = sorted[p99_idx];
5377        Ok(PerlValue::string(format!(
5378            "bench: n={} warmup={} min={:.6}ms mean={:.6}ms p99={:.6}ms",
5379            n, warmup, min_ms, mean, p99_ms
5380        )))
5381    }
5382
5383    pub fn execute(&mut self, program: &Program) -> PerlResult<PerlValue> {
5384        // `-n`/`-p`: main must run only inside [`Self::process_line`], not as a full-program VM/tree
5385        // run (would execute `print` once before any input, etc.).
5386        if self.line_mode_skip_main {
5387            return self.execute_tree(program);
5388        }
5389        // With `--profile`, the VM records per-opcode line times and sub enter/return (JIT off).
5390        // Try bytecode VM first — falls back to tree-walker on unsupported features
5391        if let Some(result) = crate::try_vm_execute(program, self) {
5392            return result;
5393        }
5394
5395        // Tree-walker fallback
5396        self.execute_tree(program)
5397    }
5398
5399    /// Run `END` blocks (after `-n`/`-p` line loop when prelude used [`Self::line_mode_skip_main`]).
5400    pub fn run_end_blocks(&mut self) -> PerlResult<()> {
5401        self.global_phase = "END".to_string();
5402        let ends = std::mem::take(&mut self.end_blocks);
5403        for block in &ends {
5404            self.exec_block(block).map_err(|e| match e {
5405                FlowOrError::Error(e) => e,
5406                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in END", 0),
5407            })?;
5408        }
5409        Ok(())
5410    }
5411
5412    /// After a **top-level** program finishes (post-`END`), set `${^GLOBAL_PHASE}` to **`DESTRUCT`**
5413    /// and drain remaining `DESTROY` callbacks.
5414    pub fn run_global_teardown(&mut self) -> PerlResult<()> {
5415        self.global_phase = "DESTRUCT".to_string();
5416        self.drain_pending_destroys(0)
5417    }
5418
5419    /// Run queued `DESTROY` methods from blessed objects whose last reference was dropped.
5420    pub(crate) fn drain_pending_destroys(&mut self, line: usize) -> PerlResult<()> {
5421        loop {
5422            let batch = crate::pending_destroy::take_queue();
5423            if batch.is_empty() {
5424                break;
5425            }
5426            for (class, payload) in batch {
5427                let fq = format!("{}::DESTROY", class);
5428                let Some(sub) = self.subs.get(&fq).cloned() else {
5429                    continue;
5430                };
5431                let inv = PerlValue::blessed(Arc::new(
5432                    crate::value::BlessedRef::new_for_destroy_invocant(class, payload),
5433                ));
5434                match self.call_sub(&sub, vec![inv], WantarrayCtx::Void, line) {
5435                    Ok(_) => {}
5436                    Err(FlowOrError::Error(e)) => return Err(e),
5437                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
5438                    Err(FlowOrError::Flow(other)) => {
5439                        return Err(PerlError::runtime(
5440                            format!("DESTROY: unexpected control flow ({other:?})"),
5441                            line,
5442                        ));
5443                    }
5444                }
5445            }
5446        }
5447        Ok(())
5448    }
5449
5450    /// Tree-walking execution (fallback when bytecode compilation fails).
5451    pub fn execute_tree(&mut self, program: &Program) -> PerlResult<PerlValue> {
5452        // `${^GLOBAL_PHASE}` — each program starts in `RUN` (Perl before any `BEGIN` runs).
5453        self.global_phase = "RUN".to_string();
5454        self.clear_flip_flop_state();
5455        // First pass: subs, `use` (source order), BEGIN/END collection
5456        self.prepare_program_top_level(program)?;
5457
5458        // Execute BEGIN blocks (Perl uses phase `START` here).
5459        let begins = std::mem::take(&mut self.begin_blocks);
5460        if !begins.is_empty() {
5461            self.global_phase = "START".to_string();
5462        }
5463        for block in &begins {
5464            self.exec_block(block).map_err(|e| match e {
5465                FlowOrError::Error(e) => e,
5466                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in BEGIN", 0),
5467            })?;
5468        }
5469
5470        // UNITCHECK — reverse order of compilation (end of unit, before CHECK).
5471        // Perl keeps `${^GLOBAL_PHASE}` as **`START`** during these blocks (not `UNITCHECK`).
5472        let ucs = std::mem::take(&mut self.unit_check_blocks);
5473        for block in ucs.iter().rev() {
5474            self.exec_block(block).map_err(|e| match e {
5475                FlowOrError::Error(e) => e,
5476                FlowOrError::Flow(_) => {
5477                    PerlError::runtime("Unexpected flow control in UNITCHECK", 0)
5478                }
5479            })?;
5480        }
5481
5482        // CHECK — reverse order (end of compile phase).
5483        let checks = std::mem::take(&mut self.check_blocks);
5484        if !checks.is_empty() {
5485            self.global_phase = "CHECK".to_string();
5486        }
5487        for block in checks.iter().rev() {
5488            self.exec_block(block).map_err(|e| match e {
5489                FlowOrError::Error(e) => e,
5490                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in CHECK", 0),
5491            })?;
5492        }
5493
5494        // INIT — forward order (before main runtime).
5495        let inits = std::mem::take(&mut self.init_blocks);
5496        if !inits.is_empty() {
5497            self.global_phase = "INIT".to_string();
5498        }
5499        for block in &inits {
5500            self.exec_block(block).map_err(|e| match e {
5501                FlowOrError::Error(e) => e,
5502                FlowOrError::Flow(_) => PerlError::runtime("Unexpected flow control in INIT", 0),
5503            })?;
5504        }
5505
5506        self.global_phase = "RUN".to_string();
5507
5508        if self.line_mode_skip_main {
5509            // Body runs once per input line in [`Self::process_line`]; `END` runs after the loop
5510            // via [`Self::run_end_blocks`].
5511            return Ok(PerlValue::UNDEF);
5512        }
5513
5514        // Execute main program
5515        let mut last = PerlValue::UNDEF;
5516        for stmt in &program.statements {
5517            match &stmt.kind {
5518                StmtKind::Begin(_)
5519                | StmtKind::UnitCheck(_)
5520                | StmtKind::Check(_)
5521                | StmtKind::Init(_)
5522                | StmtKind::End(_)
5523                | StmtKind::UsePerlVersion { .. }
5524                | StmtKind::Use { .. }
5525                | StmtKind::No { .. }
5526                | StmtKind::FormatDecl { .. } => continue,
5527                _ => {
5528                    match self.exec_statement(stmt) {
5529                        Ok(val) => last = val,
5530                        Err(FlowOrError::Error(e)) => {
5531                            // Execute END blocks before propagating (all exit codes, including 0)
5532                            self.global_phase = "END".to_string();
5533                            let ends = std::mem::take(&mut self.end_blocks);
5534                            for block in &ends {
5535                                let _ = self.exec_block(block);
5536                            }
5537                            return Err(e);
5538                        }
5539                        Err(FlowOrError::Flow(Flow::Return(v))) => {
5540                            last = v;
5541                            break;
5542                        }
5543                        Err(FlowOrError::Flow(_)) => {}
5544                    }
5545                }
5546            }
5547        }
5548
5549        // Execute END blocks (Perl uses phase `END` here).
5550        self.global_phase = "END".to_string();
5551        let ends = std::mem::take(&mut self.end_blocks);
5552        for block in &ends {
5553            let _ = self.exec_block(block);
5554        }
5555
5556        self.drain_pending_destroys(0)?;
5557        Ok(last)
5558    }
5559
5560    pub(crate) fn exec_block(&mut self, block: &Block) -> ExecResult {
5561        self.exec_block_with_tail(block, WantarrayCtx::Void)
5562    }
5563
5564    /// Run a block; the **last** statement is evaluated in `tail` wantarray (Perl `do { }` / `eval { }` value).
5565    /// Non-final statements stay void context.
5566    pub(crate) fn exec_block_with_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5567        let uses_goto = block
5568            .iter()
5569            .any(|s| matches!(s.kind, StmtKind::Goto { .. }));
5570        if uses_goto {
5571            self.scope_push_hook();
5572            let r = self.exec_block_with_goto_tail(block, tail);
5573            self.scope_pop_hook();
5574            r
5575        } else {
5576            self.scope_push_hook();
5577            let result = self.exec_block_no_scope_with_tail(block, tail);
5578            self.scope_pop_hook();
5579            result
5580        }
5581    }
5582
5583    fn exec_block_with_goto_tail(&mut self, block: &Block, tail: WantarrayCtx) -> ExecResult {
5584        let mut map: HashMap<String, usize> = HashMap::new();
5585        for (i, s) in block.iter().enumerate() {
5586            if let Some(l) = &s.label {
5587                map.insert(l.clone(), i);
5588            }
5589        }
5590        let mut pc = 0usize;
5591        let mut last = PerlValue::UNDEF;
5592        let last_idx = block.len().saturating_sub(1);
5593        while pc < block.len() {
5594            if let StmtKind::Goto { target } = &block[pc].kind {
5595                let line = block[pc].line;
5596                let name = self.eval_expr(target)?.to_string();
5597                pc = *map.get(&name).ok_or_else(|| {
5598                    FlowOrError::Error(PerlError::runtime(
5599                        format!("goto: unknown label {}", name),
5600                        line,
5601                    ))
5602                })?;
5603                continue;
5604            }
5605            let v = if pc == last_idx {
5606                match &block[pc].kind {
5607                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail)?,
5608                    _ => self.exec_statement(&block[pc])?,
5609                }
5610            } else {
5611                self.exec_statement(&block[pc])?
5612            };
5613            last = v;
5614            pc += 1;
5615        }
5616        Ok(last)
5617    }
5618
5619    /// Execute block statements without pushing/popping a scope frame.
5620    /// Used internally by loops and the VM for sub calls.
5621    #[inline]
5622    pub(crate) fn exec_block_no_scope(&mut self, block: &Block) -> ExecResult {
5623        self.exec_block_no_scope_with_tail(block, WantarrayCtx::Void)
5624    }
5625
5626    pub(crate) fn exec_block_no_scope_with_tail(
5627        &mut self,
5628        block: &Block,
5629        tail: WantarrayCtx,
5630    ) -> ExecResult {
5631        if block.is_empty() {
5632            return Ok(PerlValue::UNDEF);
5633        }
5634        let last_i = block.len() - 1;
5635        for (i, stmt) in block.iter().enumerate() {
5636            if i < last_i {
5637                self.exec_statement(stmt)?;
5638            } else {
5639                return match &stmt.kind {
5640                    StmtKind::Expression(expr) => self.eval_expr_ctx(expr, tail),
5641                    _ => self.exec_statement(stmt),
5642                };
5643            }
5644        }
5645        Ok(PerlValue::UNDEF)
5646    }
5647
5648    /// Spawn `block` on a worker thread; returns an [`PerlValue::AsyncTask`] handle (`async { }` / `spawn { }`).
5649    pub(crate) fn spawn_async_block(&self, block: &Block) -> PerlValue {
5650        use parking_lot::Mutex as ParkMutex;
5651
5652        let block = block.clone();
5653        let subs = self.subs.clone();
5654        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5655        let result = Arc::new(ParkMutex::new(None));
5656        let join = Arc::new(ParkMutex::new(None));
5657        let result2 = result.clone();
5658        let h = std::thread::spawn(move || {
5659            let mut interp = Interpreter::new();
5660            interp.subs = subs;
5661            interp.scope.restore_capture(&scalars);
5662            interp.scope.restore_atomics(&aar, &ahash);
5663            interp.enable_parallel_guard();
5664            let r = match interp.exec_block(&block) {
5665                Ok(v) => Ok(v),
5666                Err(FlowOrError::Error(e)) => Err(e),
5667                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5668                    Err(PerlError::runtime("yield inside async/spawn block", 0))
5669                }
5670                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5671            };
5672            *result2.lock() = Some(r);
5673        });
5674        *join.lock() = Some(h);
5675        PerlValue::async_task(Arc::new(PerlAsyncTask { result, join }))
5676    }
5677
5678    /// `eval_timeout SECS { ... }` — run block on another thread; this thread waits (no Unix signals).
5679    pub(crate) fn eval_timeout_block(
5680        &mut self,
5681        body: &Block,
5682        secs: f64,
5683        line: usize,
5684    ) -> ExecResult {
5685        use std::sync::mpsc::channel;
5686        use std::time::Duration;
5687
5688        let block = body.clone();
5689        let subs = self.subs.clone();
5690        let struct_defs = self.struct_defs.clone();
5691        let enum_defs = self.enum_defs.clone();
5692        let (scalars, aar, ahash) = self.scope.capture_with_atomics();
5693        self.materialize_env_if_needed();
5694        let env = self.env.clone();
5695        let argv = self.argv.clone();
5696        let inc = self.scope.get_array("INC");
5697        let (tx, rx) = channel::<PerlResult<PerlValue>>();
5698        let _handle = std::thread::spawn(move || {
5699            let mut interp = Interpreter::new();
5700            interp.subs = subs;
5701            interp.struct_defs = struct_defs;
5702            interp.enum_defs = enum_defs;
5703            interp.env = env.clone();
5704            interp.argv = argv.clone();
5705            interp.scope.declare_array(
5706                "ARGV",
5707                argv.iter().map(|s| PerlValue::string(s.clone())).collect(),
5708            );
5709            for (k, v) in env {
5710                interp
5711                    .scope
5712                    .set_hash_element("ENV", &k, v)
5713                    .expect("set ENV in timeout thread");
5714            }
5715            interp.scope.declare_array("INC", inc);
5716            interp.scope.restore_capture(&scalars);
5717            interp.scope.restore_atomics(&aar, &ahash);
5718            interp.enable_parallel_guard();
5719            let out: PerlResult<PerlValue> = match interp.exec_block(&block) {
5720                Ok(v) => Ok(v),
5721                Err(FlowOrError::Error(e)) => Err(e),
5722                Err(FlowOrError::Flow(Flow::Yield(_))) => {
5723                    Err(PerlError::runtime("yield inside eval_timeout block", 0))
5724                }
5725                Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
5726            };
5727            let _ = tx.send(out);
5728        });
5729        let dur = Duration::from_secs_f64(secs.max(0.0));
5730        match rx.recv_timeout(dur) {
5731            Ok(Ok(v)) => Ok(v),
5732            Ok(Err(e)) => Err(FlowOrError::Error(e)),
5733            Err(std::sync::mpsc::RecvTimeoutError::Timeout) => Err(PerlError::runtime(
5734                format!(
5735                    "eval_timeout: exceeded {} second(s) (worker continues in background)",
5736                    secs
5737                ),
5738                line,
5739            )
5740            .into()),
5741            Err(std::sync::mpsc::RecvTimeoutError::Disconnected) => Err(PerlError::runtime(
5742                "eval_timeout: worker thread panicked or disconnected",
5743                line,
5744            )
5745            .into()),
5746        }
5747    }
5748
5749    fn exec_given_body(&mut self, body: &Block) -> ExecResult {
5750        let mut last = PerlValue::UNDEF;
5751        for stmt in body {
5752            match &stmt.kind {
5753                StmtKind::When { cond, body: wb } => {
5754                    if self.when_matches(cond)? {
5755                        return self.exec_block_smart(wb);
5756                    }
5757                }
5758                StmtKind::DefaultCase { body: db } => {
5759                    return self.exec_block_smart(db);
5760                }
5761                _ => {
5762                    last = self.exec_statement(stmt)?;
5763                }
5764            }
5765        }
5766        Ok(last)
5767    }
5768
5769    /// `given` after the topic has been evaluated to a value (VM bytecode path or direct use).
5770    pub(crate) fn exec_given_with_topic_value(
5771        &mut self,
5772        topic: PerlValue,
5773        body: &Block,
5774    ) -> ExecResult {
5775        self.scope_push_hook();
5776        self.scope.declare_scalar("_", topic);
5777        self.english_note_lexical_scalar("_");
5778        let r = self.exec_given_body(body);
5779        self.scope_pop_hook();
5780        r
5781    }
5782
5783    pub(crate) fn exec_given(&mut self, topic: &Expr, body: &Block) -> ExecResult {
5784        let t = self.eval_expr(topic)?;
5785        self.exec_given_with_topic_value(t, body)
5786    }
5787
5788    /// `when (COND)` — topic is `$_` (set by `given`).
5789    fn when_matches(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
5790        let topic = self.scope.get_scalar("_");
5791        let line = cond.line;
5792        match &cond.kind {
5793            ExprKind::Regex(pattern, flags) => {
5794                let re = self.compile_regex(pattern, flags, line)?;
5795                let s = topic.to_string();
5796                Ok(re.is_match(&s))
5797            }
5798            ExprKind::String(s) => Ok(topic.to_string() == *s),
5799            ExprKind::Integer(n) => Ok(topic.to_int() == *n),
5800            ExprKind::Float(f) => Ok((topic.to_number() - *f).abs() < 1e-9),
5801            _ => {
5802                let c = self.eval_expr(cond)?;
5803                Ok(self.smartmatch_when(&topic, &c))
5804            }
5805        }
5806    }
5807
5808    fn smartmatch_when(&self, topic: &PerlValue, c: &PerlValue) -> bool {
5809        if let Some(re) = c.as_regex() {
5810            return re.is_match(&topic.to_string());
5811        }
5812        topic.to_string() == c.to_string()
5813    }
5814
5815    /// Boolean rvalue: bare `/.../` is `$_ =~ /.../` (Perl). Does not assign `$_`; sets `$1`… like `=~`.
5816    pub(crate) fn eval_boolean_rvalue_condition(
5817        &mut self,
5818        cond: &Expr,
5819    ) -> Result<bool, FlowOrError> {
5820        match &cond.kind {
5821            ExprKind::Regex(pattern, flags) => {
5822                let topic = self.scope.get_scalar("_");
5823                let line = cond.line;
5824                let s = topic.to_string();
5825                let v = self.regex_match_execute(s, pattern, flags, false, "_", line)?;
5826                Ok(v.is_true())
5827            }
5828            // `while (<STDIN>)` / `if (<>)` — Perl assigns the line to `$_` before testing (definedness).
5829            ExprKind::ReadLine(_) => {
5830                let v = self.eval_expr(cond)?;
5831                self.scope.set_topic(v.clone());
5832                Ok(!v.is_undef())
5833            }
5834            _ => {
5835                let v = self.eval_expr(cond)?;
5836                Ok(v.is_true())
5837            }
5838        }
5839    }
5840
5841    /// Boolean condition for postfix `if` / `unless` / `while` / `until`.
5842    fn eval_postfix_condition(&mut self, cond: &Expr) -> Result<bool, FlowOrError> {
5843        self.eval_boolean_rvalue_condition(cond)
5844    }
5845
5846    pub(crate) fn eval_algebraic_match(
5847        &mut self,
5848        subject: &Expr,
5849        arms: &[MatchArm],
5850        line: usize,
5851    ) -> ExecResult {
5852        let val = self.eval_algebraic_match_subject(subject, line)?;
5853        self.eval_algebraic_match_with_subject_value(val, arms, line)
5854    }
5855
5856    /// Value used as `match` / `if let` subject: bare `@name` / `%name` bind like `\@name` / `\%name`.
5857    fn eval_algebraic_match_subject(&mut self, subject: &Expr, line: usize) -> ExecResult {
5858        match &subject.kind {
5859            ExprKind::ArrayVar(name) => {
5860                self.check_strict_array_var(name, line)?;
5861                let aname = self.stash_array_name_for_package(name);
5862                Ok(PerlValue::array_binding_ref(aname))
5863            }
5864            ExprKind::HashVar(name) => {
5865                self.check_strict_hash_var(name, line)?;
5866                self.touch_env_hash(name);
5867                Ok(PerlValue::hash_binding_ref(name.clone()))
5868            }
5869            _ => self.eval_expr(subject),
5870        }
5871    }
5872
5873    /// Algebraic `match` after the subject has been evaluated (VM bytecode path).
5874    pub(crate) fn eval_algebraic_match_with_subject_value(
5875        &mut self,
5876        val: PerlValue,
5877        arms: &[MatchArm],
5878        line: usize,
5879    ) -> ExecResult {
5880        // Exhaustive enum match: check variant coverage before matching
5881        if let Some(e) = val.as_enum_inst() {
5882            let has_catchall = arms.iter().any(|a| matches!(a.pattern, MatchPattern::Any));
5883            if !has_catchall {
5884                let covered: Vec<String> = arms
5885                    .iter()
5886                    .filter_map(|a| {
5887                        if let MatchPattern::Value(expr) = &a.pattern {
5888                            if let ExprKind::FuncCall { name, .. } = &expr.kind {
5889                                return name.rsplit_once("::").map(|(_, v)| v.to_string());
5890                            }
5891                        }
5892                        None
5893                    })
5894                    .collect();
5895                let missing: Vec<&str> = e
5896                    .def
5897                    .variants
5898                    .iter()
5899                    .filter(|v| !covered.contains(&v.name))
5900                    .map(|v| v.name.as_str())
5901                    .collect();
5902                if !missing.is_empty() {
5903                    return Err(PerlError::runtime(
5904                        format!(
5905                            "non-exhaustive match on enum `{}`: missing variant(s) {}",
5906                            e.def.name,
5907                            missing.join(", ")
5908                        ),
5909                        line,
5910                    )
5911                    .into());
5912                }
5913            }
5914        }
5915        for arm in arms {
5916            if let MatchPattern::Regex { pattern, flags } = &arm.pattern {
5917                let re = self.compile_regex(pattern, flags, line)?;
5918                let s = val.to_string();
5919                if let Some(caps) = re.captures(&s) {
5920                    self.scope_push_hook();
5921                    self.scope.declare_scalar("_", val.clone());
5922                    self.english_note_lexical_scalar("_");
5923                    self.apply_regex_captures(&s, 0, re.as_ref(), &caps, CaptureAllMode::Empty)?;
5924                    let guard_ok = if let Some(g) = &arm.guard {
5925                        self.eval_expr(g)?.is_true()
5926                    } else {
5927                        true
5928                    };
5929                    if !guard_ok {
5930                        self.scope_pop_hook();
5931                        continue;
5932                    }
5933                    let out = self.eval_expr(&arm.body);
5934                    self.scope_pop_hook();
5935                    return out;
5936                }
5937                continue;
5938            }
5939            if let Some(bindings) = self.match_pattern_try(&val, &arm.pattern, line)? {
5940                self.scope_push_hook();
5941                self.scope.declare_scalar("_", val.clone());
5942                self.english_note_lexical_scalar("_");
5943                for b in bindings {
5944                    match b {
5945                        PatternBinding::Scalar(name, v) => {
5946                            self.scope.declare_scalar(&name, v);
5947                            self.english_note_lexical_scalar(&name);
5948                        }
5949                        PatternBinding::Array(name, elems) => {
5950                            self.scope.declare_array(&name, elems);
5951                        }
5952                    }
5953                }
5954                let guard_ok = if let Some(g) = &arm.guard {
5955                    self.eval_expr(g)?.is_true()
5956                } else {
5957                    true
5958                };
5959                if !guard_ok {
5960                    self.scope_pop_hook();
5961                    continue;
5962                }
5963                let out = self.eval_expr(&arm.body);
5964                self.scope_pop_hook();
5965                return out;
5966            }
5967        }
5968        Err(PerlError::runtime(
5969            "match: no arm matched the value (add a `_` catch-all)",
5970            line,
5971        )
5972        .into())
5973    }
5974
5975    fn parse_duration_seconds(pv: &PerlValue) -> Option<f64> {
5976        let s = pv.to_string();
5977        let s = s.trim();
5978        if let Some(rest) = s.strip_suffix("ms") {
5979            return rest.trim().parse::<f64>().ok().map(|x| x / 1000.0);
5980        }
5981        if let Some(rest) = s.strip_suffix('s') {
5982            return rest.trim().parse::<f64>().ok();
5983        }
5984        if let Some(rest) = s.strip_suffix('m') {
5985            return rest.trim().parse::<f64>().ok().map(|x| x * 60.0);
5986        }
5987        s.parse::<f64>().ok()
5988    }
5989
5990    fn eval_retry_block(
5991        &mut self,
5992        body: &Block,
5993        times: &Expr,
5994        backoff: RetryBackoff,
5995        _line: usize,
5996    ) -> ExecResult {
5997        let max = self.eval_expr(times)?.to_int().max(1) as usize;
5998        let base_ms: u64 = 10;
5999        let mut attempt = 0usize;
6000        loop {
6001            attempt += 1;
6002            match self.exec_block(body) {
6003                Ok(v) => return Ok(v),
6004                Err(FlowOrError::Error(e)) => {
6005                    if attempt >= max {
6006                        return Err(FlowOrError::Error(e));
6007                    }
6008                    let delay_ms = match backoff {
6009                        RetryBackoff::None => 0,
6010                        RetryBackoff::Linear => base_ms.saturating_mul(attempt as u64),
6011                        RetryBackoff::Exponential => {
6012                            base_ms.saturating_mul(1u64 << (attempt as u32 - 1).min(30))
6013                        }
6014                    };
6015                    if delay_ms > 0 {
6016                        std::thread::sleep(Duration::from_millis(delay_ms));
6017                    }
6018                }
6019                Err(e) => return Err(e),
6020            }
6021        }
6022    }
6023
6024    fn eval_rate_limit_block(
6025        &mut self,
6026        slot: u32,
6027        max: &Expr,
6028        window: &Expr,
6029        body: &Block,
6030        _line: usize,
6031    ) -> ExecResult {
6032        let max_n = self.eval_expr(max)?.to_int().max(0) as usize;
6033        let window_sec = Self::parse_duration_seconds(&self.eval_expr(window)?)
6034            .filter(|s| *s > 0.0)
6035            .unwrap_or(1.0);
6036        let window_d = Duration::from_secs_f64(window_sec);
6037        let slot = slot as usize;
6038        while self.rate_limit_slots.len() <= slot {
6039            self.rate_limit_slots.push(VecDeque::new());
6040        }
6041        {
6042            let dq = &mut self.rate_limit_slots[slot];
6043            loop {
6044                let now = Instant::now();
6045                while let Some(t0) = dq.front().copied() {
6046                    if now.duration_since(t0) >= window_d {
6047                        dq.pop_front();
6048                    } else {
6049                        break;
6050                    }
6051                }
6052                if dq.len() < max_n || max_n == 0 {
6053                    break;
6054                }
6055                let t0 = dq.front().copied().unwrap();
6056                let wait = window_d.saturating_sub(now.duration_since(t0));
6057                if wait.is_zero() {
6058                    dq.pop_front();
6059                    continue;
6060                }
6061                std::thread::sleep(wait);
6062            }
6063            dq.push_back(Instant::now());
6064        }
6065        self.exec_block(body)
6066    }
6067
6068    fn eval_every_block(&mut self, interval: &Expr, body: &Block, _line: usize) -> ExecResult {
6069        let sec = Self::parse_duration_seconds(&self.eval_expr(interval)?)
6070            .filter(|s| *s > 0.0)
6071            .unwrap_or(1.0);
6072        loop {
6073            match self.exec_block(body) {
6074                Ok(_) => {}
6075                Err(e) => return Err(e),
6076            }
6077            std::thread::sleep(Duration::from_secs_f64(sec));
6078        }
6079    }
6080
6081    /// `->next` on a `gen { }` value: two-element **array ref** `(value, more)`; `more` is 0 when done.
6082    pub(crate) fn generator_next(&mut self, gen: &Arc<PerlGenerator>) -> PerlResult<PerlValue> {
6083        let pair = |value: PerlValue, more: i64| {
6084            PerlValue::array_ref(Arc::new(RwLock::new(vec![value, PerlValue::integer(more)])))
6085        };
6086        let mut exhausted = gen.exhausted.lock();
6087        if *exhausted {
6088            return Ok(pair(PerlValue::UNDEF, 0));
6089        }
6090        let mut pc = gen.pc.lock();
6091        let mut scope_started = gen.scope_started.lock();
6092        if *pc >= gen.block.len() {
6093            if *scope_started {
6094                self.scope_pop_hook();
6095                *scope_started = false;
6096            }
6097            *exhausted = true;
6098            return Ok(pair(PerlValue::UNDEF, 0));
6099        }
6100        if !*scope_started {
6101            self.scope_push_hook();
6102            *scope_started = true;
6103        }
6104        self.in_generator = true;
6105        while *pc < gen.block.len() {
6106            let stmt = &gen.block[*pc];
6107            match self.exec_statement(stmt) {
6108                Ok(_) => {
6109                    *pc += 1;
6110                }
6111                Err(FlowOrError::Flow(Flow::Yield(v))) => {
6112                    *pc += 1;
6113                    self.in_generator = false;
6114                    // Suspend: pop the generator frame before returning so outer `my $x = $g->next`
6115                    // binds in the caller block, not inside a frame left across yield.
6116                    if *scope_started {
6117                        self.scope_pop_hook();
6118                        *scope_started = false;
6119                    }
6120                    return Ok(pair(v, 1));
6121                }
6122                Err(e) => {
6123                    self.in_generator = false;
6124                    if *scope_started {
6125                        self.scope_pop_hook();
6126                        *scope_started = false;
6127                    }
6128                    return Err(match e {
6129                        FlowOrError::Error(ee) => ee,
6130                        FlowOrError::Flow(Flow::Yield(_)) => {
6131                            unreachable!("yield handled above")
6132                        }
6133                        FlowOrError::Flow(flow) => PerlError::runtime(
6134                            format!("unexpected control flow in generator: {:?}", flow),
6135                            0,
6136                        ),
6137                    });
6138                }
6139            }
6140        }
6141        self.in_generator = false;
6142        if *scope_started {
6143            self.scope_pop_hook();
6144            *scope_started = false;
6145        }
6146        *exhausted = true;
6147        Ok(pair(PerlValue::UNDEF, 0))
6148    }
6149
6150    fn match_pattern_try(
6151        &mut self,
6152        subject: &PerlValue,
6153        pattern: &MatchPattern,
6154        line: usize,
6155    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6156        match pattern {
6157            MatchPattern::Any => Ok(Some(vec![])),
6158            MatchPattern::Regex { .. } => {
6159                unreachable!("regex arms are handled in eval_algebraic_match")
6160            }
6161            MatchPattern::Value(expr) => {
6162                if self.match_pattern_value_alternation(subject, expr, line)? {
6163                    Ok(Some(vec![]))
6164                } else {
6165                    Ok(None)
6166                }
6167            }
6168            MatchPattern::Array(elems) => {
6169                let Some(arr) = self.match_subject_as_array(subject) else {
6170                    return Ok(None);
6171                };
6172                self.match_array_pattern_elems(&arr, elems, line)
6173            }
6174            MatchPattern::Hash(pairs) => {
6175                let Some(h) = self.match_subject_as_hash(subject) else {
6176                    return Ok(None);
6177                };
6178                self.match_hash_pattern_pairs(&h, pairs, line)
6179            }
6180            MatchPattern::OptionSome(name) => {
6181                let Some(arr) = self.match_subject_as_array(subject) else {
6182                    return Ok(None);
6183                };
6184                if arr.len() < 2 {
6185                    return Ok(None);
6186                }
6187                if !arr[1].is_true() {
6188                    return Ok(None);
6189                }
6190                Ok(Some(vec![PatternBinding::Scalar(
6191                    name.clone(),
6192                    arr[0].clone(),
6193                )]))
6194            }
6195        }
6196    }
6197
6198    /// Handle pattern alternation (e.g., `"foo" | "bar" | "baz"`) in match patterns.
6199    /// If the expression is a BitOr chain, recursively check if subject matches any alternative.
6200    fn match_pattern_value_alternation(
6201        &mut self,
6202        subject: &PerlValue,
6203        expr: &Expr,
6204        line: usize,
6205    ) -> Result<bool, FlowOrError> {
6206        if let ExprKind::BinOp {
6207            left,
6208            op: BinOp::BitOr,
6209            right,
6210        } = &expr.kind
6211        {
6212            if self.match_pattern_value_alternation(subject, left, line)? {
6213                return Ok(true);
6214            }
6215            return self.match_pattern_value_alternation(subject, right, line);
6216        }
6217        let pv = self.eval_expr(expr)?;
6218        Ok(self.smartmatch_when(subject, &pv))
6219    }
6220
6221    /// Array value for algebraic `match`, including `\@name` array references (binding refs).
6222    fn match_subject_as_array(&self, v: &PerlValue) -> Option<Vec<PerlValue>> {
6223        if let Some(a) = v.as_array_vec() {
6224            return Some(a);
6225        }
6226        if let Some(r) = v.as_array_ref() {
6227            return Some(r.read().clone());
6228        }
6229        if let Some(name) = v.as_array_binding_name() {
6230            return Some(self.scope.get_array(&name));
6231        }
6232        None
6233    }
6234
6235    fn match_subject_as_hash(&mut self, v: &PerlValue) -> Option<IndexMap<String, PerlValue>> {
6236        if let Some(h) = v.as_hash_map() {
6237            return Some(h);
6238        }
6239        if let Some(r) = v.as_hash_ref() {
6240            return Some(r.read().clone());
6241        }
6242        if let Some(name) = v.as_hash_binding_name() {
6243            self.touch_env_hash(&name);
6244            return Some(self.scope.get_hash(&name));
6245        }
6246        None
6247    }
6248
6249    /// `@$href{k1,k2}` rvalue — `key_values` are already-evaluated key expressions (each may be an
6250    /// array to expand, like [`Self::eval_hash_slice_key_components`]). Shared by VM [`Op::HashSliceDeref`](crate::bytecode::Op::HashSliceDeref).
6251    pub(crate) fn hash_slice_deref_values(
6252        &mut self,
6253        container: &PerlValue,
6254        key_values: &[PerlValue],
6255        line: usize,
6256    ) -> Result<PerlValue, FlowOrError> {
6257        let h = if let Some(m) = self.match_subject_as_hash(container) {
6258            m
6259        } else {
6260            return Err(PerlError::runtime(
6261                "Hash slice dereference needs a hash or hash reference value",
6262                line,
6263            )
6264            .into());
6265        };
6266        let mut result = Vec::new();
6267        for kv in key_values {
6268            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6269                vv.iter().map(|x| x.to_string()).collect()
6270            } else {
6271                vec![kv.to_string()]
6272            };
6273            for k in key_strings {
6274                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6275            }
6276        }
6277        Ok(PerlValue::array(result))
6278    }
6279
6280    /// Single-key write for a hash slice container (hash ref or package hash name).
6281    /// Perl applies slice updates (`+=`, `++`, …) only to the **last** key for multi-key slices.
6282    pub(crate) fn assign_hash_slice_one_key(
6283        &mut self,
6284        container: PerlValue,
6285        key: &str,
6286        val: PerlValue,
6287        line: usize,
6288    ) -> Result<PerlValue, FlowOrError> {
6289        if let Some(r) = container.as_hash_ref() {
6290            r.write().insert(key.to_string(), val);
6291            return Ok(PerlValue::UNDEF);
6292        }
6293        if let Some(name) = container.as_hash_binding_name() {
6294            self.touch_env_hash(&name);
6295            self.scope
6296                .set_hash_element(&name, key, val)
6297                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6298            return Ok(PerlValue::UNDEF);
6299        }
6300        if let Some(s) = container.as_str() {
6301            self.touch_env_hash(&s);
6302            if self.strict_refs {
6303                return Err(PerlError::runtime(
6304                    format!(
6305                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6306                        s
6307                    ),
6308                    line,
6309                )
6310                .into());
6311            }
6312            self.scope
6313                .set_hash_element(&s, key, val)
6314                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6315            return Ok(PerlValue::UNDEF);
6316        }
6317        Err(PerlError::runtime(
6318            "Hash slice assignment needs a hash or hash reference value",
6319            line,
6320        )
6321        .into())
6322    }
6323
6324    /// `%name{k1,k2} = LIST` — element-wise like [`Self::assign_hash_slice_deref`] on a stash hash.
6325    /// Shared by VM [`crate::bytecode::Op::SetHashSlice`].
6326    pub(crate) fn assign_named_hash_slice(
6327        &mut self,
6328        hash: &str,
6329        key_values: Vec<PerlValue>,
6330        val: PerlValue,
6331        line: usize,
6332    ) -> Result<PerlValue, FlowOrError> {
6333        self.touch_env_hash(hash);
6334        let mut ks: Vec<String> = Vec::new();
6335        for kv in key_values {
6336            if let Some(vv) = kv.as_array_vec() {
6337                ks.extend(vv.iter().map(|x| x.to_string()));
6338            } else {
6339                ks.push(kv.to_string());
6340            }
6341        }
6342        if ks.is_empty() {
6343            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6344        }
6345        let items = val.to_list();
6346        for (i, k) in ks.iter().enumerate() {
6347            let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6348            self.scope
6349                .set_hash_element(hash, k, v)
6350                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6351        }
6352        Ok(PerlValue::UNDEF)
6353    }
6354
6355    /// `@$href{k1,k2} = LIST` — shared by VM [`Op::SetHashSliceDeref`](crate::bytecode::Op::SetHashSliceDeref) and [`Self::assign_value`].
6356    pub(crate) fn assign_hash_slice_deref(
6357        &mut self,
6358        container: PerlValue,
6359        key_values: Vec<PerlValue>,
6360        val: PerlValue,
6361        line: usize,
6362    ) -> Result<PerlValue, FlowOrError> {
6363        let mut ks: Vec<String> = Vec::new();
6364        for kv in key_values {
6365            if let Some(vv) = kv.as_array_vec() {
6366                ks.extend(vv.iter().map(|x| x.to_string()));
6367            } else {
6368                ks.push(kv.to_string());
6369            }
6370        }
6371        if ks.is_empty() {
6372            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6373        }
6374        let items = val.to_list();
6375        if let Some(r) = container.as_hash_ref() {
6376            let mut h = r.write();
6377            for (i, k) in ks.iter().enumerate() {
6378                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6379                h.insert(k.clone(), v);
6380            }
6381            return Ok(PerlValue::UNDEF);
6382        }
6383        if let Some(name) = container.as_hash_binding_name() {
6384            self.touch_env_hash(&name);
6385            for (i, k) in ks.iter().enumerate() {
6386                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6387                self.scope
6388                    .set_hash_element(&name, k, v)
6389                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6390            }
6391            return Ok(PerlValue::UNDEF);
6392        }
6393        if let Some(s) = container.as_str() {
6394            if self.strict_refs {
6395                return Err(PerlError::runtime(
6396                    format!(
6397                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
6398                        s
6399                    ),
6400                    line,
6401                )
6402                .into());
6403            }
6404            self.touch_env_hash(&s);
6405            for (i, k) in ks.iter().enumerate() {
6406                let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
6407                self.scope
6408                    .set_hash_element(&s, k, v)
6409                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
6410            }
6411            return Ok(PerlValue::UNDEF);
6412        }
6413        Err(PerlError::runtime(
6414            "Hash slice assignment needs a hash or hash reference value",
6415            line,
6416        )
6417        .into())
6418    }
6419
6420    /// `@$href{k1,k2} OP= rhs` — shared by VM [`Op::HashSliceDerefCompound`](crate::bytecode::Op::HashSliceDerefCompound).
6421    /// Perl 5 applies the compound op only to the **last** slice element.
6422    pub(crate) fn compound_assign_hash_slice_deref(
6423        &mut self,
6424        container: PerlValue,
6425        key_values: Vec<PerlValue>,
6426        op: BinOp,
6427        rhs: PerlValue,
6428        line: usize,
6429    ) -> Result<PerlValue, FlowOrError> {
6430        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6431        let last_old = old_list
6432            .to_list()
6433            .last()
6434            .cloned()
6435            .unwrap_or(PerlValue::UNDEF);
6436        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6437        let mut ks: Vec<String> = Vec::new();
6438        for kv in &key_values {
6439            if let Some(vv) = kv.as_array_vec() {
6440                ks.extend(vv.iter().map(|x| x.to_string()));
6441            } else {
6442                ks.push(kv.to_string());
6443            }
6444        }
6445        if ks.is_empty() {
6446            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6447        }
6448        let last_key = ks.last().expect("non-empty ks");
6449        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6450        Ok(new_val)
6451    }
6452
6453    /// `++@$href{k1,k2}` / `--…` / `…++` / `…--` — shared by VM [`Op::HashSliceDerefIncDec`](crate::bytecode::Op::HashSliceDerefIncDec).
6454    /// Perl 5 updates only the **last** key; pre `++`/`--` return the new value, post forms return
6455    /// the **old** value of that last element.
6456    ///
6457    /// `kind` byte: 0 = PreInc, 1 = PreDec, 2 = PostInc, 3 = PostDec.
6458    pub(crate) fn hash_slice_deref_inc_dec(
6459        &mut self,
6460        container: PerlValue,
6461        key_values: Vec<PerlValue>,
6462        kind: u8,
6463        line: usize,
6464    ) -> Result<PerlValue, FlowOrError> {
6465        let old_list = self.hash_slice_deref_values(&container, &key_values, line)?;
6466        let last_old = old_list
6467            .to_list()
6468            .last()
6469            .cloned()
6470            .unwrap_or(PerlValue::UNDEF);
6471        let new_val = if kind & 1 == 0 {
6472            PerlValue::integer(last_old.to_int() + 1)
6473        } else {
6474            PerlValue::integer(last_old.to_int() - 1)
6475        };
6476        let mut ks: Vec<String> = Vec::new();
6477        for kv in &key_values {
6478            if let Some(vv) = kv.as_array_vec() {
6479                ks.extend(vv.iter().map(|x| x.to_string()));
6480            } else {
6481                ks.push(kv.to_string());
6482            }
6483        }
6484        let last_key = ks.last().ok_or_else(|| {
6485            PerlError::runtime("Hash slice increment needs at least one key", line)
6486        })?;
6487        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6488        Ok(if kind < 2 { new_val } else { last_old })
6489    }
6490
6491    fn hash_slice_named_values(&mut self, hash: &str, key_values: &[PerlValue]) -> PerlValue {
6492        self.touch_env_hash(hash);
6493        let h = self.scope.get_hash(hash);
6494        let mut result = Vec::new();
6495        for kv in key_values {
6496            let key_strings: Vec<String> = if let Some(vv) = kv.as_array_vec() {
6497                vv.iter().map(|x| x.to_string()).collect()
6498            } else {
6499                vec![kv.to_string()]
6500            };
6501            for k in key_strings {
6502                result.push(h.get(&k).cloned().unwrap_or(PerlValue::UNDEF));
6503            }
6504        }
6505        PerlValue::array(result)
6506    }
6507
6508    /// `@h{k1,k2} OP= rhs` on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceCompound`].
6509    pub(crate) fn compound_assign_named_hash_slice(
6510        &mut self,
6511        hash: &str,
6512        key_values: Vec<PerlValue>,
6513        op: BinOp,
6514        rhs: PerlValue,
6515        line: usize,
6516    ) -> Result<PerlValue, FlowOrError> {
6517        let old_list = self.hash_slice_named_values(hash, &key_values);
6518        let last_old = old_list
6519            .to_list()
6520            .last()
6521            .cloned()
6522            .unwrap_or(PerlValue::UNDEF);
6523        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
6524        let mut ks: Vec<String> = Vec::new();
6525        for kv in &key_values {
6526            if let Some(vv) = kv.as_array_vec() {
6527                ks.extend(vv.iter().map(|x| x.to_string()));
6528            } else {
6529                ks.push(kv.to_string());
6530            }
6531        }
6532        if ks.is_empty() {
6533            return Err(PerlError::runtime("assign to empty hash slice", line).into());
6534        }
6535        let last_key = ks.last().expect("non-empty ks");
6536        let container = PerlValue::string(hash.to_string());
6537        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6538        Ok(new_val)
6539    }
6540
6541    /// `++@h{k1,k2}` / … on a stash hash — shared by VM [`crate::bytecode::Op::NamedHashSliceIncDec`].
6542    pub(crate) fn named_hash_slice_inc_dec(
6543        &mut self,
6544        hash: &str,
6545        key_values: Vec<PerlValue>,
6546        kind: u8,
6547        line: usize,
6548    ) -> Result<PerlValue, FlowOrError> {
6549        let old_list = self.hash_slice_named_values(hash, &key_values);
6550        let last_old = old_list
6551            .to_list()
6552            .last()
6553            .cloned()
6554            .unwrap_or(PerlValue::UNDEF);
6555        let new_val = if kind & 1 == 0 {
6556            PerlValue::integer(last_old.to_int() + 1)
6557        } else {
6558            PerlValue::integer(last_old.to_int() - 1)
6559        };
6560        let mut ks: Vec<String> = Vec::new();
6561        for kv in &key_values {
6562            if let Some(vv) = kv.as_array_vec() {
6563                ks.extend(vv.iter().map(|x| x.to_string()));
6564            } else {
6565                ks.push(kv.to_string());
6566            }
6567        }
6568        let last_key = ks.last().ok_or_else(|| {
6569            PerlError::runtime("Hash slice increment needs at least one key", line)
6570        })?;
6571        let container = PerlValue::string(hash.to_string());
6572        self.assign_hash_slice_one_key(container, last_key, new_val.clone(), line)?;
6573        Ok(if kind < 2 { new_val } else { last_old })
6574    }
6575
6576    fn match_array_pattern_elems(
6577        &mut self,
6578        arr: &[PerlValue],
6579        elems: &[MatchArrayElem],
6580        line: usize,
6581    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6582        let has_rest = elems
6583            .iter()
6584            .any(|e| matches!(e, MatchArrayElem::Rest | MatchArrayElem::RestBind(_)));
6585        let mut binds: Vec<PatternBinding> = Vec::new();
6586        let mut idx = 0usize;
6587        for (i, elem) in elems.iter().enumerate() {
6588            match elem {
6589                MatchArrayElem::Rest => {
6590                    if i != elems.len() - 1 {
6591                        return Err(PerlError::runtime(
6592                            "internal: `*` must be last in array match pattern",
6593                            line,
6594                        )
6595                        .into());
6596                    }
6597                    return Ok(Some(binds));
6598                }
6599                MatchArrayElem::RestBind(name) => {
6600                    if i != elems.len() - 1 {
6601                        return Err(PerlError::runtime(
6602                            "internal: `@name` rest bind must be last in array match pattern",
6603                            line,
6604                        )
6605                        .into());
6606                    }
6607                    let tail = arr[idx..].to_vec();
6608                    binds.push(PatternBinding::Array(name.clone(), tail));
6609                    return Ok(Some(binds));
6610                }
6611                MatchArrayElem::CaptureScalar(name) => {
6612                    if idx >= arr.len() {
6613                        return Ok(None);
6614                    }
6615                    binds.push(PatternBinding::Scalar(name.clone(), arr[idx].clone()));
6616                    idx += 1;
6617                }
6618                MatchArrayElem::Expr(e) => {
6619                    if idx >= arr.len() {
6620                        return Ok(None);
6621                    }
6622                    let expected = self.eval_expr(e)?;
6623                    if !self.smartmatch_when(&arr[idx], &expected) {
6624                        return Ok(None);
6625                    }
6626                    idx += 1;
6627                }
6628            }
6629        }
6630        if !has_rest && idx != arr.len() {
6631            return Ok(None);
6632        }
6633        Ok(Some(binds))
6634    }
6635
6636    fn match_hash_pattern_pairs(
6637        &mut self,
6638        h: &IndexMap<String, PerlValue>,
6639        pairs: &[MatchHashPair],
6640        _line: usize,
6641    ) -> Result<Option<Vec<PatternBinding>>, FlowOrError> {
6642        let mut binds = Vec::new();
6643        for pair in pairs {
6644            match pair {
6645                MatchHashPair::KeyOnly { key } => {
6646                    let ks = self.eval_expr(key)?.to_string();
6647                    if !h.contains_key(&ks) {
6648                        return Ok(None);
6649                    }
6650                }
6651                MatchHashPair::Capture { key, name } => {
6652                    let ks = self.eval_expr(key)?.to_string();
6653                    let Some(v) = h.get(&ks) else {
6654                        return Ok(None);
6655                    };
6656                    binds.push(PatternBinding::Scalar(name.clone(), v.clone()));
6657                }
6658            }
6659        }
6660        Ok(Some(binds))
6661    }
6662
6663    /// Check if a block declares variables (needs its own scope frame).
6664    #[inline]
6665    fn block_needs_scope(block: &Block) -> bool {
6666        block.iter().any(|s| match &s.kind {
6667            StmtKind::My(_)
6668            | StmtKind::Our(_)
6669            | StmtKind::Local(_)
6670            | StmtKind::State(_)
6671            | StmtKind::LocalExpr { .. } => true,
6672            StmtKind::StmtGroup(inner) => Self::block_needs_scope(inner),
6673            _ => false,
6674        })
6675    }
6676
6677    /// Execute block, only pushing a scope frame if needed.
6678    #[inline]
6679    pub(crate) fn exec_block_smart(&mut self, block: &Block) -> ExecResult {
6680        if Self::block_needs_scope(block) {
6681            self.exec_block(block)
6682        } else {
6683            self.exec_block_no_scope(block)
6684        }
6685    }
6686
6687    fn exec_statement(&mut self, stmt: &Statement) -> ExecResult {
6688        let t0 = self.profiler.is_some().then(std::time::Instant::now);
6689        let r = self.exec_statement_inner(stmt);
6690        if let (Some(prof), Some(t0)) = (&mut self.profiler, t0) {
6691            prof.on_line(&self.file, stmt.line, t0.elapsed());
6692        }
6693        r
6694    }
6695
6696    fn exec_statement_inner(&mut self, stmt: &Statement) -> ExecResult {
6697        if let Err(e) = crate::perl_signal::poll(self) {
6698            return Err(FlowOrError::Error(e));
6699        }
6700        if let Err(e) = self.drain_pending_destroys(stmt.line) {
6701            return Err(FlowOrError::Error(e));
6702        }
6703        match &stmt.kind {
6704            StmtKind::StmtGroup(block) => self.exec_block_no_scope(block),
6705            StmtKind::Expression(expr) => self.eval_expr_ctx(expr, WantarrayCtx::Void),
6706            StmtKind::If {
6707                condition,
6708                body,
6709                elsifs,
6710                else_block,
6711            } => {
6712                if self.eval_boolean_rvalue_condition(condition)? {
6713                    return self.exec_block(body);
6714                }
6715                for (c, b) in elsifs {
6716                    if self.eval_boolean_rvalue_condition(c)? {
6717                        return self.exec_block(b);
6718                    }
6719                }
6720                if let Some(eb) = else_block {
6721                    return self.exec_block(eb);
6722                }
6723                Ok(PerlValue::UNDEF)
6724            }
6725            StmtKind::Unless {
6726                condition,
6727                body,
6728                else_block,
6729            } => {
6730                if !self.eval_boolean_rvalue_condition(condition)? {
6731                    return self.exec_block(body);
6732                }
6733                if let Some(eb) = else_block {
6734                    return self.exec_block(eb);
6735                }
6736                Ok(PerlValue::UNDEF)
6737            }
6738            StmtKind::While {
6739                condition,
6740                body,
6741                label,
6742                continue_block,
6743            } => {
6744                'outer: loop {
6745                    if !self.eval_boolean_rvalue_condition(condition)? {
6746                        break;
6747                    }
6748                    'inner: loop {
6749                        match self.exec_block_smart(body) {
6750                            Ok(_) => break 'inner,
6751                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6752                                if l == label || l.is_none() =>
6753                            {
6754                                break 'outer;
6755                            }
6756                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6757                                if l == label || l.is_none() =>
6758                            {
6759                                if let Some(cb) = continue_block {
6760                                    let _ = self.exec_block_smart(cb);
6761                                }
6762                                continue 'outer;
6763                            }
6764                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6765                                if l == label || l.is_none() =>
6766                            {
6767                                continue 'inner;
6768                            }
6769                            Err(e) => return Err(e),
6770                        }
6771                    }
6772                    if let Some(cb) = continue_block {
6773                        let _ = self.exec_block_smart(cb);
6774                    }
6775                }
6776                Ok(PerlValue::UNDEF)
6777            }
6778            StmtKind::Until {
6779                condition,
6780                body,
6781                label,
6782                continue_block,
6783            } => {
6784                'outer: loop {
6785                    if self.eval_boolean_rvalue_condition(condition)? {
6786                        break;
6787                    }
6788                    'inner: loop {
6789                        match self.exec_block(body) {
6790                            Ok(_) => break 'inner,
6791                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6792                                if l == label || l.is_none() =>
6793                            {
6794                                break 'outer;
6795                            }
6796                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6797                                if l == label || l.is_none() =>
6798                            {
6799                                if let Some(cb) = continue_block {
6800                                    let _ = self.exec_block_smart(cb);
6801                                }
6802                                continue 'outer;
6803                            }
6804                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6805                                if l == label || l.is_none() =>
6806                            {
6807                                continue 'inner;
6808                            }
6809                            Err(e) => return Err(e),
6810                        }
6811                    }
6812                    if let Some(cb) = continue_block {
6813                        let _ = self.exec_block_smart(cb);
6814                    }
6815                }
6816                Ok(PerlValue::UNDEF)
6817            }
6818            StmtKind::DoWhile { body, condition } => {
6819                loop {
6820                    self.exec_block(body)?;
6821                    if !self.eval_boolean_rvalue_condition(condition)? {
6822                        break;
6823                    }
6824                }
6825                Ok(PerlValue::UNDEF)
6826            }
6827            StmtKind::For {
6828                init,
6829                condition,
6830                step,
6831                body,
6832                label,
6833                continue_block,
6834            } => {
6835                self.scope_push_hook();
6836                if let Some(init) = init {
6837                    self.exec_statement(init)?;
6838                }
6839                'outer: loop {
6840                    if let Some(cond) = condition {
6841                        if !self.eval_boolean_rvalue_condition(cond)? {
6842                            break;
6843                        }
6844                    }
6845                    'inner: loop {
6846                        match self.exec_block_smart(body) {
6847                            Ok(_) => break 'inner,
6848                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6849                                if l == label || l.is_none() =>
6850                            {
6851                                break 'outer;
6852                            }
6853                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6854                                if l == label || l.is_none() =>
6855                            {
6856                                if let Some(cb) = continue_block {
6857                                    let _ = self.exec_block_smart(cb);
6858                                }
6859                                if let Some(step) = step {
6860                                    self.eval_expr(step)?;
6861                                }
6862                                continue 'outer;
6863                            }
6864                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6865                                if l == label || l.is_none() =>
6866                            {
6867                                continue 'inner;
6868                            }
6869                            Err(e) => {
6870                                self.scope_pop_hook();
6871                                return Err(e);
6872                            }
6873                        }
6874                    }
6875                    if let Some(cb) = continue_block {
6876                        let _ = self.exec_block_smart(cb);
6877                    }
6878                    if let Some(step) = step {
6879                        self.eval_expr(step)?;
6880                    }
6881                }
6882                self.scope_pop_hook();
6883                Ok(PerlValue::UNDEF)
6884            }
6885            StmtKind::Foreach {
6886                var,
6887                list,
6888                body,
6889                label,
6890                continue_block,
6891            } => {
6892                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
6893                let items = list_val.to_list();
6894                self.scope_push_hook();
6895                self.scope.declare_scalar(var, PerlValue::UNDEF);
6896                self.english_note_lexical_scalar(var);
6897                let mut i = 0usize;
6898                'outer: while i < items.len() {
6899                    self.scope
6900                        .set_scalar(var, items[i].clone())
6901                        .map_err(|e| FlowOrError::Error(e.at_line(stmt.line)))?;
6902                    'inner: loop {
6903                        match self.exec_block_smart(body) {
6904                            Ok(_) => break 'inner,
6905                            Err(FlowOrError::Flow(Flow::Last(ref l)))
6906                                if l == label || l.is_none() =>
6907                            {
6908                                break 'outer;
6909                            }
6910                            Err(FlowOrError::Flow(Flow::Next(ref l)))
6911                                if l == label || l.is_none() =>
6912                            {
6913                                if let Some(cb) = continue_block {
6914                                    let _ = self.exec_block_smart(cb);
6915                                }
6916                                i += 1;
6917                                continue 'outer;
6918                            }
6919                            Err(FlowOrError::Flow(Flow::Redo(ref l)))
6920                                if l == label || l.is_none() =>
6921                            {
6922                                continue 'inner;
6923                            }
6924                            Err(e) => {
6925                                self.scope_pop_hook();
6926                                return Err(e);
6927                            }
6928                        }
6929                    }
6930                    if let Some(cb) = continue_block {
6931                        let _ = self.exec_block_smart(cb);
6932                    }
6933                    i += 1;
6934                }
6935                self.scope_pop_hook();
6936                Ok(PerlValue::UNDEF)
6937            }
6938            StmtKind::SubDecl {
6939                name,
6940                params,
6941                body,
6942                prototype,
6943            } => {
6944                let key = self.qualify_sub_key(name);
6945                let captured = self.scope.capture();
6946                let closure_env = if captured.is_empty() {
6947                    None
6948                } else {
6949                    Some(captured)
6950                };
6951                let mut sub = PerlSub {
6952                    name: name.clone(),
6953                    params: params.clone(),
6954                    body: body.clone(),
6955                    closure_env,
6956                    prototype: prototype.clone(),
6957                    fib_like: None,
6958                };
6959                sub.fib_like = crate::fib_like_tail::detect_fib_like_recursive_add(&sub);
6960                self.subs.insert(key, Arc::new(sub));
6961                Ok(PerlValue::UNDEF)
6962            }
6963            StmtKind::StructDecl { def } => {
6964                if self.struct_defs.contains_key(&def.name) {
6965                    return Err(PerlError::runtime(
6966                        format!("duplicate struct `{}`", def.name),
6967                        stmt.line,
6968                    )
6969                    .into());
6970                }
6971                self.struct_defs
6972                    .insert(def.name.clone(), Arc::new(def.clone()));
6973                Ok(PerlValue::UNDEF)
6974            }
6975            StmtKind::EnumDecl { def } => {
6976                if self.enum_defs.contains_key(&def.name) {
6977                    return Err(PerlError::runtime(
6978                        format!("duplicate enum `{}`", def.name),
6979                        stmt.line,
6980                    )
6981                    .into());
6982                }
6983                self.enum_defs
6984                    .insert(def.name.clone(), Arc::new(def.clone()));
6985                Ok(PerlValue::UNDEF)
6986            }
6987            StmtKind::ClassDecl { def } => {
6988                if self.class_defs.contains_key(&def.name) {
6989                    return Err(PerlError::runtime(
6990                        format!("duplicate class `{}`", def.name),
6991                        stmt.line,
6992                    )
6993                    .into());
6994                }
6995                // Final class enforcement: prevent subclassing
6996                for parent_name in &def.extends {
6997                    if let Some(parent_def) = self.class_defs.get(parent_name) {
6998                        if parent_def.is_final {
6999                            return Err(PerlError::runtime(
7000                                format!("cannot extend final class `{}`", parent_name),
7001                                stmt.line,
7002                            )
7003                            .into());
7004                        }
7005                        // Final method enforcement: prevent overriding
7006                        for m in &def.methods {
7007                            if let Some(parent_method) = parent_def.method(&m.name) {
7008                                if parent_method.is_final {
7009                                    return Err(PerlError::runtime(
7010                                        format!(
7011                                            "cannot override final method `{}` from class `{}`",
7012                                            m.name, parent_name
7013                                        ),
7014                                        stmt.line,
7015                                    )
7016                                    .into());
7017                                }
7018                            }
7019                        }
7020                    }
7021                }
7022                // Trait contract enforcement + default method inheritance
7023                let mut def = def.clone();
7024                for trait_name in &def.implements.clone() {
7025                    if let Some(trait_def) = self.trait_defs.get(trait_name).cloned() {
7026                        for required in trait_def.required_methods() {
7027                            let has_method = def.methods.iter().any(|m| m.name == required.name);
7028                            if !has_method {
7029                                return Err(PerlError::runtime(
7030                                    format!(
7031                                        "class `{}` implements trait `{}` but does not define required method `{}`",
7032                                        def.name, trait_name, required.name
7033                                    ),
7034                                    stmt.line,
7035                                )
7036                                .into());
7037                            }
7038                        }
7039                        // Inherit default methods from trait (methods with bodies)
7040                        for tm in &trait_def.methods {
7041                            if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
7042                                def.methods.push(tm.clone());
7043                            }
7044                        }
7045                    }
7046                }
7047                // Abstract method enforcement: concrete subclasses must implement
7048                // all abstract methods (body-less methods) from abstract parents
7049                if !def.is_abstract {
7050                    for parent_name in &def.extends.clone() {
7051                        if let Some(parent_def) = self.class_defs.get(parent_name) {
7052                            if parent_def.is_abstract {
7053                                for m in &parent_def.methods {
7054                                    if m.body.is_none()
7055                                        && !def.methods.iter().any(|dm| dm.name == m.name)
7056                                    {
7057                                        return Err(PerlError::runtime(
7058                                            format!(
7059                                                "class `{}` must implement abstract method `{}` from `{}`",
7060                                                def.name, m.name, parent_name
7061                                            ),
7062                                            stmt.line,
7063                                        )
7064                                        .into());
7065                                    }
7066                                }
7067                            }
7068                        }
7069                    }
7070                }
7071                // Initialize static fields
7072                for sf in &def.static_fields {
7073                    let val = if let Some(ref expr) = sf.default {
7074                        self.eval_expr(expr)?
7075                    } else {
7076                        PerlValue::UNDEF
7077                    };
7078                    let key = format!("{}::{}", def.name, sf.name);
7079                    self.scope.declare_scalar(&key, val);
7080                }
7081                // Register class methods into self.subs so method dispatch finds them.
7082                for m in &def.methods {
7083                    if let Some(ref body) = m.body {
7084                        let fq = format!("{}::{}", def.name, m.name);
7085                        let sub = Arc::new(PerlSub {
7086                            name: fq.clone(),
7087                            params: m.params.clone(),
7088                            body: body.clone(),
7089                            closure_env: None,
7090                            prototype: None,
7091                            fib_like: None,
7092                        });
7093                        self.subs.insert(fq, sub);
7094                    }
7095                }
7096                // Set @ClassName::ISA so MRO/isa resolution works.
7097                if !def.extends.is_empty() {
7098                    let isa_key = format!("{}::ISA", def.name);
7099                    let parents: Vec<PerlValue> = def
7100                        .extends
7101                        .iter()
7102                        .map(|p| PerlValue::string(p.clone()))
7103                        .collect();
7104                    self.scope.declare_array(&isa_key, parents);
7105                }
7106                self.class_defs.insert(def.name.clone(), Arc::new(def));
7107                Ok(PerlValue::UNDEF)
7108            }
7109            StmtKind::TraitDecl { def } => {
7110                if self.trait_defs.contains_key(&def.name) {
7111                    return Err(PerlError::runtime(
7112                        format!("duplicate trait `{}`", def.name),
7113                        stmt.line,
7114                    )
7115                    .into());
7116                }
7117                self.trait_defs
7118                    .insert(def.name.clone(), Arc::new(def.clone()));
7119                Ok(PerlValue::UNDEF)
7120            }
7121            StmtKind::My(decls) | StmtKind::Our(decls) => {
7122                let is_our = matches!(&stmt.kind, StmtKind::Our(_));
7123                // For list assignment my ($a, $b) = (10, 20), distribute elements.
7124                // All decls share the same initializer in the AST (parser clones it).
7125                if decls.len() > 1 && decls[0].initializer.is_some() {
7126                    let val = self.eval_expr_ctx(
7127                        decls[0].initializer.as_ref().unwrap(),
7128                        WantarrayCtx::List,
7129                    )?;
7130                    let items = val.to_list();
7131                    let mut idx = 0;
7132                    for decl in decls {
7133                        match decl.sigil {
7134                            Sigil::Scalar => {
7135                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7136                                let skey = if is_our {
7137                                    self.stash_scalar_name_for_package(&decl.name)
7138                                } else {
7139                                    decl.name.clone()
7140                                };
7141                                self.scope.declare_scalar_frozen(
7142                                    &skey,
7143                                    v,
7144                                    decl.frozen,
7145                                    decl.type_annotation.clone(),
7146                                )?;
7147                                self.english_note_lexical_scalar(&decl.name);
7148                                if is_our {
7149                                    self.note_our_scalar(&decl.name);
7150                                }
7151                                idx += 1;
7152                            }
7153                            Sigil::Array => {
7154                                // Array slurps remaining elements
7155                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7156                                idx = items.len();
7157                                if is_our {
7158                                    self.record_exporter_our_array_name(&decl.name, &rest);
7159                                }
7160                                let aname = self.stash_array_name_for_package(&decl.name);
7161                                self.scope.declare_array(&aname, rest);
7162                            }
7163                            Sigil::Hash => {
7164                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7165                                idx = items.len();
7166                                let mut map = IndexMap::new();
7167                                let mut i = 0;
7168                                while i + 1 < rest.len() {
7169                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7170                                    i += 2;
7171                                }
7172                                self.scope.declare_hash(&decl.name, map);
7173                            }
7174                            Sigil::Typeglob => {
7175                                return Err(PerlError::runtime(
7176                                    "list assignment to typeglob (`my (*a,*b)=...`) is not supported",
7177                                    stmt.line,
7178                                )
7179                                .into());
7180                            }
7181                        }
7182                    }
7183                } else {
7184                    // Single decl or no initializer
7185                    for decl in decls {
7186                        // `our $Verbose ||= 0` / `my $x //= 1` — Perl declares the variable before
7187                        // evaluating `||=` / `//=` / `+=` … so strict sees a binding when the
7188                        // compound op reads the lhs (see system Exporter.pm).
7189                        let compound_init = decl
7190                            .initializer
7191                            .as_ref()
7192                            .is_some_and(|i| matches!(i.kind, ExprKind::CompoundAssign { .. }));
7193
7194                        if compound_init {
7195                            match decl.sigil {
7196                                Sigil::Typeglob => {
7197                                    return Err(PerlError::runtime(
7198                                        "compound assignment on typeglob declaration is not supported",
7199                                        stmt.line,
7200                                    )
7201                                    .into());
7202                                }
7203                                Sigil::Scalar => {
7204                                    let skey = if is_our {
7205                                        self.stash_scalar_name_for_package(&decl.name)
7206                                    } else {
7207                                        decl.name.clone()
7208                                    };
7209                                    self.scope.declare_scalar_frozen(
7210                                        &skey,
7211                                        PerlValue::UNDEF,
7212                                        decl.frozen,
7213                                        decl.type_annotation.clone(),
7214                                    )?;
7215                                    self.english_note_lexical_scalar(&decl.name);
7216                                    if is_our {
7217                                        self.note_our_scalar(&decl.name);
7218                                    }
7219                                    let init = decl.initializer.as_ref().unwrap();
7220                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7221                                }
7222                                Sigil::Array => {
7223                                    let aname = self.stash_array_name_for_package(&decl.name);
7224                                    self.scope.declare_array_frozen(&aname, vec![], decl.frozen);
7225                                    let init = decl.initializer.as_ref().unwrap();
7226                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7227                                    if is_our {
7228                                        let items = self.scope.get_array(&aname);
7229                                        self.record_exporter_our_array_name(&decl.name, &items);
7230                                    }
7231                                }
7232                                Sigil::Hash => {
7233                                    self.scope.declare_hash_frozen(
7234                                        &decl.name,
7235                                        IndexMap::new(),
7236                                        decl.frozen,
7237                                    );
7238                                    let init = decl.initializer.as_ref().unwrap();
7239                                    self.eval_expr_ctx(init, WantarrayCtx::Void)?;
7240                                }
7241                            }
7242                            continue;
7243                        }
7244
7245                        let val = if let Some(init) = &decl.initializer {
7246                            let ctx = match decl.sigil {
7247                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7248                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7249                            };
7250                            self.eval_expr_ctx(init, ctx)?
7251                        } else {
7252                            PerlValue::UNDEF
7253                        };
7254                        match decl.sigil {
7255                            Sigil::Typeglob => {
7256                                return Err(PerlError::runtime(
7257                                    "`my *FH` / typeglob declaration is not supported",
7258                                    stmt.line,
7259                                )
7260                                .into());
7261                            }
7262                            Sigil::Scalar => {
7263                                let skey = if is_our {
7264                                    self.stash_scalar_name_for_package(&decl.name)
7265                                } else {
7266                                    decl.name.clone()
7267                                };
7268                                self.scope.declare_scalar_frozen(
7269                                    &skey,
7270                                    val,
7271                                    decl.frozen,
7272                                    decl.type_annotation.clone(),
7273                                )?;
7274                                self.english_note_lexical_scalar(&decl.name);
7275                                if is_our {
7276                                    self.note_our_scalar(&decl.name);
7277                                }
7278                            }
7279                            Sigil::Array => {
7280                                let items = val.to_list();
7281                                if is_our {
7282                                    self.record_exporter_our_array_name(&decl.name, &items);
7283                                }
7284                                let aname = self.stash_array_name_for_package(&decl.name);
7285                                self.scope.declare_array_frozen(&aname, items, decl.frozen);
7286                            }
7287                            Sigil::Hash => {
7288                                let items = val.to_list();
7289                                let mut map = IndexMap::new();
7290                                let mut i = 0;
7291                                while i + 1 < items.len() {
7292                                    let k = items[i].to_string();
7293                                    let v = items[i + 1].clone();
7294                                    map.insert(k, v);
7295                                    i += 2;
7296                                }
7297                                self.scope.declare_hash_frozen(&decl.name, map, decl.frozen);
7298                            }
7299                        }
7300                    }
7301                }
7302                Ok(PerlValue::UNDEF)
7303            }
7304            StmtKind::State(decls) => {
7305                // `state` variables persist across subroutine calls.
7306                // Key by source line + name for uniqueness.
7307                for decl in decls {
7308                    let state_key = format!("{}:{}", stmt.line, decl.name);
7309                    match decl.sigil {
7310                        Sigil::Scalar => {
7311                            if let Some(prev) = self.state_vars.get(&state_key).cloned() {
7312                                // Already initialized — declare with persisted value
7313                                self.scope.declare_scalar(&decl.name, prev);
7314                            } else {
7315                                // First encounter — evaluate initializer
7316                                let val = if let Some(init) = &decl.initializer {
7317                                    self.eval_expr(init)?
7318                                } else {
7319                                    PerlValue::UNDEF
7320                                };
7321                                self.state_vars.insert(state_key.clone(), val.clone());
7322                                self.scope.declare_scalar(&decl.name, val);
7323                            }
7324                            // Register for save-back when scope pops
7325                            if let Some(frame) = self.state_bindings_stack.last_mut() {
7326                                frame.push((decl.name.clone(), state_key));
7327                            }
7328                        }
7329                        _ => {
7330                            // For arrays/hashes, fall back to simple my-like behavior
7331                            let val = if let Some(init) = &decl.initializer {
7332                                self.eval_expr(init)?
7333                            } else {
7334                                PerlValue::UNDEF
7335                            };
7336                            match decl.sigil {
7337                                Sigil::Array => self.scope.declare_array(&decl.name, val.to_list()),
7338                                Sigil::Hash => {
7339                                    let items = val.to_list();
7340                                    let mut map = IndexMap::new();
7341                                    let mut i = 0;
7342                                    while i + 1 < items.len() {
7343                                        map.insert(items[i].to_string(), items[i + 1].clone());
7344                                        i += 2;
7345                                    }
7346                                    self.scope.declare_hash(&decl.name, map);
7347                                }
7348                                _ => {}
7349                            }
7350                        }
7351                    }
7352                }
7353                Ok(PerlValue::UNDEF)
7354            }
7355            StmtKind::Local(decls) => {
7356                if decls.len() > 1 && decls[0].initializer.is_some() {
7357                    let val = self.eval_expr_ctx(
7358                        decls[0].initializer.as_ref().unwrap(),
7359                        WantarrayCtx::List,
7360                    )?;
7361                    let items = val.to_list();
7362                    let mut idx = 0;
7363                    for decl in decls {
7364                        match decl.sigil {
7365                            Sigil::Scalar => {
7366                                let v = items.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
7367                                idx += 1;
7368                                self.scope.local_set_scalar(&decl.name, v)?;
7369                            }
7370                            Sigil::Array => {
7371                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7372                                idx = items.len();
7373                                self.scope.local_set_array(&decl.name, rest)?;
7374                            }
7375                            Sigil::Hash => {
7376                                let rest: Vec<PerlValue> = items[idx..].to_vec();
7377                                idx = items.len();
7378                                if decl.name == "ENV" {
7379                                    self.materialize_env_if_needed();
7380                                }
7381                                let mut map = IndexMap::new();
7382                                let mut i = 0;
7383                                while i + 1 < rest.len() {
7384                                    map.insert(rest[i].to_string(), rest[i + 1].clone());
7385                                    i += 2;
7386                                }
7387                                self.scope.local_set_hash(&decl.name, map)?;
7388                            }
7389                            Sigil::Typeglob => {
7390                                return Err(PerlError::runtime(
7391                                    "list assignment to typeglob (`local (*a,*b)=...`) is not supported",
7392                                    stmt.line,
7393                                )
7394                                .into());
7395                            }
7396                        }
7397                    }
7398                    Ok(val)
7399                } else {
7400                    let mut last_val = PerlValue::UNDEF;
7401                    for decl in decls {
7402                        let val = if let Some(init) = &decl.initializer {
7403                            let ctx = match decl.sigil {
7404                                Sigil::Array | Sigil::Hash => WantarrayCtx::List,
7405                                Sigil::Scalar | Sigil::Typeglob => WantarrayCtx::Scalar,
7406                            };
7407                            self.eval_expr_ctx(init, ctx)?
7408                        } else {
7409                            PerlValue::UNDEF
7410                        };
7411                        last_val = val.clone();
7412                        match decl.sigil {
7413                            Sigil::Typeglob => {
7414                                let old = self.glob_handle_alias.remove(&decl.name);
7415                                if let Some(frame) = self.glob_restore_frames.last_mut() {
7416                                    frame.push((decl.name.clone(), old));
7417                                }
7418                                if let Some(init) = &decl.initializer {
7419                                    if let ExprKind::Typeglob(rhs) = &init.kind {
7420                                        self.glob_handle_alias
7421                                            .insert(decl.name.clone(), rhs.clone());
7422                                    } else {
7423                                        return Err(PerlError::runtime(
7424                                            "local *GLOB = *OTHER — right side must be a typeglob",
7425                                            stmt.line,
7426                                        )
7427                                        .into());
7428                                    }
7429                                }
7430                            }
7431                            Sigil::Scalar => {
7432                                // `local $X = …` on a special var (`$/`, `$\`, `$,`, `$"`, …)
7433                                // must update the interpreter's backing field too — these are
7434                                // not stored in `Scope`. Save the prior value for restoration
7435                                // on `scope_pop_hook` so the block-exit restore is visible to
7436                                // print/I/O code.
7437                                if Self::is_special_scalar_name_for_set(&decl.name) {
7438                                    let old = self.get_special_var(&decl.name);
7439                                    if let Some(frame) = self.special_var_restore_frames.last_mut()
7440                                    {
7441                                        frame.push((decl.name.clone(), old));
7442                                    }
7443                                    self.set_special_var(&decl.name, &val)
7444                                        .map_err(|e| e.at_line(stmt.line))?;
7445                                }
7446                                self.scope.local_set_scalar(&decl.name, val)?;
7447                            }
7448                            Sigil::Array => {
7449                                self.scope.local_set_array(&decl.name, val.to_list())?;
7450                            }
7451                            Sigil::Hash => {
7452                                if decl.name == "ENV" {
7453                                    self.materialize_env_if_needed();
7454                                }
7455                                let items = val.to_list();
7456                                let mut map = IndexMap::new();
7457                                let mut i = 0;
7458                                while i + 1 < items.len() {
7459                                    let k = items[i].to_string();
7460                                    let v = items[i + 1].clone();
7461                                    map.insert(k, v);
7462                                    i += 2;
7463                                }
7464                                self.scope.local_set_hash(&decl.name, map)?;
7465                            }
7466                        }
7467                    }
7468                    Ok(last_val)
7469                }
7470            }
7471            StmtKind::LocalExpr {
7472                target,
7473                initializer,
7474            } => {
7475                let rhs_name = |init: &Expr| -> PerlResult<Option<String>> {
7476                    match &init.kind {
7477                        ExprKind::Typeglob(rhs) => Ok(Some(rhs.clone())),
7478                        _ => Err(PerlError::runtime(
7479                            "local *GLOB = *OTHER — right side must be a typeglob",
7480                            stmt.line,
7481                        )),
7482                    }
7483                };
7484                match &target.kind {
7485                    ExprKind::Typeglob(name) => {
7486                        let rhs = if let Some(init) = initializer {
7487                            rhs_name(init)?
7488                        } else {
7489                            None
7490                        };
7491                        self.local_declare_typeglob(name, rhs.as_deref(), stmt.line)?;
7492                        return Ok(PerlValue::UNDEF);
7493                    }
7494                    ExprKind::Deref {
7495                        expr,
7496                        kind: Sigil::Typeglob,
7497                    } => {
7498                        let lhs = self.eval_expr(expr)?.to_string();
7499                        let rhs = if let Some(init) = initializer {
7500                            rhs_name(init)?
7501                        } else {
7502                            None
7503                        };
7504                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7505                        return Ok(PerlValue::UNDEF);
7506                    }
7507                    ExprKind::TypeglobExpr(e) => {
7508                        let lhs = self.eval_expr(e)?.to_string();
7509                        let rhs = if let Some(init) = initializer {
7510                            rhs_name(init)?
7511                        } else {
7512                            None
7513                        };
7514                        self.local_declare_typeglob(lhs.as_str(), rhs.as_deref(), stmt.line)?;
7515                        return Ok(PerlValue::UNDEF);
7516                    }
7517                    _ => {}
7518                }
7519                let val = if let Some(init) = initializer {
7520                    let ctx = match &target.kind {
7521                        ExprKind::HashVar(_) | ExprKind::ArrayVar(_) => WantarrayCtx::List,
7522                        _ => WantarrayCtx::Scalar,
7523                    };
7524                    self.eval_expr_ctx(init, ctx)?
7525                } else {
7526                    PerlValue::UNDEF
7527                };
7528                match &target.kind {
7529                    ExprKind::ScalarVar(name) => {
7530                        // `local $X = …` on a special var — see twin block in
7531                        // `StmtKind::Local` (`Sigil::Scalar`) for rationale.
7532                        if Self::is_special_scalar_name_for_set(name) {
7533                            let old = self.get_special_var(name);
7534                            if let Some(frame) = self.special_var_restore_frames.last_mut() {
7535                                frame.push((name.clone(), old));
7536                            }
7537                            self.set_special_var(name, &val)
7538                                .map_err(|e| e.at_line(stmt.line))?;
7539                        }
7540                        self.scope.local_set_scalar(name, val.clone())?;
7541                    }
7542                    ExprKind::ArrayVar(name) => {
7543                        self.scope.local_set_array(name, val.to_list())?;
7544                    }
7545                    ExprKind::HashVar(name) => {
7546                        if name == "ENV" {
7547                            self.materialize_env_if_needed();
7548                        }
7549                        let items = val.to_list();
7550                        let mut map = IndexMap::new();
7551                        let mut i = 0;
7552                        while i + 1 < items.len() {
7553                            map.insert(items[i].to_string(), items[i + 1].clone());
7554                            i += 2;
7555                        }
7556                        self.scope.local_set_hash(name, map)?;
7557                    }
7558                    ExprKind::HashElement { hash, key } => {
7559                        let ks = self.eval_expr(key)?.to_string();
7560                        self.scope.local_set_hash_element(hash, &ks, val.clone())?;
7561                    }
7562                    ExprKind::ArrayElement { array, index } => {
7563                        self.check_strict_array_var(array, stmt.line)?;
7564                        let aname = self.stash_array_name_for_package(array);
7565                        let idx = self.eval_expr(index)?.to_int();
7566                        self.scope
7567                            .local_set_array_element(&aname, idx, val.clone())?;
7568                    }
7569                    _ => {
7570                        return Err(PerlError::runtime(
7571                            format!(
7572                                "local on this lvalue is not supported yet ({:?})",
7573                                target.kind
7574                            ),
7575                            stmt.line,
7576                        )
7577                        .into());
7578                    }
7579                }
7580                Ok(val)
7581            }
7582            StmtKind::MySync(decls) => {
7583                for decl in decls {
7584                    let val = if let Some(init) = &decl.initializer {
7585                        self.eval_expr(init)?
7586                    } else {
7587                        PerlValue::UNDEF
7588                    };
7589                    match decl.sigil {
7590                        Sigil::Typeglob => {
7591                            return Err(PerlError::runtime(
7592                                "`mysync` does not support typeglob variables",
7593                                stmt.line,
7594                            )
7595                            .into());
7596                        }
7597                        Sigil::Scalar => {
7598                            // `deque()` / `heap(...)` are already `Arc<Mutex<…>>`; avoid a second
7599                            // mutex wrapper. Other scalars (including `Set->new`) use Atomic.
7600                            let stored = if val.is_mysync_deque_or_heap() {
7601                                val
7602                            } else {
7603                                PerlValue::atomic(std::sync::Arc::new(parking_lot::Mutex::new(val)))
7604                            };
7605                            self.scope.declare_scalar(&decl.name, stored);
7606                        }
7607                        Sigil::Array => {
7608                            self.scope.declare_atomic_array(&decl.name, val.to_list());
7609                        }
7610                        Sigil::Hash => {
7611                            let items = val.to_list();
7612                            let mut map = IndexMap::new();
7613                            let mut i = 0;
7614                            while i + 1 < items.len() {
7615                                map.insert(items[i].to_string(), items[i + 1].clone());
7616                                i += 2;
7617                            }
7618                            self.scope.declare_atomic_hash(&decl.name, map);
7619                        }
7620                    }
7621                }
7622                Ok(PerlValue::UNDEF)
7623            }
7624            StmtKind::Package { name } => {
7625                // Minimal package support — just set a variable
7626                let _ = self
7627                    .scope
7628                    .set_scalar("__PACKAGE__", PerlValue::string(name.clone()));
7629                Ok(PerlValue::UNDEF)
7630            }
7631            StmtKind::UsePerlVersion { .. } => Ok(PerlValue::UNDEF),
7632            StmtKind::Use { .. } => {
7633                // Handled in `prepare_program_top_level` before BEGIN / main.
7634                Ok(PerlValue::UNDEF)
7635            }
7636            StmtKind::UseOverload { pairs } => {
7637                self.install_use_overload_pairs(pairs);
7638                Ok(PerlValue::UNDEF)
7639            }
7640            StmtKind::No { .. } => {
7641                // Handled in `prepare_program_top_level` (same phase as `use`).
7642                Ok(PerlValue::UNDEF)
7643            }
7644            StmtKind::Return(val) => {
7645                let v = if let Some(e) = val {
7646                    // `return EXPR` evaluates EXPR in the caller's wantarray context so
7647                    // list-producing constructs like `1..$n`, `grep`, or `map` flatten rather
7648                    // than collapsing to a scalar flip-flop / count (`perlsyn` `return`).
7649                    self.eval_expr_ctx(e, self.wantarray_kind)?
7650                } else {
7651                    PerlValue::UNDEF
7652                };
7653                Err(Flow::Return(v).into())
7654            }
7655            StmtKind::Last(label) => Err(Flow::Last(label.clone()).into()),
7656            StmtKind::Next(label) => Err(Flow::Next(label.clone()).into()),
7657            StmtKind::Redo(label) => Err(Flow::Redo(label.clone()).into()),
7658            StmtKind::Block(block) => self.exec_block(block),
7659            StmtKind::Begin(_)
7660            | StmtKind::UnitCheck(_)
7661            | StmtKind::Check(_)
7662            | StmtKind::Init(_)
7663            | StmtKind::End(_) => Ok(PerlValue::UNDEF),
7664            StmtKind::Empty => Ok(PerlValue::UNDEF),
7665            StmtKind::Goto { target } => {
7666                // goto &sub — tail call
7667                if let ExprKind::SubroutineRef(name) = &target.kind {
7668                    return Err(Flow::GotoSub(name.clone()).into());
7669                }
7670                Err(PerlError::runtime("goto reached outside goto-aware block", stmt.line).into())
7671            }
7672            StmtKind::EvalTimeout { timeout, body } => {
7673                let secs = self.eval_expr(timeout)?.to_number();
7674                self.eval_timeout_block(body, secs, stmt.line)
7675            }
7676            StmtKind::Tie {
7677                target,
7678                class,
7679                args,
7680            } => {
7681                let kind = match &target {
7682                    TieTarget::Scalar(_) => 0u8,
7683                    TieTarget::Array(_) => 1u8,
7684                    TieTarget::Hash(_) => 2u8,
7685                };
7686                let name = match &target {
7687                    TieTarget::Scalar(s) => s.as_str(),
7688                    TieTarget::Array(a) => a.as_str(),
7689                    TieTarget::Hash(h) => h.as_str(),
7690                };
7691                let mut vals = vec![self.eval_expr(class)?];
7692                for a in args {
7693                    vals.push(self.eval_expr(a)?);
7694                }
7695                self.tie_execute(kind, name, vals, stmt.line)
7696                    .map_err(Into::into)
7697            }
7698            StmtKind::TryCatch {
7699                try_block,
7700                catch_var,
7701                catch_block,
7702                finally_block,
7703            } => match self.exec_block(try_block) {
7704                Ok(v) => {
7705                    if let Some(fb) = finally_block {
7706                        self.exec_block(fb)?;
7707                    }
7708                    Ok(v)
7709                }
7710                Err(FlowOrError::Error(e)) => {
7711                    if matches!(e.kind, ErrorKind::Exit(_)) {
7712                        return Err(FlowOrError::Error(e));
7713                    }
7714                    self.scope_push_hook();
7715                    self.scope
7716                        .declare_scalar(catch_var, PerlValue::string(e.to_string()));
7717                    self.english_note_lexical_scalar(catch_var);
7718                    let r = self.exec_block(catch_block);
7719                    self.scope_pop_hook();
7720                    if let Some(fb) = finally_block {
7721                        self.exec_block(fb)?;
7722                    }
7723                    r
7724                }
7725                Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
7726            },
7727            StmtKind::Given { topic, body } => self.exec_given(topic, body),
7728            StmtKind::When { .. } | StmtKind::DefaultCase { .. } => Err(PerlError::runtime(
7729                "when/default may only appear inside a given block",
7730                stmt.line,
7731            )
7732            .into()),
7733            StmtKind::FormatDecl { .. } => {
7734                // Registered in `prepare_program_top_level`; no per-statement runtime effect.
7735                Ok(PerlValue::UNDEF)
7736            }
7737            StmtKind::Continue(block) => self.exec_block_smart(block),
7738        }
7739    }
7740
7741    #[inline]
7742    pub(crate) fn eval_expr(&mut self, expr: &Expr) -> ExecResult {
7743        self.eval_expr_ctx(expr, WantarrayCtx::Scalar)
7744    }
7745
7746    /// Scalar `$x OP= $rhs` — single [`Scope::atomic_mutate`] so `mysync` is RMW-safe.
7747    /// For `.=`, uses [`Scope::scalar_concat_inplace`] so the LHS is not cloned via
7748    /// [`Scope::get_scalar`] and `old.to_string()` on every iteration.
7749    pub(crate) fn scalar_compound_assign_scalar_target(
7750        &mut self,
7751        name: &str,
7752        op: BinOp,
7753        rhs: PerlValue,
7754    ) -> Result<PerlValue, PerlError> {
7755        if op == BinOp::Concat {
7756            return self.scope.scalar_concat_inplace(name, &rhs);
7757        }
7758        Ok(self
7759            .scope
7760            .atomic_mutate(name, |old| Self::compound_scalar_binop(old, op, &rhs)))
7761    }
7762
7763    fn compound_scalar_binop(old: &PerlValue, op: BinOp, rhs: &PerlValue) -> PerlValue {
7764        match op {
7765            BinOp::Add => {
7766                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7767                    PerlValue::integer(a.wrapping_add(b))
7768                } else {
7769                    PerlValue::float(old.to_number() + rhs.to_number())
7770                }
7771            }
7772            BinOp::Sub => {
7773                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7774                    PerlValue::integer(a.wrapping_sub(b))
7775                } else {
7776                    PerlValue::float(old.to_number() - rhs.to_number())
7777                }
7778            }
7779            BinOp::Mul => {
7780                if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
7781                    PerlValue::integer(a.wrapping_mul(b))
7782                } else {
7783                    PerlValue::float(old.to_number() * rhs.to_number())
7784                }
7785            }
7786            BinOp::BitAnd => {
7787                if let Some(s) = crate::value::set_intersection(old, rhs) {
7788                    s
7789                } else {
7790                    PerlValue::integer(old.to_int() & rhs.to_int())
7791                }
7792            }
7793            BinOp::BitOr => {
7794                if let Some(s) = crate::value::set_union(old, rhs) {
7795                    s
7796                } else {
7797                    PerlValue::integer(old.to_int() | rhs.to_int())
7798                }
7799            }
7800            BinOp::BitXor => PerlValue::integer(old.to_int() ^ rhs.to_int()),
7801            BinOp::ShiftLeft => PerlValue::integer(old.to_int() << rhs.to_int()),
7802            BinOp::ShiftRight => PerlValue::integer(old.to_int() >> rhs.to_int()),
7803            BinOp::Div => PerlValue::float(old.to_number() / rhs.to_number()),
7804            BinOp::Mod => PerlValue::float(old.to_number() % rhs.to_number()),
7805            BinOp::Pow => PerlValue::float(old.to_number().powf(rhs.to_number())),
7806            BinOp::LogOr => {
7807                if old.is_true() {
7808                    old.clone()
7809                } else {
7810                    rhs.clone()
7811                }
7812            }
7813            BinOp::DefinedOr => {
7814                if !old.is_undef() {
7815                    old.clone()
7816                } else {
7817                    rhs.clone()
7818                }
7819            }
7820            BinOp::LogAnd => {
7821                if old.is_true() {
7822                    rhs.clone()
7823                } else {
7824                    old.clone()
7825                }
7826            }
7827            _ => PerlValue::float(old.to_number() + rhs.to_number()),
7828        }
7829    }
7830
7831    /// One `{ ... }` entry in `@h{k1,k2}` may expand to several keys (`qw/a b/` → two keys,
7832    /// `'a'..'c'` → three keys). Hash-slice subscripts are evaluated in list context so that
7833    /// `..` expands via [`crate::value::perl_list_range_expand`] rather than flip-flopping.
7834    fn eval_hash_slice_key_components(
7835        &mut self,
7836        key_expr: &Expr,
7837    ) -> Result<Vec<String>, FlowOrError> {
7838        let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
7839            self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
7840        } else {
7841            self.eval_expr(key_expr)?
7842        };
7843        if let Some(vv) = v.as_array_vec() {
7844            Ok(vv.iter().map(|x| x.to_string()).collect())
7845        } else {
7846            Ok(vec![v.to_string()])
7847        }
7848    }
7849
7850    /// Symbolic ref deref (`$$r`, `@{...}`, `%{...}`, `*{...}`) — shared by [`Self::eval_expr_ctx`] and the VM.
7851    pub(crate) fn symbolic_deref(
7852        &mut self,
7853        val: PerlValue,
7854        kind: Sigil,
7855        line: usize,
7856    ) -> ExecResult {
7857        match kind {
7858            Sigil::Scalar => {
7859                if let Some(name) = val.as_scalar_binding_name() {
7860                    return Ok(self.get_special_var(&name));
7861                }
7862                if let Some(r) = val.as_scalar_ref() {
7863                    return Ok(r.read().clone());
7864                }
7865                // `${$cref}` / `$$href{k}` outer deref — array or hash ref (incl. binding refs).
7866                if let Some(r) = val.as_array_ref() {
7867                    return Ok(PerlValue::array(r.read().clone()));
7868                }
7869                if let Some(name) = val.as_array_binding_name() {
7870                    return Ok(PerlValue::array(self.scope.get_array(&name)));
7871                }
7872                if let Some(r) = val.as_hash_ref() {
7873                    return Ok(PerlValue::hash(r.read().clone()));
7874                }
7875                if let Some(name) = val.as_hash_binding_name() {
7876                    self.touch_env_hash(&name);
7877                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
7878                }
7879                if let Some(s) = val.as_str() {
7880                    if self.strict_refs {
7881                        return Err(PerlError::runtime(
7882                            format!(
7883                                "Can't use string (\"{}\") as a SCALAR ref while \"strict refs\" in use",
7884                                s
7885                            ),
7886                            line,
7887                        )
7888                        .into());
7889                    }
7890                    return Ok(self.get_special_var(&s));
7891                }
7892                Err(PerlError::runtime("Can't dereference non-reference as scalar", line).into())
7893            }
7894            Sigil::Array => {
7895                if let Some(r) = val.as_array_ref() {
7896                    return Ok(PerlValue::array(r.read().clone()));
7897                }
7898                if let Some(name) = val.as_array_binding_name() {
7899                    return Ok(PerlValue::array(self.scope.get_array(&name)));
7900                }
7901                if let Some(s) = val.as_str() {
7902                    if self.strict_refs {
7903                        return Err(PerlError::runtime(
7904                            format!(
7905                                "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
7906                                s
7907                            ),
7908                            line,
7909                        )
7910                        .into());
7911                    }
7912                    return Ok(PerlValue::array(self.scope.get_array(&s)));
7913                }
7914                Err(PerlError::runtime("Can't dereference non-reference as array", line).into())
7915            }
7916            Sigil::Hash => {
7917                if let Some(r) = val.as_hash_ref() {
7918                    return Ok(PerlValue::hash(r.read().clone()));
7919                }
7920                if let Some(name) = val.as_hash_binding_name() {
7921                    self.touch_env_hash(&name);
7922                    return Ok(PerlValue::hash(self.scope.get_hash(&name)));
7923                }
7924                if let Some(s) = val.as_str() {
7925                    if self.strict_refs {
7926                        return Err(PerlError::runtime(
7927                            format!(
7928                                "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
7929                                s
7930                            ),
7931                            line,
7932                        )
7933                        .into());
7934                    }
7935                    self.touch_env_hash(&s);
7936                    return Ok(PerlValue::hash(self.scope.get_hash(&s)));
7937                }
7938                Err(PerlError::runtime("Can't dereference non-reference as hash", line).into())
7939            }
7940            Sigil::Typeglob => {
7941                if let Some(s) = val.as_str() {
7942                    return Ok(PerlValue::string(self.resolve_io_handle_name(&s)));
7943                }
7944                Err(PerlError::runtime("Can't dereference non-reference as typeglob", line).into())
7945            }
7946        }
7947    }
7948
7949    /// `qq` list join expects a plain array; if a bare [`PerlValue::array_ref`] reaches join, peel
7950    /// one level so elements stringify like Perl (`"@$r"`).
7951    #[inline]
7952    pub(crate) fn peel_array_ref_for_list_join(&self, v: PerlValue) -> PerlValue {
7953        if let Some(r) = v.as_array_ref() {
7954            return PerlValue::array(r.read().clone());
7955        }
7956        v
7957    }
7958
7959    /// `\@{EXPR}` / alias of an existing array ref — shared by [`crate::bytecode::Op::MakeArrayRefAlias`].
7960    pub(crate) fn make_array_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
7961        if let Some(a) = val.as_array_ref() {
7962            return Ok(PerlValue::array_ref(Arc::clone(&a)));
7963        }
7964        if let Some(name) = val.as_array_binding_name() {
7965            return Ok(PerlValue::array_binding_ref(name));
7966        }
7967        if let Some(s) = val.as_str() {
7968            if self.strict_refs {
7969                return Err(PerlError::runtime(
7970                    format!(
7971                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
7972                        s
7973                    ),
7974                    line,
7975                )
7976                .into());
7977            }
7978            return Ok(PerlValue::array_binding_ref(s.to_string()));
7979        }
7980        if let Some(r) = val.as_scalar_ref() {
7981            let inner = r.read().clone();
7982            return self.make_array_ref_alias(inner, line);
7983        }
7984        Err(PerlError::runtime("Can't make array reference from value", line).into())
7985    }
7986
7987    /// `\%{EXPR}` — shared by [`crate::bytecode::Op::MakeHashRefAlias`].
7988    pub(crate) fn make_hash_ref_alias(&self, val: PerlValue, line: usize) -> ExecResult {
7989        if let Some(h) = val.as_hash_ref() {
7990            return Ok(PerlValue::hash_ref(Arc::clone(&h)));
7991        }
7992        if let Some(name) = val.as_hash_binding_name() {
7993            return Ok(PerlValue::hash_binding_ref(name));
7994        }
7995        if let Some(s) = val.as_str() {
7996            if self.strict_refs {
7997                return Err(PerlError::runtime(
7998                    format!(
7999                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
8000                        s
8001                    ),
8002                    line,
8003                )
8004                .into());
8005            }
8006            return Ok(PerlValue::hash_binding_ref(s.to_string()));
8007        }
8008        if let Some(r) = val.as_scalar_ref() {
8009            let inner = r.read().clone();
8010            return self.make_hash_ref_alias(inner, line);
8011        }
8012        Err(PerlError::runtime("Can't make hash reference from value", line).into())
8013    }
8014
8015    /// Process Perl case escapes: \U (uppercase), \L (lowercase), \u (ucfirst),
8016    /// \l (lcfirst), \Q (quotemeta), \E (end modifier).
8017    pub(crate) fn process_case_escapes(s: &str) -> String {
8018        // Quick check: if no backslash, nothing to do
8019        if !s.contains('\\') {
8020            return s.to_string();
8021        }
8022        let mut result = String::with_capacity(s.len());
8023        let mut chars = s.chars().peekable();
8024        let mut mode: Option<char> = None; // 'U', 'L', or 'Q'
8025        let mut next_char_mod: Option<char> = None; // 'u' or 'l'
8026
8027        while let Some(c) = chars.next() {
8028            if c == '\\' {
8029                match chars.peek() {
8030                    Some(&'U') => {
8031                        chars.next();
8032                        mode = Some('U');
8033                        continue;
8034                    }
8035                    Some(&'L') => {
8036                        chars.next();
8037                        mode = Some('L');
8038                        continue;
8039                    }
8040                    Some(&'Q') => {
8041                        chars.next();
8042                        mode = Some('Q');
8043                        continue;
8044                    }
8045                    Some(&'E') => {
8046                        chars.next();
8047                        mode = None;
8048                        next_char_mod = None;
8049                        continue;
8050                    }
8051                    Some(&'u') => {
8052                        chars.next();
8053                        next_char_mod = Some('u');
8054                        continue;
8055                    }
8056                    Some(&'l') => {
8057                        chars.next();
8058                        next_char_mod = Some('l');
8059                        continue;
8060                    }
8061                    _ => {}
8062                }
8063            }
8064
8065            let ch = c;
8066
8067            // One-shot modifier (`\u` / `\l`) overrides the ongoing mode for this character.
8068            if let Some(m) = next_char_mod.take() {
8069                let transformed = match m {
8070                    'u' => ch.to_uppercase().next().unwrap_or(ch),
8071                    'l' => ch.to_lowercase().next().unwrap_or(ch),
8072                    _ => ch,
8073                };
8074                result.push(transformed);
8075            } else {
8076                // Apply ongoing mode
8077                match mode {
8078                    Some('U') => {
8079                        for uc in ch.to_uppercase() {
8080                            result.push(uc);
8081                        }
8082                    }
8083                    Some('L') => {
8084                        for lc in ch.to_lowercase() {
8085                            result.push(lc);
8086                        }
8087                    }
8088                    Some('Q') => {
8089                        if !ch.is_ascii_alphanumeric() && ch != '_' {
8090                            result.push('\\');
8091                        }
8092                        result.push(ch);
8093                    }
8094                    None | Some(_) => {
8095                        result.push(ch);
8096                    }
8097                }
8098            }
8099        }
8100        result
8101    }
8102
8103    pub(crate) fn eval_expr_ctx(&mut self, expr: &Expr, ctx: WantarrayCtx) -> ExecResult {
8104        let line = expr.line;
8105        match &expr.kind {
8106            ExprKind::Integer(n) => Ok(PerlValue::integer(*n)),
8107            ExprKind::Float(f) => Ok(PerlValue::float(*f)),
8108            ExprKind::String(s) => {
8109                let processed = Self::process_case_escapes(s);
8110                Ok(PerlValue::string(processed))
8111            }
8112            ExprKind::Bareword(s) => {
8113                if s == "__PACKAGE__" {
8114                    return Ok(PerlValue::string(self.current_package()));
8115                }
8116                if let Some(sub) = self.resolve_sub_by_name(s) {
8117                    return self.call_sub(&sub, vec![], ctx, line);
8118                }
8119                // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
8120                if let Some(r) = crate::builtins::try_builtin(self, s, &[], line) {
8121                    return r.map_err(Into::into);
8122                }
8123                Ok(PerlValue::string(s.clone()))
8124            }
8125            ExprKind::Undef => Ok(PerlValue::UNDEF),
8126            ExprKind::MagicConst(MagicConstKind::File) => Ok(PerlValue::string(self.file.clone())),
8127            ExprKind::MagicConst(MagicConstKind::Line) => Ok(PerlValue::integer(expr.line as i64)),
8128            ExprKind::MagicConst(MagicConstKind::Sub) => {
8129                if let Some(sub) = self.current_sub_stack.last().cloned() {
8130                    Ok(PerlValue::code_ref(sub))
8131                } else {
8132                    Ok(PerlValue::UNDEF)
8133                }
8134            }
8135            ExprKind::Regex(pattern, flags) => {
8136                if ctx == WantarrayCtx::Void {
8137                    // Expression statement: bare `/pat/;` is `$_ =~ /pat/` (Perl), not a regex object.
8138                    let topic = self.scope.get_scalar("_");
8139                    let s = topic.to_string();
8140                    self.regex_match_execute(s, pattern, flags, false, "_", line)
8141                } else {
8142                    let re = self.compile_regex(pattern, flags, line)?;
8143                    Ok(PerlValue::regex(re, pattern.clone(), flags.clone()))
8144                }
8145            }
8146            ExprKind::QW(words) => Ok(PerlValue::array(
8147                words.iter().map(|w| PerlValue::string(w.clone())).collect(),
8148            )),
8149
8150            // Interpolated strings
8151            ExprKind::InterpolatedString(parts) => {
8152                let mut raw_result = String::new();
8153                for part in parts {
8154                    match part {
8155                        StringPart::Literal(s) => raw_result.push_str(s),
8156                        StringPart::ScalarVar(name) => {
8157                            self.check_strict_scalar_var(name, line)?;
8158                            let val = self.get_special_var(name);
8159                            let s = self.stringify_value(val, line)?;
8160                            raw_result.push_str(&s);
8161                        }
8162                        StringPart::ArrayVar(name) => {
8163                            self.check_strict_array_var(name, line)?;
8164                            let aname = self.stash_array_name_for_package(name);
8165                            let arr = self.scope.get_array(&aname);
8166                            let mut parts = Vec::with_capacity(arr.len());
8167                            for v in &arr {
8168                                parts.push(self.stringify_value(v.clone(), line)?);
8169                            }
8170                            let sep = self.list_separator.clone();
8171                            raw_result.push_str(&parts.join(&sep));
8172                        }
8173                        StringPart::Expr(e) => {
8174                            if let ExprKind::ArraySlice { array, .. } = &e.kind {
8175                                self.check_strict_array_var(array, line)?;
8176                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8177                                let val = self.peel_array_ref_for_list_join(val);
8178                                let list = val.to_list();
8179                                let sep = self.list_separator.clone();
8180                                let mut parts = Vec::with_capacity(list.len());
8181                                for v in list {
8182                                    parts.push(self.stringify_value(v, line)?);
8183                                }
8184                                raw_result.push_str(&parts.join(&sep));
8185                            } else if let ExprKind::Deref {
8186                                kind: Sigil::Array, ..
8187                            } = &e.kind
8188                            {
8189                                let val = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8190                                let val = self.peel_array_ref_for_list_join(val);
8191                                let list = val.to_list();
8192                                let sep = self.list_separator.clone();
8193                                let mut parts = Vec::with_capacity(list.len());
8194                                for v in list {
8195                                    parts.push(self.stringify_value(v, line)?);
8196                                }
8197                                raw_result.push_str(&parts.join(&sep));
8198                            } else {
8199                                let val = self.eval_expr(e)?;
8200                                let s = self.stringify_value(val, line)?;
8201                                raw_result.push_str(&s);
8202                            }
8203                        }
8204                    }
8205                }
8206                let result = Self::process_case_escapes(&raw_result);
8207                Ok(PerlValue::string(result))
8208            }
8209
8210            // Variables
8211            ExprKind::ScalarVar(name) => {
8212                self.check_strict_scalar_var(name, line)?;
8213                let stor = self.tree_scalar_storage_name(name);
8214                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
8215                    let class = obj
8216                        .as_blessed_ref()
8217                        .map(|b| b.class.clone())
8218                        .unwrap_or_default();
8219                    let full = format!("{}::FETCH", class);
8220                    if let Some(sub) = self.subs.get(&full).cloned() {
8221                        return self.call_sub(&sub, vec![obj], ctx, line);
8222                    }
8223                }
8224                Ok(self.get_special_var(&stor))
8225            }
8226            ExprKind::ArrayVar(name) => {
8227                self.check_strict_array_var(name, line)?;
8228                let aname = self.stash_array_name_for_package(name);
8229                let arr = self.scope.get_array(&aname);
8230                if ctx == WantarrayCtx::List {
8231                    Ok(PerlValue::array(arr))
8232                } else {
8233                    Ok(PerlValue::integer(arr.len() as i64))
8234                }
8235            }
8236            ExprKind::HashVar(name) => {
8237                self.check_strict_hash_var(name, line)?;
8238                self.touch_env_hash(name);
8239                let h = self.scope.get_hash(name);
8240                let pv = PerlValue::hash(h);
8241                if ctx == WantarrayCtx::List {
8242                    Ok(pv)
8243                } else {
8244                    Ok(pv.scalar_context())
8245                }
8246            }
8247            ExprKind::Typeglob(name) => {
8248                let n = self.resolve_io_handle_name(name);
8249                Ok(PerlValue::string(n))
8250            }
8251            ExprKind::TypeglobExpr(e) => {
8252                let name = self.eval_expr(e)?.to_string();
8253                let n = self.resolve_io_handle_name(&name);
8254                Ok(PerlValue::string(n))
8255            }
8256            ExprKind::ArrayElement { array, index } => {
8257                self.check_strict_array_var(array, line)?;
8258                let idx = self.eval_expr(index)?.to_int();
8259                let aname = self.stash_array_name_for_package(array);
8260                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
8261                    let class = obj
8262                        .as_blessed_ref()
8263                        .map(|b| b.class.clone())
8264                        .unwrap_or_default();
8265                    let full = format!("{}::FETCH", class);
8266                    if let Some(sub) = self.subs.get(&full).cloned() {
8267                        let arg_vals = vec![obj, PerlValue::integer(idx)];
8268                        return self.call_sub(&sub, arg_vals, ctx, line);
8269                    }
8270                }
8271                Ok(self.scope.get_array_element(&aname, idx))
8272            }
8273            ExprKind::HashElement { hash, key } => {
8274                self.check_strict_hash_var(hash, line)?;
8275                let k = self.eval_expr(key)?.to_string();
8276                self.touch_env_hash(hash);
8277                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
8278                    let class = obj
8279                        .as_blessed_ref()
8280                        .map(|b| b.class.clone())
8281                        .unwrap_or_default();
8282                    let full = format!("{}::FETCH", class);
8283                    if let Some(sub) = self.subs.get(&full).cloned() {
8284                        let arg_vals = vec![obj, PerlValue::string(k)];
8285                        return self.call_sub(&sub, arg_vals, ctx, line);
8286                    }
8287                }
8288                Ok(self.scope.get_hash_element(hash, &k))
8289            }
8290            ExprKind::ArraySlice { array, indices } => {
8291                self.check_strict_array_var(array, line)?;
8292                let aname = self.stash_array_name_for_package(array);
8293                let flat = self.flatten_array_slice_index_specs(indices)?;
8294                let mut result = Vec::with_capacity(flat.len());
8295                for idx in flat {
8296                    result.push(self.scope.get_array_element(&aname, idx));
8297                }
8298                Ok(PerlValue::array(result))
8299            }
8300            ExprKind::HashSlice { hash, keys } => {
8301                self.check_strict_hash_var(hash, line)?;
8302                self.touch_env_hash(hash);
8303                let mut result = Vec::new();
8304                for key_expr in keys {
8305                    for k in self.eval_hash_slice_key_components(key_expr)? {
8306                        result.push(self.scope.get_hash_element(hash, &k));
8307                    }
8308                }
8309                Ok(PerlValue::array(result))
8310            }
8311            ExprKind::HashSliceDeref { container, keys } => {
8312                let hv = self.eval_expr(container)?;
8313                let mut key_vals = Vec::with_capacity(keys.len());
8314                for key_expr in keys {
8315                    let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
8316                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
8317                    } else {
8318                        self.eval_expr(key_expr)?
8319                    };
8320                    key_vals.push(v);
8321                }
8322                self.hash_slice_deref_values(&hv, &key_vals, line)
8323            }
8324            ExprKind::AnonymousListSlice { source, indices } => {
8325                let list_val = self.eval_expr_ctx(source, WantarrayCtx::List)?;
8326                let items = list_val.to_list();
8327                let flat = self.flatten_array_slice_index_specs(indices)?;
8328                let mut out = Vec::with_capacity(flat.len());
8329                for idx in flat {
8330                    let i = if idx < 0 {
8331                        (items.len() as i64 + idx) as usize
8332                    } else {
8333                        idx as usize
8334                    };
8335                    out.push(items.get(i).cloned().unwrap_or(PerlValue::UNDEF));
8336                }
8337                let arr = PerlValue::array(out);
8338                if ctx != WantarrayCtx::List {
8339                    let v = arr.to_list();
8340                    Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF))
8341                } else {
8342                    Ok(arr)
8343                }
8344            }
8345
8346            // References
8347            ExprKind::ScalarRef(inner) => match &inner.kind {
8348                ExprKind::ScalarVar(name) => Ok(PerlValue::scalar_binding_ref(name.clone())),
8349                ExprKind::ArrayVar(name) => {
8350                    self.check_strict_array_var(name, line)?;
8351                    let aname = self.stash_array_name_for_package(name);
8352                    Ok(PerlValue::array_binding_ref(aname))
8353                }
8354                ExprKind::HashVar(name) => {
8355                    self.check_strict_hash_var(name, line)?;
8356                    Ok(PerlValue::hash_binding_ref(name.clone()))
8357                }
8358                ExprKind::Deref {
8359                    expr: e,
8360                    kind: Sigil::Array,
8361                } => {
8362                    let v = self.eval_expr(e)?;
8363                    self.make_array_ref_alias(v, line)
8364                }
8365                ExprKind::Deref {
8366                    expr: e,
8367                    kind: Sigil::Hash,
8368                } => {
8369                    let v = self.eval_expr(e)?;
8370                    self.make_hash_ref_alias(v, line)
8371                }
8372                ExprKind::ArraySlice { .. } | ExprKind::HashSlice { .. } => {
8373                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8374                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8375                }
8376                ExprKind::HashSliceDeref { .. } => {
8377                    let list = self.eval_expr_ctx(inner, WantarrayCtx::List)?;
8378                    Ok(PerlValue::array_ref(Arc::new(RwLock::new(list.to_list()))))
8379                }
8380                _ => {
8381                    let val = self.eval_expr(inner)?;
8382                    Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8383                }
8384            },
8385            ExprKind::ArrayRef(elems) => {
8386                // `[ LIST ]` is list context so `1..5`, `reverse`, `grep`, `map`, and array
8387                // variables flatten into the ref rather than collapsing to a scalar count /
8388                // flip-flop value.
8389                let mut arr = Vec::with_capacity(elems.len());
8390                for e in elems {
8391                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
8392                    if let Some(vec) = v.as_array_vec() {
8393                        arr.extend(vec);
8394                    } else {
8395                        arr.push(v);
8396                    }
8397                }
8398                Ok(PerlValue::array_ref(Arc::new(RwLock::new(arr))))
8399            }
8400            ExprKind::HashRef(pairs) => {
8401                // `{ KEY => VAL, ... }` — keys are scalar-context, but values are list-context
8402                // so `{ a => [1..3] }` and `{ key => grep/sort/... }` flatten through.
8403                let mut map = IndexMap::new();
8404                for (k, v) in pairs {
8405                    let key_str = self.eval_expr(k)?.to_string();
8406                    if key_str == "__HASH_SPREAD__" {
8407                        // Hash spread: `{ %hash }` — flatten hash into key-value pairs
8408                        let spread = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8409                        let items = spread.to_list();
8410                        let mut i = 0;
8411                        while i + 1 < items.len() {
8412                            map.insert(items[i].to_string(), items[i + 1].clone());
8413                            i += 2;
8414                        }
8415                    } else {
8416                        let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
8417                        map.insert(key_str, val);
8418                    }
8419                }
8420                Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map))))
8421            }
8422            ExprKind::CodeRef { params, body } => {
8423                let captured = self.scope.capture();
8424                Ok(PerlValue::code_ref(Arc::new(PerlSub {
8425                    name: "__ANON__".to_string(),
8426                    params: params.clone(),
8427                    body: body.clone(),
8428                    closure_env: Some(captured),
8429                    prototype: None,
8430                    fib_like: None,
8431                })))
8432            }
8433            ExprKind::SubroutineRef(name) => self.call_named_sub(name, vec![], line, ctx),
8434            ExprKind::SubroutineCodeRef(name) => {
8435                let sub = self.resolve_sub_by_name(name).ok_or_else(|| {
8436                    PerlError::runtime(self.undefined_subroutine_resolve_message(name), line)
8437                })?;
8438                Ok(PerlValue::code_ref(sub))
8439            }
8440            ExprKind::DynamicSubCodeRef(expr) => {
8441                let name = self.eval_expr(expr)?.to_string();
8442                let sub = self.resolve_sub_by_name(&name).ok_or_else(|| {
8443                    PerlError::runtime(self.undefined_subroutine_resolve_message(&name), line)
8444                })?;
8445                Ok(PerlValue::code_ref(sub))
8446            }
8447            ExprKind::Deref { expr, kind } => {
8448                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Array) {
8449                    let val = self.eval_expr(expr)?;
8450                    let n = self.array_deref_len(val, line)?;
8451                    return Ok(PerlValue::integer(n));
8452                }
8453                if ctx != WantarrayCtx::List && matches!(kind, Sigil::Hash) {
8454                    let val = self.eval_expr(expr)?;
8455                    let h = self.symbolic_deref(val, Sigil::Hash, line)?;
8456                    return Ok(h.scalar_context());
8457                }
8458                let val = self.eval_expr(expr)?;
8459                self.symbolic_deref(val, *kind, line)
8460            }
8461            ExprKind::ArrowDeref { expr, index, kind } => {
8462                match kind {
8463                    DerefKind::Array => {
8464                        let container = self.eval_arrow_array_base(expr, line)?;
8465                        if let ExprKind::List(indices) = &index.kind {
8466                            let mut out = Vec::with_capacity(indices.len());
8467                            for ix in indices {
8468                                let idx = self.eval_expr(ix)?.to_int();
8469                                out.push(self.read_arrow_array_element(
8470                                    container.clone(),
8471                                    idx,
8472                                    line,
8473                                )?);
8474                            }
8475                            let arr = PerlValue::array(out);
8476                            if ctx != WantarrayCtx::List {
8477                                let v = arr.to_list();
8478                                return Ok(v.last().cloned().unwrap_or(PerlValue::UNDEF));
8479                            }
8480                            return Ok(arr);
8481                        }
8482                        let idx = self.eval_expr(index)?.to_int();
8483                        self.read_arrow_array_element(container, idx, line)
8484                    }
8485                    DerefKind::Hash => {
8486                        let val = self.eval_arrow_hash_base(expr, line)?;
8487                        let key = self.eval_expr(index)?.to_string();
8488                        self.read_arrow_hash_element(val, key.as_str(), line)
8489                    }
8490                    DerefKind::Call => {
8491                        // $coderef->(args)
8492                        let val = self.eval_expr(expr)?;
8493                        if let ExprKind::List(ref arg_exprs) = index.kind {
8494                            let mut args = Vec::new();
8495                            for a in arg_exprs {
8496                                args.push(self.eval_expr(a)?);
8497                            }
8498                            if let Some(sub) = val.as_code_ref() {
8499                                return self.call_sub(&sub, args, ctx, line);
8500                            }
8501                            Err(PerlError::runtime("Not a code reference", line).into())
8502                        } else {
8503                            Err(PerlError::runtime("Invalid call deref", line).into())
8504                        }
8505                    }
8506                }
8507            }
8508
8509            // Binary operators
8510            ExprKind::BinOp { left, op, right } => {
8511                // Short-circuit ops: bare `/.../` in boolean context is `$_ =~`, not a regex object.
8512                match op {
8513                    BinOp::BindMatch => {
8514                        let lv = self.eval_expr(left)?;
8515                        let rv = self.eval_expr(right)?;
8516                        let s = lv.to_string();
8517                        let pat = rv.to_string();
8518                        return self.regex_match_execute(s, &pat, "", false, "_", line);
8519                    }
8520                    BinOp::BindNotMatch => {
8521                        let lv = self.eval_expr(left)?;
8522                        let rv = self.eval_expr(right)?;
8523                        let s = lv.to_string();
8524                        let pat = rv.to_string();
8525                        let m = self.regex_match_execute(s, &pat, "", false, "_", line)?;
8526                        return Ok(PerlValue::integer(if m.is_true() { 0 } else { 1 }));
8527                    }
8528                    BinOp::LogAnd | BinOp::LogAndWord => {
8529                        match &left.kind {
8530                            ExprKind::Regex(_, _) => {
8531                                if !self.eval_boolean_rvalue_condition(left)? {
8532                                    return Ok(PerlValue::string(String::new()));
8533                                }
8534                            }
8535                            _ => {
8536                                let lv = self.eval_expr(left)?;
8537                                if !lv.is_true() {
8538                                    return Ok(lv);
8539                                }
8540                            }
8541                        }
8542                        return match &right.kind {
8543                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8544                                if self.eval_boolean_rvalue_condition(right)? {
8545                                    1
8546                                } else {
8547                                    0
8548                                },
8549                            )),
8550                            _ => self.eval_expr(right),
8551                        };
8552                    }
8553                    BinOp::LogOr | BinOp::LogOrWord => {
8554                        match &left.kind {
8555                            ExprKind::Regex(_, _) => {
8556                                if self.eval_boolean_rvalue_condition(left)? {
8557                                    return Ok(PerlValue::integer(1));
8558                                }
8559                            }
8560                            _ => {
8561                                let lv = self.eval_expr(left)?;
8562                                if lv.is_true() {
8563                                    return Ok(lv);
8564                                }
8565                            }
8566                        }
8567                        return match &right.kind {
8568                            ExprKind::Regex(_, _) => Ok(PerlValue::integer(
8569                                if self.eval_boolean_rvalue_condition(right)? {
8570                                    1
8571                                } else {
8572                                    0
8573                                },
8574                            )),
8575                            _ => self.eval_expr(right),
8576                        };
8577                    }
8578                    BinOp::DefinedOr => {
8579                        let lv = self.eval_expr(left)?;
8580                        if !lv.is_undef() {
8581                            return Ok(lv);
8582                        }
8583                        return self.eval_expr(right);
8584                    }
8585                    _ => {}
8586                }
8587                let lv = self.eval_expr(left)?;
8588                let rv = self.eval_expr(right)?;
8589                if let Some(r) = self.try_overload_binop(*op, &lv, &rv, line) {
8590                    return r;
8591                }
8592                self.eval_binop(*op, &lv, &rv, line)
8593            }
8594
8595            // Unary
8596            ExprKind::UnaryOp { op, expr } => match op {
8597                UnaryOp::PreIncrement => {
8598                    if let ExprKind::ScalarVar(name) = &expr.kind {
8599                        self.check_strict_scalar_var(name, line)?;
8600                        let n = self.english_scalar_name(name);
8601                        return Ok(self
8602                            .scope
8603                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() + 1)));
8604                    }
8605                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8606                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8607                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8608                                *kind, true, true, line,
8609                            ));
8610                        }
8611                    }
8612                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8613                        let href = self.eval_expr(container)?;
8614                        let mut key_vals = Vec::with_capacity(keys.len());
8615                        for key_expr in keys {
8616                            key_vals.push(self.eval_expr(key_expr)?);
8617                        }
8618                        return self.hash_slice_deref_inc_dec(href, key_vals, 0, line);
8619                    }
8620                    if let ExprKind::ArrowDeref {
8621                        expr: arr_expr,
8622                        index,
8623                        kind: DerefKind::Array,
8624                    } = &expr.kind
8625                    {
8626                        if let ExprKind::List(indices) = &index.kind {
8627                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8628                            let mut idxs = Vec::with_capacity(indices.len());
8629                            for ix in indices {
8630                                idxs.push(self.eval_expr(ix)?.to_int());
8631                            }
8632                            return self.arrow_array_slice_inc_dec(container, idxs, 0, line);
8633                        }
8634                    }
8635                    let val = self.eval_expr(expr)?;
8636                    let new_val = PerlValue::integer(val.to_int() + 1);
8637                    self.assign_value(expr, new_val.clone())?;
8638                    Ok(new_val)
8639                }
8640                UnaryOp::PreDecrement => {
8641                    if let ExprKind::ScalarVar(name) = &expr.kind {
8642                        self.check_strict_scalar_var(name, line)?;
8643                        let n = self.english_scalar_name(name);
8644                        return Ok(self
8645                            .scope
8646                            .atomic_mutate(n, |v| PerlValue::integer(v.to_int() - 1)));
8647                    }
8648                    if let ExprKind::Deref { kind, .. } = &expr.kind {
8649                        if matches!(kind, Sigil::Array | Sigil::Hash) {
8650                            return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8651                                *kind, true, false, line,
8652                            ));
8653                        }
8654                    }
8655                    if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8656                        let href = self.eval_expr(container)?;
8657                        let mut key_vals = Vec::with_capacity(keys.len());
8658                        for key_expr in keys {
8659                            key_vals.push(self.eval_expr(key_expr)?);
8660                        }
8661                        return self.hash_slice_deref_inc_dec(href, key_vals, 1, line);
8662                    }
8663                    if let ExprKind::ArrowDeref {
8664                        expr: arr_expr,
8665                        index,
8666                        kind: DerefKind::Array,
8667                    } = &expr.kind
8668                    {
8669                        if let ExprKind::List(indices) = &index.kind {
8670                            let container = self.eval_arrow_array_base(arr_expr, line)?;
8671                            let mut idxs = Vec::with_capacity(indices.len());
8672                            for ix in indices {
8673                                idxs.push(self.eval_expr(ix)?.to_int());
8674                            }
8675                            return self.arrow_array_slice_inc_dec(container, idxs, 1, line);
8676                        }
8677                    }
8678                    let val = self.eval_expr(expr)?;
8679                    let new_val = PerlValue::integer(val.to_int() - 1);
8680                    self.assign_value(expr, new_val.clone())?;
8681                    Ok(new_val)
8682                }
8683                _ => {
8684                    match op {
8685                        UnaryOp::LogNot | UnaryOp::LogNotWord => {
8686                            if let ExprKind::Regex(pattern, flags) = &expr.kind {
8687                                let topic = self.scope.get_scalar("_");
8688                                let rl = expr.line;
8689                                let s = topic.to_string();
8690                                let v =
8691                                    self.regex_match_execute(s, pattern, flags, false, "_", rl)?;
8692                                return Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }));
8693                            }
8694                        }
8695                        _ => {}
8696                    }
8697                    let val = self.eval_expr(expr)?;
8698                    match op {
8699                        UnaryOp::Negate => {
8700                            if let Some(r) = self.try_overload_unary_dispatch("neg", &val, line) {
8701                                return r;
8702                            }
8703                            if let Some(n) = val.as_integer() {
8704                                Ok(PerlValue::integer(-n))
8705                            } else {
8706                                Ok(PerlValue::float(-val.to_number()))
8707                            }
8708                        }
8709                        UnaryOp::LogNot => {
8710                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8711                                let pv = r?;
8712                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8713                            }
8714                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8715                        }
8716                        UnaryOp::BitNot => Ok(PerlValue::integer(!val.to_int())),
8717                        UnaryOp::LogNotWord => {
8718                            if let Some(r) = self.try_overload_unary_dispatch("bool", &val, line) {
8719                                let pv = r?;
8720                                return Ok(PerlValue::integer(if pv.is_true() { 0 } else { 1 }));
8721                            }
8722                            Ok(PerlValue::integer(if val.is_true() { 0 } else { 1 }))
8723                        }
8724                        UnaryOp::Ref => {
8725                            if let ExprKind::ScalarVar(name) = &expr.kind {
8726                                return Ok(PerlValue::scalar_binding_ref(name.clone()));
8727                            }
8728                            Ok(PerlValue::scalar_ref(Arc::new(RwLock::new(val))))
8729                        }
8730                        _ => unreachable!(),
8731                    }
8732                }
8733            },
8734
8735            ExprKind::PostfixOp { expr, op } => {
8736                // For scalar variables, use atomic_mutate_post to hold the lock
8737                // for the entire read-modify-write (critical for mysync).
8738                if let ExprKind::ScalarVar(name) = &expr.kind {
8739                    self.check_strict_scalar_var(name, line)?;
8740                    let n = self.english_scalar_name(name);
8741                    let f: fn(&PerlValue) -> PerlValue = match op {
8742                        PostfixOp::Increment => |v| PerlValue::integer(v.to_int() + 1),
8743                        PostfixOp::Decrement => |v| PerlValue::integer(v.to_int() - 1),
8744                    };
8745                    return Ok(self.scope.atomic_mutate_post(n, f));
8746                }
8747                if let ExprKind::Deref { kind, .. } = &expr.kind {
8748                    if matches!(kind, Sigil::Array | Sigil::Hash) {
8749                        let is_inc = matches!(op, PostfixOp::Increment);
8750                        return Err(Self::err_modify_symbolic_aggregate_deref_inc_dec(
8751                            *kind, false, is_inc, line,
8752                        ));
8753                    }
8754                }
8755                if let ExprKind::HashSliceDeref { container, keys } = &expr.kind {
8756                    let href = self.eval_expr(container)?;
8757                    let mut key_vals = Vec::with_capacity(keys.len());
8758                    for key_expr in keys {
8759                        key_vals.push(self.eval_expr(key_expr)?);
8760                    }
8761                    let kind_byte = match op {
8762                        PostfixOp::Increment => 2u8,
8763                        PostfixOp::Decrement => 3u8,
8764                    };
8765                    return self.hash_slice_deref_inc_dec(href, key_vals, kind_byte, line);
8766                }
8767                if let ExprKind::ArrowDeref {
8768                    expr: arr_expr,
8769                    index,
8770                    kind: DerefKind::Array,
8771                } = &expr.kind
8772                {
8773                    if let ExprKind::List(indices) = &index.kind {
8774                        let container = self.eval_arrow_array_base(arr_expr, line)?;
8775                        let mut idxs = Vec::with_capacity(indices.len());
8776                        for ix in indices {
8777                            idxs.push(self.eval_expr(ix)?.to_int());
8778                        }
8779                        let kind_byte = match op {
8780                            PostfixOp::Increment => 2u8,
8781                            PostfixOp::Decrement => 3u8,
8782                        };
8783                        return self.arrow_array_slice_inc_dec(container, idxs, kind_byte, line);
8784                    }
8785                }
8786                let val = self.eval_expr(expr)?;
8787                let old = val.clone();
8788                let new_val = match op {
8789                    PostfixOp::Increment => PerlValue::integer(val.to_int() + 1),
8790                    PostfixOp::Decrement => PerlValue::integer(val.to_int() - 1),
8791                };
8792                self.assign_value(expr, new_val)?;
8793                Ok(old)
8794            }
8795
8796            // Assignment
8797            ExprKind::Assign { target, value } => {
8798                if let ExprKind::Typeglob(lhs) = &target.kind {
8799                    if let ExprKind::Typeglob(rhs) = &value.kind {
8800                        self.copy_typeglob_slots(lhs, rhs, line)?;
8801                        return self.eval_expr(value);
8802                    }
8803                }
8804                let val = self.eval_expr_ctx(value, assign_rhs_wantarray(target))?;
8805                self.assign_value(target, val.clone())?;
8806                Ok(val)
8807            }
8808            ExprKind::CompoundAssign { target, op, value } => {
8809                // For scalar targets, use atomic_mutate to hold the lock.
8810                // `||=` / `//=` short-circuit: do not evaluate RHS if LHS is already true / defined.
8811                if let ExprKind::ScalarVar(name) = &target.kind {
8812                    self.check_strict_scalar_var(name, line)?;
8813                    let n = self.english_scalar_name(name);
8814                    let op = *op;
8815                    let rhs = match op {
8816                        BinOp::LogOr => {
8817                            let old = self.scope.get_scalar(n);
8818                            if old.is_true() {
8819                                return Ok(old);
8820                            }
8821                            self.eval_expr(value)?
8822                        }
8823                        BinOp::DefinedOr => {
8824                            let old = self.scope.get_scalar(n);
8825                            if !old.is_undef() {
8826                                return Ok(old);
8827                            }
8828                            self.eval_expr(value)?
8829                        }
8830                        BinOp::LogAnd => {
8831                            let old = self.scope.get_scalar(n);
8832                            if !old.is_true() {
8833                                return Ok(old);
8834                            }
8835                            self.eval_expr(value)?
8836                        }
8837                        _ => self.eval_expr(value)?,
8838                    };
8839                    return Ok(self.scalar_compound_assign_scalar_target(n, op, rhs)?);
8840                }
8841                let rhs = self.eval_expr(value)?;
8842                // For hash element targets: $h{key} += 1
8843                if let ExprKind::HashElement { hash, key } = &target.kind {
8844                    self.check_strict_hash_var(hash, line)?;
8845                    let k = self.eval_expr(key)?.to_string();
8846                    let op = *op;
8847                    return Ok(self.scope.atomic_hash_mutate(hash, &k, |old| match op {
8848                        BinOp::Add => {
8849                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8850                                PerlValue::integer(a.wrapping_add(b))
8851                            } else {
8852                                PerlValue::float(old.to_number() + rhs.to_number())
8853                            }
8854                        }
8855                        BinOp::Sub => {
8856                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8857                                PerlValue::integer(a.wrapping_sub(b))
8858                            } else {
8859                                PerlValue::float(old.to_number() - rhs.to_number())
8860                            }
8861                        }
8862                        BinOp::Concat => {
8863                            let mut s = old.to_string();
8864                            rhs.append_to(&mut s);
8865                            PerlValue::string(s)
8866                        }
8867                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
8868                    })?);
8869                }
8870                // For array element targets: $a[i] += 1
8871                if let ExprKind::ArrayElement { array, index } = &target.kind {
8872                    self.check_strict_array_var(array, line)?;
8873                    let idx = self.eval_expr(index)?.to_int();
8874                    let op = *op;
8875                    return Ok(self.scope.atomic_array_mutate(array, idx, |old| match op {
8876                        BinOp::Add => {
8877                            if let (Some(a), Some(b)) = (old.as_integer(), rhs.as_integer()) {
8878                                PerlValue::integer(a.wrapping_add(b))
8879                            } else {
8880                                PerlValue::float(old.to_number() + rhs.to_number())
8881                            }
8882                        }
8883                        _ => PerlValue::float(old.to_number() + rhs.to_number()),
8884                    })?);
8885                }
8886                if let ExprKind::HashSliceDeref { container, keys } = &target.kind {
8887                    let href = self.eval_expr(container)?;
8888                    let mut key_vals = Vec::with_capacity(keys.len());
8889                    for key_expr in keys {
8890                        key_vals.push(self.eval_expr(key_expr)?);
8891                    }
8892                    return self.compound_assign_hash_slice_deref(href, key_vals, *op, rhs, line);
8893                }
8894                if let ExprKind::AnonymousListSlice { source, indices } = &target.kind {
8895                    if let ExprKind::Deref {
8896                        expr: inner,
8897                        kind: Sigil::Array,
8898                    } = &source.kind
8899                    {
8900                        let container = self.eval_arrow_array_base(inner, line)?;
8901                        let idxs = self.flatten_array_slice_index_specs(indices)?;
8902                        return self
8903                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
8904                    }
8905                }
8906                if let ExprKind::ArrowDeref {
8907                    expr: arr_expr,
8908                    index,
8909                    kind: DerefKind::Array,
8910                } = &target.kind
8911                {
8912                    if let ExprKind::List(indices) = &index.kind {
8913                        let container = self.eval_arrow_array_base(arr_expr, line)?;
8914                        let mut idxs = Vec::with_capacity(indices.len());
8915                        for ix in indices {
8916                            idxs.push(self.eval_expr(ix)?.to_int());
8917                        }
8918                        return self
8919                            .compound_assign_arrow_array_slice(container, idxs, *op, rhs, line);
8920                    }
8921                }
8922                let old = self.eval_expr(target)?;
8923                let new_val = self.eval_binop(*op, &old, &rhs, line)?;
8924                self.assign_value(target, new_val.clone())?;
8925                Ok(new_val)
8926            }
8927
8928            // Ternary
8929            ExprKind::Ternary {
8930                condition,
8931                then_expr,
8932                else_expr,
8933            } => {
8934                if self.eval_boolean_rvalue_condition(condition)? {
8935                    self.eval_expr(then_expr)
8936                } else {
8937                    self.eval_expr(else_expr)
8938                }
8939            }
8940
8941            // Range
8942            ExprKind::Range {
8943                from,
8944                to,
8945                exclusive,
8946            } => {
8947                if ctx == WantarrayCtx::List {
8948                    let f = self.eval_expr(from)?;
8949                    let t = self.eval_expr(to)?;
8950                    let list = perl_list_range_expand(f, t);
8951                    Ok(PerlValue::array(list))
8952                } else {
8953                    let key = std::ptr::from_ref(expr) as usize;
8954                    match (&from.kind, &to.kind) {
8955                        (
8956                            ExprKind::Regex(left_pat, left_flags),
8957                            ExprKind::Regex(right_pat, right_flags),
8958                        ) => {
8959                            let dot = self.scalar_flipflop_dot_line();
8960                            let subject = self.scope.get_scalar("_").to_string();
8961                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
8962                                |e| match e {
8963                                    FlowOrError::Error(err) => err,
8964                                    FlowOrError::Flow(_) => PerlError::runtime(
8965                                        "unexpected flow in regex flip-flop",
8966                                        line,
8967                                    ),
8968                                },
8969                            )?;
8970                            let right_re = self
8971                                .compile_regex(right_pat, right_flags, line)
8972                                .map_err(|e| match e {
8973                                    FlowOrError::Error(err) => err,
8974                                    FlowOrError::Flow(_) => PerlError::runtime(
8975                                        "unexpected flow in regex flip-flop",
8976                                        line,
8977                                    ),
8978                                })?;
8979                            let left_m = left_re.is_match(&subject);
8980                            let right_m = right_re.is_match(&subject);
8981                            let st = self.flip_flop_tree.entry(key).or_default();
8982                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
8983                                &mut st.active,
8984                                &mut st.exclusive_left_line,
8985                                *exclusive,
8986                                dot,
8987                                left_m,
8988                                right_m,
8989                            )))
8990                        }
8991                        (ExprKind::Regex(left_pat, left_flags), ExprKind::Eof(None)) => {
8992                            let dot = self.scalar_flipflop_dot_line();
8993                            let subject = self.scope.get_scalar("_").to_string();
8994                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
8995                                |e| match e {
8996                                    FlowOrError::Error(err) => err,
8997                                    FlowOrError::Flow(_) => PerlError::runtime(
8998                                        "unexpected flow in regex/eof flip-flop",
8999                                        line,
9000                                    ),
9001                                },
9002                            )?;
9003                            let left_m = left_re.is_match(&subject);
9004                            let right_m = self.eof_without_arg_is_true();
9005                            let st = self.flip_flop_tree.entry(key).or_default();
9006                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9007                                &mut st.active,
9008                                &mut st.exclusive_left_line,
9009                                *exclusive,
9010                                dot,
9011                                left_m,
9012                                right_m,
9013                            )))
9014                        }
9015                        (
9016                            ExprKind::Regex(left_pat, left_flags),
9017                            ExprKind::Integer(_) | ExprKind::Float(_),
9018                        ) => {
9019                            let dot = self.scalar_flipflop_dot_line();
9020                            let right = self.eval_expr(to)?.to_int();
9021                            let subject = self.scope.get_scalar("_").to_string();
9022                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9023                                |e| match e {
9024                                    FlowOrError::Error(err) => err,
9025                                    FlowOrError::Flow(_) => PerlError::runtime(
9026                                        "unexpected flow in regex flip-flop",
9027                                        line,
9028                                    ),
9029                                },
9030                            )?;
9031                            let left_m = left_re.is_match(&subject);
9032                            let right_m = dot == right;
9033                            let st = self.flip_flop_tree.entry(key).or_default();
9034                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9035                                &mut st.active,
9036                                &mut st.exclusive_left_line,
9037                                *exclusive,
9038                                dot,
9039                                left_m,
9040                                right_m,
9041                            )))
9042                        }
9043                        (ExprKind::Regex(left_pat, left_flags), _) => {
9044                            if let ExprKind::Eof(Some(_)) = &to.kind {
9045                                return Err(FlowOrError::Error(PerlError::runtime(
9046                                    "regex flip-flop with eof(HANDLE) is not supported",
9047                                    line,
9048                                )));
9049                            }
9050                            let dot = self.scalar_flipflop_dot_line();
9051                            let subject = self.scope.get_scalar("_").to_string();
9052                            let left_re = self.compile_regex(left_pat, left_flags, line).map_err(
9053                                |e| match e {
9054                                    FlowOrError::Error(err) => err,
9055                                    FlowOrError::Flow(_) => PerlError::runtime(
9056                                        "unexpected flow in regex flip-flop",
9057                                        line,
9058                                    ),
9059                                },
9060                            )?;
9061                            let left_m = left_re.is_match(&subject);
9062                            let right_m = self.eval_boolean_rvalue_condition(to)?;
9063                            let st = self.flip_flop_tree.entry(key).or_default();
9064                            Ok(PerlValue::integer(Self::regex_flip_flop_transition(
9065                                &mut st.active,
9066                                &mut st.exclusive_left_line,
9067                                *exclusive,
9068                                dot,
9069                                left_m,
9070                                right_m,
9071                            )))
9072                        }
9073                        _ => {
9074                            let left = self.eval_expr(from)?.to_int();
9075                            let right = self.eval_expr(to)?.to_int();
9076                            let dot = self.scalar_flipflop_dot_line();
9077                            let st = self.flip_flop_tree.entry(key).or_default();
9078                            if !st.active {
9079                                if dot == left {
9080                                    st.active = true;
9081                                    if *exclusive {
9082                                        st.exclusive_left_line = Some(dot);
9083                                    } else {
9084                                        st.exclusive_left_line = None;
9085                                        if dot == right {
9086                                            st.active = false;
9087                                        }
9088                                    }
9089                                    return Ok(PerlValue::integer(1));
9090                                }
9091                                return Ok(PerlValue::integer(0));
9092                            }
9093                            if let Some(ll) = st.exclusive_left_line {
9094                                if dot == right && dot > ll {
9095                                    st.active = false;
9096                                    st.exclusive_left_line = None;
9097                                }
9098                            } else if dot == right {
9099                                st.active = false;
9100                            }
9101                            Ok(PerlValue::integer(1))
9102                        }
9103                    }
9104                }
9105            }
9106
9107            // Repeat
9108            ExprKind::Repeat { expr, count } => {
9109                let val = self.eval_expr(expr)?;
9110                let n = self.eval_expr(count)?.to_int().max(0) as usize;
9111                if let Some(s) = val.as_str() {
9112                    Ok(PerlValue::string(s.repeat(n)))
9113                } else if let Some(a) = val.as_array_vec() {
9114                    let mut result = Vec::with_capacity(a.len() * n);
9115                    for _ in 0..n {
9116                        result.extend(a.iter().cloned());
9117                    }
9118                    Ok(PerlValue::array(result))
9119                } else {
9120                    Ok(PerlValue::string(val.to_string().repeat(n)))
9121                }
9122            }
9123
9124            // `my $x = …` / `our` / `state` / `local` used as an expression
9125            // (e.g. `if (my $line = readline)`).  Declare each variable in the
9126            // current scope, evaluate the initializer (if any), and return the
9127            // assigned value(s).  Re-uses the same scope APIs as `StmtKind::My`.
9128            ExprKind::MyExpr { keyword, decls } => {
9129                // Build a temporary statement and dispatch to the canonical
9130                // statement handler so behavior matches `my $x = …;` exactly.
9131                let stmt_kind = match keyword.as_str() {
9132                    "my" => StmtKind::My(decls.clone()),
9133                    "our" => StmtKind::Our(decls.clone()),
9134                    "state" => StmtKind::State(decls.clone()),
9135                    "local" => StmtKind::Local(decls.clone()),
9136                    _ => StmtKind::My(decls.clone()),
9137                };
9138                let stmt = Statement {
9139                    label: None,
9140                    kind: stmt_kind,
9141                    line,
9142                };
9143                self.exec_statement(&stmt)?;
9144                // Return the value of the (first) declared variable so the
9145                // surrounding expression sees the assigned value, matching
9146                // Perl: `if (my $x = 5) { … }` evaluates the condition as 5.
9147                let first = decls.first().ok_or_else(|| {
9148                    FlowOrError::Error(PerlError::runtime("MyExpr: empty decl list", line))
9149                })?;
9150                Ok(match first.sigil {
9151                    Sigil::Scalar => self.scope.get_scalar(&first.name),
9152                    Sigil::Array => PerlValue::array(self.scope.get_array(&first.name)),
9153                    Sigil::Hash => {
9154                        let h = self.scope.get_hash(&first.name);
9155                        let mut flat: Vec<PerlValue> = Vec::with_capacity(h.len() * 2);
9156                        for (k, v) in h {
9157                            flat.push(PerlValue::string(k));
9158                            flat.push(v);
9159                        }
9160                        PerlValue::array(flat)
9161                    }
9162                    Sigil::Typeglob => PerlValue::UNDEF,
9163                })
9164            }
9165
9166            // Function calls
9167            ExprKind::FuncCall { name, args } => {
9168                // read(FH, $buf, LEN [, OFFSET]) needs special handling: $buf is an lvalue
9169                if matches!(name.as_str(), "read" | "CORE::read") && args.len() >= 3 {
9170                    let fh_val = self.eval_expr(&args[0])?;
9171                    let fh = fh_val
9172                        .as_io_handle_name()
9173                        .unwrap_or_else(|| fh_val.to_string());
9174                    let len = self.eval_expr(&args[2])?.to_int().max(0) as usize;
9175                    let offset = if args.len() > 3 {
9176                        self.eval_expr(&args[3])?.to_int().max(0) as usize
9177                    } else {
9178                        0
9179                    };
9180                    // Extract the variable name from the AST
9181                    let var_name = match &args[1].kind {
9182                        ExprKind::ScalarVar(n) => n.clone(),
9183                        _ => self.eval_expr(&args[1])?.to_string(),
9184                    };
9185                    let mut buf = vec![0u8; len];
9186                    let n = if let Some(slot) = self.io_file_slots.get(&fh).cloned() {
9187                        slot.lock().read(&mut buf).unwrap_or(0)
9188                    } else if fh == "STDIN" {
9189                        std::io::stdin().read(&mut buf).unwrap_or(0)
9190                    } else {
9191                        return Err(PerlError::runtime(
9192                            format!("read: unopened handle {}", fh),
9193                            line,
9194                        )
9195                        .into());
9196                    };
9197                    buf.truncate(n);
9198                    let read_str = crate::perl_fs::decode_utf8_or_latin1(&buf);
9199                    if offset > 0 {
9200                        let mut existing = self.scope.get_scalar(&var_name).to_string();
9201                        while existing.len() < offset {
9202                            existing.push('\0');
9203                        }
9204                        existing.push_str(&read_str);
9205                        let _ = self
9206                            .scope
9207                            .set_scalar(&var_name, PerlValue::string(existing));
9208                    } else {
9209                        let _ = self
9210                            .scope
9211                            .set_scalar(&var_name, PerlValue::string(read_str));
9212                    }
9213                    return Ok(PerlValue::integer(n as i64));
9214                }
9215                if matches!(name.as_str(), "group_by" | "chunk_by") {
9216                    if args.len() != 2 {
9217                        return Err(PerlError::runtime(
9218                            "group_by/chunk_by: expected { BLOCK } or EXPR, LIST",
9219                            line,
9220                        )
9221                        .into());
9222                    }
9223                    return self.eval_chunk_by_builtin(&args[0], &args[1], ctx, line);
9224                }
9225                if matches!(name.as_str(), "puniq" | "pfirst" | "pany") {
9226                    let mut arg_vals = Vec::with_capacity(args.len());
9227                    for a in args {
9228                        arg_vals.push(self.eval_expr(a)?);
9229                    }
9230                    let saved_wa = self.wantarray_kind;
9231                    self.wantarray_kind = ctx;
9232                    let r = self.eval_par_list_call(name.as_str(), &arg_vals, ctx, line);
9233                    self.wantarray_kind = saved_wa;
9234                    return r.map_err(Into::into);
9235                }
9236                let arg_vals = if matches!(name.as_str(), "any" | "all" | "none" | "first")
9237                    || matches!(
9238                        name.as_str(),
9239                        "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9240                    )
9241                    || matches!(
9242                        name.as_str(),
9243                        "partition" | "min_by" | "max_by" | "zip_with" | "count_by"
9244                    ) {
9245                    if args.len() != 2 {
9246                        return Err(PerlError::runtime(
9247                            format!("{}: expected BLOCK, LIST", name),
9248                            line,
9249                        )
9250                        .into());
9251                    }
9252                    let cr = self.eval_expr(&args[0])?;
9253                    let list_src = self.eval_expr_ctx(&args[1], WantarrayCtx::List)?;
9254                    let mut v = vec![cr];
9255                    v.extend(list_src.to_list());
9256                    v
9257                } else if matches!(
9258                    name.as_str(),
9259                    "zip" | "List::Util::zip" | "List::Util::zip_longest"
9260                ) {
9261                    let mut v = Vec::with_capacity(args.len());
9262                    for a in args {
9263                        v.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9264                    }
9265                    v
9266                } else if matches!(
9267                    name.as_str(),
9268                    "uniq"
9269                        | "distinct"
9270                        | "uniqstr"
9271                        | "uniqint"
9272                        | "uniqnum"
9273                        | "flatten"
9274                        | "set"
9275                        | "list_count"
9276                        | "list_size"
9277                        | "count"
9278                        | "size"
9279                        | "cnt"
9280                        | "with_index"
9281                        | "List::Util::uniq"
9282                        | "List::Util::uniqstr"
9283                        | "List::Util::uniqint"
9284                        | "List::Util::uniqnum"
9285                        | "shuffle"
9286                        | "List::Util::shuffle"
9287                        | "sum"
9288                        | "sum0"
9289                        | "product"
9290                        | "min"
9291                        | "max"
9292                        | "minstr"
9293                        | "maxstr"
9294                        | "mean"
9295                        | "median"
9296                        | "mode"
9297                        | "stddev"
9298                        | "variance"
9299                        | "List::Util::sum"
9300                        | "List::Util::sum0"
9301                        | "List::Util::product"
9302                        | "List::Util::min"
9303                        | "List::Util::max"
9304                        | "List::Util::minstr"
9305                        | "List::Util::maxstr"
9306                        | "List::Util::mean"
9307                        | "List::Util::median"
9308                        | "List::Util::mode"
9309                        | "List::Util::stddev"
9310                        | "List::Util::variance"
9311                        | "pairs"
9312                        | "unpairs"
9313                        | "pairkeys"
9314                        | "pairvalues"
9315                        | "List::Util::pairs"
9316                        | "List::Util::unpairs"
9317                        | "List::Util::pairkeys"
9318                        | "List::Util::pairvalues"
9319                ) {
9320                    // Perl prototype `(@)`: one slurpy list — either one list expr (`uniq @x`) or
9321                    // multiple actuals (`List::Util::uniq(1, 1, 2)`). Each actual is evaluated in
9322                    // list context so `@a, @b` flattens like Perl.
9323                    let mut list_out = Vec::new();
9324                    if args.len() == 1 {
9325                        list_out = self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list();
9326                    } else {
9327                        for a in args {
9328                            list_out.extend(self.eval_expr_ctx(a, WantarrayCtx::List)?.to_list());
9329                        }
9330                    }
9331                    list_out
9332                } else if matches!(
9333                    name.as_str(),
9334                    "take" | "head" | "tail" | "drop" | "List::Util::head" | "List::Util::tail"
9335                ) {
9336                    if args.is_empty() {
9337                        return Err(PerlError::runtime(
9338                            "take/head/tail/drop/List::Util::head|tail: need LIST..., N or unary N",
9339                            line,
9340                        )
9341                        .into());
9342                    }
9343                    let mut arg_vals = Vec::with_capacity(args.len());
9344                    if args.len() == 1 {
9345                        // head @l == head @l, 1 — evaluate in list context
9346                        arg_vals.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9347                    } else {
9348                        for a in &args[..args.len() - 1] {
9349                            arg_vals.push(self.eval_expr_ctx(a, WantarrayCtx::List)?);
9350                        }
9351                        arg_vals.push(self.eval_expr(&args[args.len() - 1])?);
9352                    }
9353                    arg_vals
9354                } else if matches!(
9355                    name.as_str(),
9356                    "chunked" | "List::Util::chunked" | "windowed" | "List::Util::windowed"
9357                ) {
9358                    let mut list_out = Vec::new();
9359                    match args.len() {
9360                        0 => {
9361                            return Err(PerlError::runtime(
9362                                format!("{name}: expected (LIST, N) or unary N after |>"),
9363                                line,
9364                            )
9365                            .into());
9366                        }
9367                        1 => {
9368                            // chunked @l / windowed @l — evaluate in list context, default size
9369                            list_out.push(self.eval_expr_ctx(&args[0], WantarrayCtx::List)?);
9370                        }
9371                        2 => {
9372                            list_out.extend(
9373                                self.eval_expr_ctx(&args[0], WantarrayCtx::List)?.to_list(),
9374                            );
9375                            list_out.push(self.eval_expr(&args[1])?);
9376                        }
9377                        _ => {
9378                            return Err(PerlError::runtime(
9379                                format!(
9380                                    "{name}: expected exactly (LIST, N); use one list expression then size"
9381                                ),
9382                                line,
9383                            )
9384                            .into());
9385                        }
9386                    }
9387                    list_out
9388                } else {
9389                    // Generic sub call: args are in list context so `f(1..10)`, `f(@a)`,
9390                    // `f(reverse LIST)` flatten into `@_` (matches Perl's call list semantics).
9391                    let mut arg_vals = Vec::with_capacity(args.len());
9392                    for a in args {
9393                        let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
9394                        if let Some(items) = v.as_array_vec() {
9395                            arg_vals.extend(items);
9396                        } else {
9397                            arg_vals.push(v);
9398                        }
9399                    }
9400                    arg_vals
9401                };
9402                // Builtins read [`Self::wantarray_kind`] (VM sets it too); thread `ctx` through.
9403                let saved_wa = self.wantarray_kind;
9404                self.wantarray_kind = ctx;
9405                // User-defined subs shadow builtins (correct Perl semantics).
9406                if let Some(sub) = self.resolve_sub_by_name(name) {
9407                    self.wantarray_kind = saved_wa;
9408                    let args = self.with_topic_default_args(arg_vals);
9409                    return self.call_sub(&sub, args, ctx, line);
9410                }
9411                if matches!(
9412                    name.as_str(),
9413                    "take_while" | "drop_while" | "skip_while" | "reject" | "tap" | "peek"
9414                ) {
9415                    let r = self.list_higher_order_block_builtin(name.as_str(), &arg_vals, line);
9416                    self.wantarray_kind = saved_wa;
9417                    return r.map_err(Into::into);
9418                }
9419                if let Some(r) = crate::builtins::try_builtin(self, name.as_str(), &arg_vals, line)
9420                {
9421                    self.wantarray_kind = saved_wa;
9422                    return r.map_err(Into::into);
9423                }
9424                self.wantarray_kind = saved_wa;
9425                self.call_named_sub(name, arg_vals, line, ctx)
9426            }
9427            ExprKind::IndirectCall {
9428                target,
9429                args,
9430                ampersand: _,
9431                pass_caller_arglist,
9432            } => {
9433                let tval = self.eval_expr(target)?;
9434                let arg_vals = if *pass_caller_arglist {
9435                    self.scope.get_array("_")
9436                } else {
9437                    let mut v = Vec::with_capacity(args.len());
9438                    for a in args {
9439                        v.push(self.eval_expr(a)?);
9440                    }
9441                    v
9442                };
9443                self.dispatch_indirect_call(tval, arg_vals, ctx, line)
9444            }
9445            ExprKind::MethodCall {
9446                object,
9447                method,
9448                args,
9449                super_call,
9450            } => {
9451                let obj = self.eval_expr(object)?;
9452                let mut arg_vals = vec![obj.clone()];
9453                for a in args {
9454                    arg_vals.push(self.eval_expr(a)?);
9455                }
9456                if let Some(r) =
9457                    crate::pchannel::dispatch_method(&obj, method, &arg_vals[1..], line)
9458                {
9459                    return r.map_err(Into::into);
9460                }
9461                if let Some(r) = self.try_native_method(&obj, method, &arg_vals[1..], line) {
9462                    return r.map_err(Into::into);
9463                }
9464                // Get class name
9465                let class = if let Some(b) = obj.as_blessed_ref() {
9466                    b.class.clone()
9467                } else if let Some(s) = obj.as_str() {
9468                    s // Class->method()
9469                } else {
9470                    return Err(PerlError::runtime("Can't call method on non-object", line).into());
9471                };
9472                if method == "VERSION" && !*super_call {
9473                    if let Some(ver) = self.package_version_scalar(class.as_str())? {
9474                        return Ok(ver);
9475                    }
9476                }
9477                // UNIVERSAL methods: isa, can, DOES
9478                if !*super_call {
9479                    match method.as_str() {
9480                        "isa" => {
9481                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9482                            let mro = self.mro_linearize(&class);
9483                            let result = mro.iter().any(|c| c == &target);
9484                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9485                        }
9486                        "can" => {
9487                            let target_method =
9488                                arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9489                            let found = self
9490                                .resolve_method_full_name(&class, &target_method, false)
9491                                .and_then(|fq| self.subs.get(&fq))
9492                                .is_some();
9493                            if found {
9494                                return Ok(PerlValue::code_ref(Arc::new(PerlSub {
9495                                    name: target_method,
9496                                    params: vec![],
9497                                    body: vec![],
9498                                    closure_env: None,
9499                                    prototype: None,
9500                                    fib_like: None,
9501                                })));
9502                            } else {
9503                                return Ok(PerlValue::UNDEF);
9504                            }
9505                        }
9506                        "DOES" => {
9507                            let target = arg_vals.get(1).map(|v| v.to_string()).unwrap_or_default();
9508                            let mro = self.mro_linearize(&class);
9509                            let result = mro.iter().any(|c| c == &target);
9510                            return Ok(PerlValue::integer(if result { 1 } else { 0 }));
9511                        }
9512                        _ => {}
9513                    }
9514                }
9515                let full_name = self
9516                    .resolve_method_full_name(&class, method, *super_call)
9517                    .ok_or_else(|| {
9518                        PerlError::runtime(
9519                            format!(
9520                                "Can't locate method \"{}\" for invocant \"{}\"",
9521                                method, class
9522                            ),
9523                            line,
9524                        )
9525                    })?;
9526                if let Some(sub) = self.subs.get(&full_name).cloned() {
9527                    self.call_sub(&sub, arg_vals, ctx, line)
9528                } else if method == "new" && !*super_call {
9529                    // Default constructor
9530                    self.builtin_new(&class, arg_vals, line)
9531                } else if let Some(r) =
9532                    self.try_autoload_call(&full_name, arg_vals, line, ctx, Some(&class))
9533                {
9534                    r
9535                } else {
9536                    Err(PerlError::runtime(
9537                        format!(
9538                            "Can't locate method \"{}\" in package \"{}\"",
9539                            method, class
9540                        ),
9541                        line,
9542                    )
9543                    .into())
9544                }
9545            }
9546
9547            // Print/Say/Printf
9548            ExprKind::Print { handle, args } => {
9549                self.exec_print(handle.as_deref(), args, false, line)
9550            }
9551            ExprKind::Say { handle, args } => self.exec_print(handle.as_deref(), args, true, line),
9552            ExprKind::Printf { handle, args } => self.exec_printf(handle.as_deref(), args, line),
9553            ExprKind::Die(args) => {
9554                if args.is_empty() {
9555                    // `die` with no args: re-die with current $@ or "Died"
9556                    let current = self.scope.get_scalar("@");
9557                    let msg = if current.is_undef() || current.to_string().is_empty() {
9558                        let mut m = "Died".to_string();
9559                        m.push_str(&self.die_warn_at_suffix(line));
9560                        m.push('\n');
9561                        m
9562                    } else {
9563                        current.to_string()
9564                    };
9565                    return Err(PerlError::die(msg, line).into());
9566                }
9567                // Single ref argument: store the ref value in $@
9568                if args.len() == 1 {
9569                    let v = self.eval_expr(&args[0])?;
9570                    if v.as_hash_ref().is_some()
9571                        || v.as_blessed_ref().is_some()
9572                        || v.as_array_ref().is_some()
9573                        || v.as_code_ref().is_some()
9574                    {
9575                        let msg = v.to_string();
9576                        return Err(PerlError::die_with_value(v, msg, line).into());
9577                    }
9578                }
9579                let mut msg = String::new();
9580                for a in args {
9581                    let v = self.eval_expr(a)?;
9582                    msg.push_str(&v.to_string());
9583                }
9584                if msg.is_empty() {
9585                    msg = "Died".to_string();
9586                }
9587                if !msg.ends_with('\n') {
9588                    msg.push_str(&self.die_warn_at_suffix(line));
9589                    msg.push('\n');
9590                }
9591                Err(PerlError::die(msg, line).into())
9592            }
9593            ExprKind::Warn(args) => {
9594                let mut msg = String::new();
9595                for a in args {
9596                    let v = self.eval_expr(a)?;
9597                    msg.push_str(&v.to_string());
9598                }
9599                if msg.is_empty() {
9600                    msg = "Warning: something's wrong".to_string();
9601                }
9602                if !msg.ends_with('\n') {
9603                    msg.push_str(&self.die_warn_at_suffix(line));
9604                    msg.push('\n');
9605                }
9606                eprint!("{}", msg);
9607                Ok(PerlValue::integer(1))
9608            }
9609
9610            // Regex
9611            ExprKind::Match {
9612                expr,
9613                pattern,
9614                flags,
9615                scalar_g,
9616                delim: _,
9617            } => {
9618                let val = self.eval_expr(expr)?;
9619                if val.is_iterator() {
9620                    let source = crate::map_stream::into_pull_iter(val);
9621                    let re = self.compile_regex(pattern, flags, line)?;
9622                    let global = flags.contains('g');
9623                    if global {
9624                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9625                            crate::map_stream::MatchGlobalStreamIterator::new(source, re),
9626                        )));
9627                    } else {
9628                        return Ok(PerlValue::iterator(std::sync::Arc::new(
9629                            crate::map_stream::MatchStreamIterator::new(source, re),
9630                        )));
9631                    }
9632                }
9633                let s = val.to_string();
9634                let pos_key = match &expr.kind {
9635                    ExprKind::ScalarVar(n) => n.as_str(),
9636                    _ => "_",
9637                };
9638                self.regex_match_execute(s, pattern, flags, *scalar_g, pos_key, line)
9639            }
9640            ExprKind::Substitution {
9641                expr,
9642                pattern,
9643                replacement,
9644                flags,
9645                delim: _,
9646            } => {
9647                let val = self.eval_expr(expr)?;
9648                if val.is_iterator() {
9649                    let source = crate::map_stream::into_pull_iter(val);
9650                    let re = self.compile_regex(pattern, flags, line)?;
9651                    let global = flags.contains('g');
9652                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9653                        crate::map_stream::SubstStreamIterator::new(
9654                            source,
9655                            re,
9656                            normalize_replacement_backrefs(replacement),
9657                            global,
9658                        ),
9659                    )));
9660                }
9661                let s = val.to_string();
9662                self.regex_subst_execute(
9663                    s,
9664                    pattern,
9665                    replacement.as_str(),
9666                    flags.as_str(),
9667                    expr,
9668                    line,
9669                )
9670            }
9671            ExprKind::Transliterate {
9672                expr,
9673                from,
9674                to,
9675                flags,
9676                delim: _,
9677            } => {
9678                let val = self.eval_expr(expr)?;
9679                if val.is_iterator() {
9680                    let source = crate::map_stream::into_pull_iter(val);
9681                    return Ok(PerlValue::iterator(std::sync::Arc::new(
9682                        crate::map_stream::TransliterateStreamIterator::new(
9683                            source, from, to, flags,
9684                        ),
9685                    )));
9686                }
9687                let s = val.to_string();
9688                self.regex_transliterate_execute(
9689                    s,
9690                    from.as_str(),
9691                    to.as_str(),
9692                    flags.as_str(),
9693                    expr,
9694                    line,
9695                )
9696            }
9697
9698            // List operations
9699            ExprKind::MapExpr {
9700                block,
9701                list,
9702                flatten_array_refs,
9703                stream,
9704            } => {
9705                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9706                if *stream {
9707                    let out =
9708                        self.map_stream_block_output(list_val, block, *flatten_array_refs, line)?;
9709                    if ctx == WantarrayCtx::List {
9710                        return Ok(out);
9711                    }
9712                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9713                }
9714                let items = list_val.to_list();
9715                if items.len() == 1 {
9716                    if let Some(p) = items[0].as_pipeline() {
9717                        if *flatten_array_refs {
9718                            return Err(PerlError::runtime(
9719                                "flat_map onto a pipeline value is not supported in this form — use a pipeline ->map stage",
9720                                line,
9721                            )
9722                            .into());
9723                        }
9724                        let sub = self.anon_coderef_from_block(block);
9725                        self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
9726                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
9727                    }
9728                }
9729                // `map { BLOCK } LIST` evaluates BLOCK in list context so its tail statement's
9730                // list value (comma operator, `..`, `reverse`, `grep`, `@array`, `return
9731                // wantarray-aware sub`, …) flattens into the output instead of collapsing to a
9732                // scalar. Matches Perl's `perlfunc` note that the block is always list context.
9733                let mut result = Vec::new();
9734                for item in items {
9735                    self.scope.set_topic(item);
9736                    let val = self.exec_block_with_tail(block, WantarrayCtx::List)?;
9737                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
9738                }
9739                if ctx == WantarrayCtx::List {
9740                    Ok(PerlValue::array(result))
9741                } else {
9742                    Ok(PerlValue::integer(result.len() as i64))
9743                }
9744            }
9745            ExprKind::ForEachExpr { block, list } => {
9746                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9747                // Lazy: consume iterator one-at-a-time without materializing.
9748                if list_val.is_iterator() {
9749                    let iter = list_val.into_iterator();
9750                    let mut count = 0i64;
9751                    while let Some(item) = iter.next_item() {
9752                        count += 1;
9753                        self.scope.set_topic(item);
9754                        self.exec_block(block)?;
9755                    }
9756                    return Ok(PerlValue::integer(count));
9757                }
9758                let items = list_val.to_list();
9759                let count = items.len();
9760                for item in items {
9761                    self.scope.set_topic(item);
9762                    self.exec_block(block)?;
9763                }
9764                Ok(PerlValue::integer(count as i64))
9765            }
9766            ExprKind::MapExprComma {
9767                expr,
9768                list,
9769                flatten_array_refs,
9770                stream,
9771            } => {
9772                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9773                if *stream {
9774                    let out =
9775                        self.map_stream_expr_output(list_val, expr, *flatten_array_refs, line)?;
9776                    if ctx == WantarrayCtx::List {
9777                        return Ok(out);
9778                    }
9779                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9780                }
9781                let items = list_val.to_list();
9782                let mut result = Vec::new();
9783                for item in items {
9784                    self.scope.set_topic(item.clone());
9785                    let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
9786                    result.extend(val.map_flatten_outputs(*flatten_array_refs));
9787                }
9788                if ctx == WantarrayCtx::List {
9789                    Ok(PerlValue::array(result))
9790                } else {
9791                    Ok(PerlValue::integer(result.len() as i64))
9792                }
9793            }
9794            ExprKind::GrepExpr {
9795                block,
9796                list,
9797                keyword,
9798            } => {
9799                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9800                if keyword.is_stream() {
9801                    let out = self.filter_stream_block_output(list_val, block, line)?;
9802                    if ctx == WantarrayCtx::List {
9803                        return Ok(out);
9804                    }
9805                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9806                }
9807                let items = list_val.to_list();
9808                if items.len() == 1 {
9809                    if let Some(p) = items[0].as_pipeline() {
9810                        let sub = self.anon_coderef_from_block(block);
9811                        self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
9812                        return Ok(PerlValue::pipeline(Arc::clone(&p)));
9813                    }
9814                }
9815                let mut result = Vec::new();
9816                for item in items {
9817                    self.scope.set_topic(item.clone());
9818                    let val = self.exec_block(block)?;
9819                    // Bare regex in block → match against $_ (Perl: /pat/ in
9820                    // grep is `$_ =~ /pat/`, not a truthy regex object).
9821                    let keep = if let Some(re) = val.as_regex() {
9822                        re.is_match(&item.to_string())
9823                    } else {
9824                        val.is_true()
9825                    };
9826                    if keep {
9827                        result.push(item);
9828                    }
9829                }
9830                if ctx == WantarrayCtx::List {
9831                    Ok(PerlValue::array(result))
9832                } else {
9833                    Ok(PerlValue::integer(result.len() as i64))
9834                }
9835            }
9836            ExprKind::GrepExprComma {
9837                expr,
9838                list,
9839                keyword,
9840            } => {
9841                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9842                if keyword.is_stream() {
9843                    let out = self.filter_stream_expr_output(list_val, expr, line)?;
9844                    if ctx == WantarrayCtx::List {
9845                        return Ok(out);
9846                    }
9847                    return Ok(PerlValue::integer(out.to_list().len() as i64));
9848                }
9849                let items = list_val.to_list();
9850                let mut result = Vec::new();
9851                for item in items {
9852                    self.scope.set_topic(item.clone());
9853                    let val = self.eval_expr(expr)?;
9854                    let keep = if let Some(re) = val.as_regex() {
9855                        re.is_match(&item.to_string())
9856                    } else {
9857                        val.is_true()
9858                    };
9859                    if keep {
9860                        result.push(item);
9861                    }
9862                }
9863                if ctx == WantarrayCtx::List {
9864                    Ok(PerlValue::array(result))
9865                } else {
9866                    Ok(PerlValue::integer(result.len() as i64))
9867                }
9868            }
9869            ExprKind::SortExpr { cmp, list } => {
9870                let list_val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9871                let mut items = list_val.to_list();
9872                match cmp {
9873                    Some(SortComparator::Code(code_expr)) => {
9874                        let sub = self.eval_expr(code_expr)?;
9875                        let Some(sub) = sub.as_code_ref() else {
9876                            return Err(PerlError::runtime(
9877                                "sort: comparator must be a code reference",
9878                                line,
9879                            )
9880                            .into());
9881                        };
9882                        let sub = sub.clone();
9883                        items.sort_by(|a, b| {
9884                            let _ = self.scope.set_scalar("a", a.clone());
9885                            let _ = self.scope.set_scalar("b", b.clone());
9886                            let _ = self.scope.set_scalar("_0", a.clone());
9887                            let _ = self.scope.set_scalar("_1", b.clone());
9888                            match self.call_sub(&sub, vec![], ctx, line) {
9889                                Ok(v) => {
9890                                    let n = v.to_int();
9891                                    if n < 0 {
9892                                        Ordering::Less
9893                                    } else if n > 0 {
9894                                        Ordering::Greater
9895                                    } else {
9896                                        Ordering::Equal
9897                                    }
9898                                }
9899                                Err(_) => Ordering::Equal,
9900                            }
9901                        });
9902                    }
9903                    Some(SortComparator::Block(cmp_block)) => {
9904                        if let Some(mode) = detect_sort_block_fast(cmp_block) {
9905                            items.sort_by(|a, b| sort_magic_cmp(a, b, mode));
9906                        } else {
9907                            let cmp_block = cmp_block.clone();
9908                            items.sort_by(|a, b| {
9909                                let _ = self.scope.set_scalar("a", a.clone());
9910                                let _ = self.scope.set_scalar("b", b.clone());
9911                                let _ = self.scope.set_scalar("_0", a.clone());
9912                                let _ = self.scope.set_scalar("_1", b.clone());
9913                                match self.exec_block(&cmp_block) {
9914                                    Ok(v) => {
9915                                        let n = v.to_int();
9916                                        if n < 0 {
9917                                            Ordering::Less
9918                                        } else if n > 0 {
9919                                            Ordering::Greater
9920                                        } else {
9921                                            Ordering::Equal
9922                                        }
9923                                    }
9924                                    Err(_) => Ordering::Equal,
9925                                }
9926                            });
9927                        }
9928                    }
9929                    None => {
9930                        items.sort_by_key(|a| a.to_string());
9931                    }
9932                }
9933                Ok(PerlValue::array(items))
9934            }
9935            ExprKind::ScalarReverse(expr) => {
9936                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
9937                // Lazy: wrap iterator without materializing
9938                if val.is_iterator() {
9939                    return Ok(PerlValue::iterator(Arc::new(
9940                        crate::value::ScalarReverseIterator::new(val.into_iterator()),
9941                    )));
9942                }
9943                let items = val.to_list();
9944                if items.len() <= 1 {
9945                    let s = if items.is_empty() {
9946                        String::new()
9947                    } else {
9948                        items[0].to_string()
9949                    };
9950                    Ok(PerlValue::string(s.chars().rev().collect()))
9951                } else {
9952                    let mut items = items;
9953                    items.reverse();
9954                    Ok(PerlValue::array(items))
9955                }
9956            }
9957            ExprKind::ReverseExpr(list) => {
9958                let val = self.eval_expr_ctx(list, WantarrayCtx::List)?;
9959                match ctx {
9960                    WantarrayCtx::List => {
9961                        let mut items = val.to_list();
9962                        items.reverse();
9963                        Ok(PerlValue::array(items))
9964                    }
9965                    _ => {
9966                        let items = val.to_list();
9967                        let s: String = items.iter().map(|v| v.to_string()).collect();
9968                        Ok(PerlValue::string(s.chars().rev().collect()))
9969                    }
9970                }
9971            }
9972
9973            // ── Parallel operations (rayon-powered) ──
9974            ExprKind::ParLinesExpr {
9975                path,
9976                callback,
9977                progress,
9978            } => self.eval_par_lines_expr(
9979                path.as_ref(),
9980                callback.as_ref(),
9981                progress.as_deref(),
9982                line,
9983            ),
9984            ExprKind::ParWalkExpr {
9985                path,
9986                callback,
9987                progress,
9988            } => {
9989                self.eval_par_walk_expr(path.as_ref(), callback.as_ref(), progress.as_deref(), line)
9990            }
9991            ExprKind::PwatchExpr { path, callback } => {
9992                self.eval_pwatch_expr(path.as_ref(), callback.as_ref(), line)
9993            }
9994            ExprKind::PMapExpr {
9995                block,
9996                list,
9997                progress,
9998                flat_outputs,
9999                on_cluster,
10000                stream,
10001            } => {
10002                let show_progress = progress
10003                    .as_ref()
10004                    .map(|p| self.eval_expr(p))
10005                    .transpose()?
10006                    .map(|v| v.is_true())
10007                    .unwrap_or(false);
10008                let list_val = self.eval_expr(list)?;
10009                if let Some(cluster_e) = on_cluster {
10010                    let cluster_val = self.eval_expr(cluster_e.as_ref())?;
10011                    return self.eval_pmap_remote(
10012                        cluster_val,
10013                        list_val,
10014                        show_progress,
10015                        block,
10016                        *flat_outputs,
10017                        line,
10018                    );
10019                }
10020                if *stream {
10021                    let source = crate::map_stream::into_pull_iter(list_val);
10022                    let sub = self.anon_coderef_from_block(block);
10023                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
10024                    return Ok(PerlValue::iterator(Arc::new(
10025                        crate::map_stream::PMapStreamIterator::new(
10026                            source,
10027                            sub,
10028                            self.subs.clone(),
10029                            capture,
10030                            atomic_arrays,
10031                            atomic_hashes,
10032                            *flat_outputs,
10033                        ),
10034                    )));
10035                }
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                let pmap_progress = PmapProgress::new(show_progress, items.len());
10042
10043                if *flat_outputs {
10044                    let mut indexed: Vec<(usize, Vec<PerlValue>)> = items
10045                        .into_par_iter()
10046                        .enumerate()
10047                        .map(|(i, item)| {
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                            let val = match local_interp.exec_block(&block) {
10057                                Ok(val) => val,
10058                                Err(_) => PerlValue::UNDEF,
10059                            };
10060                            let chunk = val.map_flatten_outputs(true);
10061                            pmap_progress.tick();
10062                            (i, chunk)
10063                        })
10064                        .collect();
10065                    pmap_progress.finish();
10066                    indexed.sort_by_key(|(i, _)| *i);
10067                    let results: Vec<PerlValue> =
10068                        indexed.into_iter().flat_map(|(_, v)| v).collect();
10069                    Ok(PerlValue::array(results))
10070                } else {
10071                    let results: Vec<PerlValue> = items
10072                        .into_par_iter()
10073                        .map(|item| {
10074                            let mut local_interp = Interpreter::new();
10075                            local_interp.subs = subs.clone();
10076                            local_interp.scope.restore_capture(&scope_capture);
10077                            local_interp
10078                                .scope
10079                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10080                            local_interp.enable_parallel_guard();
10081                            local_interp.scope.set_topic(item);
10082                            let val = match local_interp.exec_block(&block) {
10083                                Ok(val) => val,
10084                                Err(_) => PerlValue::UNDEF,
10085                            };
10086                            pmap_progress.tick();
10087                            val
10088                        })
10089                        .collect();
10090                    pmap_progress.finish();
10091                    Ok(PerlValue::array(results))
10092                }
10093            }
10094            ExprKind::PMapChunkedExpr {
10095                chunk_size,
10096                block,
10097                list,
10098                progress,
10099            } => {
10100                let show_progress = progress
10101                    .as_ref()
10102                    .map(|p| self.eval_expr(p))
10103                    .transpose()?
10104                    .map(|v| v.is_true())
10105                    .unwrap_or(false);
10106                let chunk_n = self.eval_expr(chunk_size)?.to_int().max(1) as usize;
10107                let list_val = self.eval_expr(list)?;
10108                let items = list_val.to_list();
10109                let block = block.clone();
10110                let subs = self.subs.clone();
10111                let (scope_capture, atomic_arrays, atomic_hashes) =
10112                    self.scope.capture_with_atomics();
10113
10114                let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = items
10115                    .chunks(chunk_n)
10116                    .enumerate()
10117                    .map(|(i, c)| (i, c.to_vec()))
10118                    .collect();
10119
10120                let n_chunks = indexed_chunks.len();
10121                let pmap_progress = PmapProgress::new(show_progress, n_chunks);
10122
10123                let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
10124                    .into_par_iter()
10125                    .map(|(chunk_idx, chunk)| {
10126                        let mut local_interp = Interpreter::new();
10127                        local_interp.subs = subs.clone();
10128                        local_interp.scope.restore_capture(&scope_capture);
10129                        local_interp
10130                            .scope
10131                            .restore_atomics(&atomic_arrays, &atomic_hashes);
10132                        local_interp.enable_parallel_guard();
10133                        let mut out = Vec::with_capacity(chunk.len());
10134                        for item in chunk {
10135                            local_interp.scope.set_topic(item);
10136                            match local_interp.exec_block(&block) {
10137                                Ok(val) => out.push(val),
10138                                Err(_) => out.push(PerlValue::UNDEF),
10139                            }
10140                        }
10141                        pmap_progress.tick();
10142                        (chunk_idx, out)
10143                    })
10144                    .collect();
10145
10146                pmap_progress.finish();
10147                chunk_results.sort_by_key(|(i, _)| *i);
10148                let results: Vec<PerlValue> =
10149                    chunk_results.into_iter().flat_map(|(_, v)| v).collect();
10150                Ok(PerlValue::array(results))
10151            }
10152            ExprKind::PGrepExpr {
10153                block,
10154                list,
10155                progress,
10156                stream,
10157            } => {
10158                let show_progress = progress
10159                    .as_ref()
10160                    .map(|p| self.eval_expr(p))
10161                    .transpose()?
10162                    .map(|v| v.is_true())
10163                    .unwrap_or(false);
10164                let list_val = self.eval_expr(list)?;
10165                if *stream {
10166                    let source = crate::map_stream::into_pull_iter(list_val);
10167                    let sub = self.anon_coderef_from_block(block);
10168                    let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
10169                    return Ok(PerlValue::iterator(Arc::new(
10170                        crate::map_stream::PGrepStreamIterator::new(
10171                            source,
10172                            sub,
10173                            self.subs.clone(),
10174                            capture,
10175                            atomic_arrays,
10176                            atomic_hashes,
10177                        ),
10178                    )));
10179                }
10180                let items = list_val.to_list();
10181                let block = block.clone();
10182                let subs = self.subs.clone();
10183                let (scope_capture, atomic_arrays, atomic_hashes) =
10184                    self.scope.capture_with_atomics();
10185                let pmap_progress = PmapProgress::new(show_progress, items.len());
10186
10187                let results: Vec<PerlValue> = items
10188                    .into_par_iter()
10189                    .filter_map(|item| {
10190                        let mut local_interp = Interpreter::new();
10191                        local_interp.subs = subs.clone();
10192                        local_interp.scope.restore_capture(&scope_capture);
10193                        local_interp
10194                            .scope
10195                            .restore_atomics(&atomic_arrays, &atomic_hashes);
10196                        local_interp.enable_parallel_guard();
10197                        local_interp.scope.set_topic(item.clone());
10198                        let keep = match local_interp.exec_block(&block) {
10199                            Ok(val) => val.is_true(),
10200                            Err(_) => false,
10201                        };
10202                        pmap_progress.tick();
10203                        if keep {
10204                            Some(item)
10205                        } else {
10206                            None
10207                        }
10208                    })
10209                    .collect();
10210                pmap_progress.finish();
10211                Ok(PerlValue::array(results))
10212            }
10213            ExprKind::PForExpr {
10214                block,
10215                list,
10216                progress,
10217            } => {
10218                let show_progress = progress
10219                    .as_ref()
10220                    .map(|p| self.eval_expr(p))
10221                    .transpose()?
10222                    .map(|v| v.is_true())
10223                    .unwrap_or(false);
10224                let list_val = self.eval_expr(list)?;
10225                let items = list_val.to_list();
10226                let block = block.clone();
10227                let subs = self.subs.clone();
10228                let (scope_capture, atomic_arrays, atomic_hashes) =
10229                    self.scope.capture_with_atomics();
10230
10231                let pmap_progress = PmapProgress::new(show_progress, items.len());
10232                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10233                items.into_par_iter().for_each(|item| {
10234                    if first_err.lock().is_some() {
10235                        return;
10236                    }
10237                    let mut local_interp = Interpreter::new();
10238                    local_interp.subs = subs.clone();
10239                    local_interp.scope.restore_capture(&scope_capture);
10240                    local_interp
10241                        .scope
10242                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10243                    local_interp.enable_parallel_guard();
10244                    local_interp.scope.set_topic(item);
10245                    match local_interp.exec_block(&block) {
10246                        Ok(_) => {}
10247                        Err(e) => {
10248                            let stryke = match e {
10249                                FlowOrError::Error(stryke) => stryke,
10250                                FlowOrError::Flow(_) => PerlError::runtime(
10251                                    "return/last/next/redo not supported inside pfor block",
10252                                    line,
10253                                ),
10254                            };
10255                            let mut g = first_err.lock();
10256                            if g.is_none() {
10257                                *g = Some(stryke);
10258                            }
10259                        }
10260                    }
10261                    pmap_progress.tick();
10262                });
10263                pmap_progress.finish();
10264                if let Some(e) = first_err.lock().take() {
10265                    return Err(FlowOrError::Error(e));
10266                }
10267                Ok(PerlValue::UNDEF)
10268            }
10269            ExprKind::FanExpr {
10270                count,
10271                block,
10272                progress,
10273                capture,
10274            } => {
10275                let show_progress = progress
10276                    .as_ref()
10277                    .map(|p| self.eval_expr(p))
10278                    .transpose()?
10279                    .map(|v| v.is_true())
10280                    .unwrap_or(false);
10281                let n = match count {
10282                    Some(c) => self.eval_expr(c)?.to_int().max(0) as usize,
10283                    None => self.parallel_thread_count(),
10284                };
10285                let block = block.clone();
10286                let subs = self.subs.clone();
10287                let (scope_capture, atomic_arrays, atomic_hashes) =
10288                    self.scope.capture_with_atomics();
10289
10290                let fan_progress = FanProgress::new(show_progress, n);
10291                if *capture {
10292                    if n == 0 {
10293                        return Ok(PerlValue::array(Vec::new()));
10294                    }
10295                    let pairs: Vec<(usize, ExecResult)> = (0..n)
10296                        .into_par_iter()
10297                        .map(|i| {
10298                            fan_progress.start_worker(i);
10299                            let mut local_interp = Interpreter::new();
10300                            local_interp.subs = subs.clone();
10301                            local_interp.suppress_stdout = show_progress;
10302                            local_interp.scope.restore_capture(&scope_capture);
10303                            local_interp
10304                                .scope
10305                                .restore_atomics(&atomic_arrays, &atomic_hashes);
10306                            local_interp.enable_parallel_guard();
10307                            local_interp.scope.set_topic(PerlValue::integer(i as i64));
10308                            crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10309                            let res = local_interp.exec_block(&block);
10310                            crate::parallel_trace::fan_worker_set_index(None);
10311                            fan_progress.finish_worker(i);
10312                            (i, res)
10313                        })
10314                        .collect();
10315                    fan_progress.finish();
10316                    let mut pairs = pairs;
10317                    pairs.sort_by_key(|(i, _)| *i);
10318                    let mut out = Vec::with_capacity(n);
10319                    for (_, r) in pairs {
10320                        match r {
10321                            Ok(v) => out.push(v),
10322                            Err(e) => return Err(e),
10323                        }
10324                    }
10325                    return Ok(PerlValue::array(out));
10326                }
10327                let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
10328                (0..n).into_par_iter().for_each(|i| {
10329                    if first_err.lock().is_some() {
10330                        return;
10331                    }
10332                    fan_progress.start_worker(i);
10333                    let mut local_interp = Interpreter::new();
10334                    local_interp.subs = subs.clone();
10335                    local_interp.suppress_stdout = show_progress;
10336                    local_interp.scope.restore_capture(&scope_capture);
10337                    local_interp
10338                        .scope
10339                        .restore_atomics(&atomic_arrays, &atomic_hashes);
10340                    local_interp.enable_parallel_guard();
10341                    local_interp.scope.set_topic(PerlValue::integer(i as i64));
10342                    crate::parallel_trace::fan_worker_set_index(Some(i as i64));
10343                    match local_interp.exec_block(&block) {
10344                        Ok(_) => {}
10345                        Err(e) => {
10346                            let stryke = match e {
10347                                FlowOrError::Error(stryke) => stryke,
10348                                FlowOrError::Flow(_) => PerlError::runtime(
10349                                    "return/last/next/redo not supported inside fan block",
10350                                    line,
10351                                ),
10352                            };
10353                            let mut g = first_err.lock();
10354                            if g.is_none() {
10355                                *g = Some(stryke);
10356                            }
10357                        }
10358                    }
10359                    crate::parallel_trace::fan_worker_set_index(None);
10360                    fan_progress.finish_worker(i);
10361                });
10362                fan_progress.finish();
10363                if let Some(e) = first_err.lock().take() {
10364                    return Err(FlowOrError::Error(e));
10365                }
10366                Ok(PerlValue::UNDEF)
10367            }
10368            ExprKind::RetryBlock {
10369                body,
10370                times,
10371                backoff,
10372            } => self.eval_retry_block(body, times, *backoff, line),
10373            ExprKind::RateLimitBlock {
10374                slot,
10375                max,
10376                window,
10377                body,
10378            } => self.eval_rate_limit_block(*slot, max, window, body, line),
10379            ExprKind::EveryBlock { interval, body } => self.eval_every_block(interval, body, line),
10380            ExprKind::GenBlock { body } => {
10381                let g = Arc::new(PerlGenerator {
10382                    block: body.clone(),
10383                    pc: Mutex::new(0),
10384                    scope_started: Mutex::new(false),
10385                    exhausted: Mutex::new(false),
10386                });
10387                Ok(PerlValue::generator(g))
10388            }
10389            ExprKind::Yield(e) => {
10390                if !self.in_generator {
10391                    return Err(PerlError::runtime("yield outside gen block", line).into());
10392                }
10393                let v = self.eval_expr(e)?;
10394                Err(FlowOrError::Flow(Flow::Yield(v)))
10395            }
10396            ExprKind::AlgebraicMatch { subject, arms } => {
10397                self.eval_algebraic_match(subject, arms, line)
10398            }
10399            ExprKind::AsyncBlock { body } | ExprKind::SpawnBlock { body } => {
10400                Ok(self.spawn_async_block(body))
10401            }
10402            ExprKind::Trace { body } => {
10403                crate::parallel_trace::trace_enter();
10404                let out = self.exec_block(body);
10405                crate::parallel_trace::trace_leave();
10406                out
10407            }
10408            ExprKind::Spinner { message, body } => {
10409                use std::io::Write as _;
10410                let msg = self.eval_expr(message)?.to_string();
10411                let done = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
10412                let done2 = done.clone();
10413                let handle = std::thread::spawn(move || {
10414                    let frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
10415                    let mut i = 0;
10416                    let stderr = std::io::stderr();
10417                    while !done2.load(std::sync::atomic::Ordering::Relaxed) {
10418                        {
10419                            let stdout = std::io::stdout();
10420                            let _stdout_lock = stdout.lock();
10421                            let mut err = stderr.lock();
10422                            let _ = write!(
10423                                err,
10424                                "\r\x1b[2K\x1b[36m{}\x1b[0m {} ",
10425                                frames[i % frames.len()],
10426                                msg
10427                            );
10428                            let _ = err.flush();
10429                        }
10430                        std::thread::sleep(std::time::Duration::from_millis(80));
10431                        i += 1;
10432                    }
10433                    let mut err = stderr.lock();
10434                    let _ = write!(err, "\r\x1b[2K");
10435                    let _ = err.flush();
10436                });
10437                let result = self.exec_block(body);
10438                done.store(true, std::sync::atomic::Ordering::Relaxed);
10439                let _ = handle.join();
10440                result
10441            }
10442            ExprKind::Timer { body } => {
10443                let start = std::time::Instant::now();
10444                self.exec_block(body)?;
10445                let ms = start.elapsed().as_secs_f64() * 1000.0;
10446                Ok(PerlValue::float(ms))
10447            }
10448            ExprKind::Bench { body, times } => {
10449                let n = self.eval_expr(times)?.to_int();
10450                if n < 0 {
10451                    return Err(PerlError::runtime(
10452                        "bench: iteration count must be non-negative",
10453                        line,
10454                    )
10455                    .into());
10456                }
10457                self.run_bench_block(body, n as usize, line)
10458            }
10459            ExprKind::Await(expr) => {
10460                let v = self.eval_expr(expr)?;
10461                if let Some(t) = v.as_async_task() {
10462                    t.await_result().map_err(FlowOrError::from)
10463                } else {
10464                    Ok(v)
10465                }
10466            }
10467            ExprKind::Slurp(e) => {
10468                let path = self.eval_expr(e)?.to_string();
10469                read_file_text_perl_compat(&path)
10470                    .map(PerlValue::string)
10471                    .map_err(|e| {
10472                        FlowOrError::Error(PerlError::runtime(format!("slurp: {}", e), line))
10473                    })
10474            }
10475            ExprKind::Capture(e) => {
10476                let cmd = self.eval_expr(e)?.to_string();
10477                let output = Command::new("sh")
10478                    .arg("-c")
10479                    .arg(&cmd)
10480                    .output()
10481                    .map_err(|e| {
10482                        FlowOrError::Error(PerlError::runtime(format!("capture: {}", e), line))
10483                    })?;
10484                self.record_child_exit_status(output.status);
10485                let exitcode = output.status.code().unwrap_or(-1) as i64;
10486                let stdout = decode_utf8_or_latin1(&output.stdout);
10487                let stderr = decode_utf8_or_latin1(&output.stderr);
10488                Ok(PerlValue::capture(Arc::new(CaptureResult {
10489                    stdout,
10490                    stderr,
10491                    exitcode,
10492                })))
10493            }
10494            ExprKind::Qx(e) => {
10495                let cmd = self.eval_expr(e)?.to_string();
10496                crate::capture::run_readpipe(self, &cmd, line).map_err(FlowOrError::Error)
10497            }
10498            ExprKind::FetchUrl(e) => {
10499                let url = self.eval_expr(e)?.to_string();
10500                ureq::get(&url)
10501                    .call()
10502                    .map_err(|e| {
10503                        FlowOrError::Error(PerlError::runtime(format!("fetch_url: {}", e), line))
10504                    })
10505                    .and_then(|r| {
10506                        r.into_string().map(PerlValue::string).map_err(|e| {
10507                            FlowOrError::Error(PerlError::runtime(
10508                                format!("fetch_url: {}", e),
10509                                line,
10510                            ))
10511                        })
10512                    })
10513            }
10514            ExprKind::Pchannel { capacity } => {
10515                if let Some(c) = capacity {
10516                    let n = self.eval_expr(c)?.to_int().max(1) as usize;
10517                    Ok(crate::pchannel::create_bounded_pair(n))
10518                } else {
10519                    Ok(crate::pchannel::create_pair())
10520                }
10521            }
10522            ExprKind::PSortExpr {
10523                cmp,
10524                list,
10525                progress,
10526            } => {
10527                let show_progress = progress
10528                    .as_ref()
10529                    .map(|p| self.eval_expr(p))
10530                    .transpose()?
10531                    .map(|v| v.is_true())
10532                    .unwrap_or(false);
10533                let list_val = self.eval_expr(list)?;
10534                let mut items = list_val.to_list();
10535                let pmap_progress = PmapProgress::new(show_progress, 2);
10536                pmap_progress.tick();
10537                if let Some(cmp_block) = cmp {
10538                    if let Some(mode) = detect_sort_block_fast(cmp_block) {
10539                        items.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
10540                    } else {
10541                        let cmp_block = cmp_block.clone();
10542                        let subs = self.subs.clone();
10543                        let scope_capture = self.scope.capture();
10544                        items.par_sort_by(|a, b| {
10545                            let mut local_interp = Interpreter::new();
10546                            local_interp.subs = subs.clone();
10547                            local_interp.scope.restore_capture(&scope_capture);
10548                            let _ = local_interp.scope.set_scalar("a", a.clone());
10549                            let _ = local_interp.scope.set_scalar("b", b.clone());
10550                            let _ = local_interp.scope.set_scalar("_0", a.clone());
10551                            let _ = local_interp.scope.set_scalar("_1", b.clone());
10552                            match local_interp.exec_block(&cmp_block) {
10553                                Ok(v) => {
10554                                    let n = v.to_int();
10555                                    if n < 0 {
10556                                        std::cmp::Ordering::Less
10557                                    } else if n > 0 {
10558                                        std::cmp::Ordering::Greater
10559                                    } else {
10560                                        std::cmp::Ordering::Equal
10561                                    }
10562                                }
10563                                Err(_) => std::cmp::Ordering::Equal,
10564                            }
10565                        });
10566                    }
10567                } else {
10568                    items.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
10569                }
10570                pmap_progress.tick();
10571                pmap_progress.finish();
10572                Ok(PerlValue::array(items))
10573            }
10574
10575            ExprKind::ReduceExpr { block, list } => {
10576                let list_val = self.eval_expr(list)?;
10577                let items = list_val.to_list();
10578                if items.is_empty() {
10579                    return Ok(PerlValue::UNDEF);
10580                }
10581                if items.len() == 1 {
10582                    return Ok(items.into_iter().next().unwrap());
10583                }
10584                let block = block.clone();
10585                let subs = self.subs.clone();
10586                let scope_capture = self.scope.capture();
10587                let mut acc = items[0].clone();
10588                for b in items.into_iter().skip(1) {
10589                    let mut local_interp = Interpreter::new();
10590                    local_interp.subs = subs.clone();
10591                    local_interp.scope.restore_capture(&scope_capture);
10592                    let _ = local_interp.scope.set_scalar("a", acc.clone());
10593                    let _ = local_interp.scope.set_scalar("b", b.clone());
10594                    let _ = local_interp.scope.set_scalar("_0", acc);
10595                    let _ = local_interp.scope.set_scalar("_1", b);
10596                    acc = match local_interp.exec_block(&block) {
10597                        Ok(val) => val,
10598                        Err(_) => PerlValue::UNDEF,
10599                    };
10600                }
10601                Ok(acc)
10602            }
10603
10604            ExprKind::PReduceExpr {
10605                block,
10606                list,
10607                progress,
10608            } => {
10609                let show_progress = progress
10610                    .as_ref()
10611                    .map(|p| self.eval_expr(p))
10612                    .transpose()?
10613                    .map(|v| v.is_true())
10614                    .unwrap_or(false);
10615                let list_val = self.eval_expr(list)?;
10616                let items = list_val.to_list();
10617                if items.is_empty() {
10618                    return Ok(PerlValue::UNDEF);
10619                }
10620                if items.len() == 1 {
10621                    return Ok(items.into_iter().next().unwrap());
10622                }
10623                let block = block.clone();
10624                let subs = self.subs.clone();
10625                let scope_capture = self.scope.capture();
10626                let pmap_progress = PmapProgress::new(show_progress, items.len());
10627
10628                let result = items
10629                    .into_par_iter()
10630                    .map(|x| {
10631                        pmap_progress.tick();
10632                        x
10633                    })
10634                    .reduce_with(|a, b| {
10635                        let mut local_interp = Interpreter::new();
10636                        local_interp.subs = subs.clone();
10637                        local_interp.scope.restore_capture(&scope_capture);
10638                        let _ = local_interp.scope.set_scalar("a", a.clone());
10639                        let _ = local_interp.scope.set_scalar("b", b.clone());
10640                        let _ = local_interp.scope.set_scalar("_0", a);
10641                        let _ = local_interp.scope.set_scalar("_1", b);
10642                        match local_interp.exec_block(&block) {
10643                            Ok(val) => val,
10644                            Err(_) => PerlValue::UNDEF,
10645                        }
10646                    });
10647                pmap_progress.finish();
10648                Ok(result.unwrap_or(PerlValue::UNDEF))
10649            }
10650
10651            ExprKind::PReduceInitExpr {
10652                init,
10653                block,
10654                list,
10655                progress,
10656            } => {
10657                let show_progress = progress
10658                    .as_ref()
10659                    .map(|p| self.eval_expr(p))
10660                    .transpose()?
10661                    .map(|v| v.is_true())
10662                    .unwrap_or(false);
10663                let init_val = self.eval_expr(init)?;
10664                let list_val = self.eval_expr(list)?;
10665                let items = list_val.to_list();
10666                if items.is_empty() {
10667                    return Ok(init_val);
10668                }
10669                let block = block.clone();
10670                let subs = self.subs.clone();
10671                let scope_capture = self.scope.capture();
10672                let cap: &[(String, PerlValue)] = scope_capture.as_slice();
10673                if items.len() == 1 {
10674                    return Ok(fold_preduce_init_step(
10675                        &subs,
10676                        cap,
10677                        &block,
10678                        preduce_init_fold_identity(&init_val),
10679                        items.into_iter().next().unwrap(),
10680                    ));
10681                }
10682                let pmap_progress = PmapProgress::new(show_progress, items.len());
10683                let result = items
10684                    .into_par_iter()
10685                    .fold(
10686                        || preduce_init_fold_identity(&init_val),
10687                        |acc, item| {
10688                            pmap_progress.tick();
10689                            fold_preduce_init_step(&subs, cap, &block, acc, item)
10690                        },
10691                    )
10692                    .reduce(
10693                        || preduce_init_fold_identity(&init_val),
10694                        |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
10695                    );
10696                pmap_progress.finish();
10697                Ok(result)
10698            }
10699
10700            ExprKind::PMapReduceExpr {
10701                map_block,
10702                reduce_block,
10703                list,
10704                progress,
10705            } => {
10706                let show_progress = progress
10707                    .as_ref()
10708                    .map(|p| self.eval_expr(p))
10709                    .transpose()?
10710                    .map(|v| v.is_true())
10711                    .unwrap_or(false);
10712                let list_val = self.eval_expr(list)?;
10713                let items = list_val.to_list();
10714                if items.is_empty() {
10715                    return Ok(PerlValue::UNDEF);
10716                }
10717                let map_block = map_block.clone();
10718                let reduce_block = reduce_block.clone();
10719                let subs = self.subs.clone();
10720                let scope_capture = self.scope.capture();
10721                if items.len() == 1 {
10722                    let mut local_interp = Interpreter::new();
10723                    local_interp.subs = subs.clone();
10724                    local_interp.scope.restore_capture(&scope_capture);
10725                    local_interp.scope.set_topic(items[0].clone());
10726                    return match local_interp.exec_block_no_scope(&map_block) {
10727                        Ok(v) => Ok(v),
10728                        Err(_) => Ok(PerlValue::UNDEF),
10729                    };
10730                }
10731                let pmap_progress = PmapProgress::new(show_progress, items.len());
10732                let result = items
10733                    .into_par_iter()
10734                    .map(|item| {
10735                        let mut local_interp = Interpreter::new();
10736                        local_interp.subs = subs.clone();
10737                        local_interp.scope.restore_capture(&scope_capture);
10738                        local_interp.scope.set_topic(item);
10739                        let val = match local_interp.exec_block_no_scope(&map_block) {
10740                            Ok(val) => val,
10741                            Err(_) => PerlValue::UNDEF,
10742                        };
10743                        pmap_progress.tick();
10744                        val
10745                    })
10746                    .reduce_with(|a, b| {
10747                        let mut local_interp = Interpreter::new();
10748                        local_interp.subs = subs.clone();
10749                        local_interp.scope.restore_capture(&scope_capture);
10750                        let _ = local_interp.scope.set_scalar("a", a.clone());
10751                        let _ = local_interp.scope.set_scalar("b", b.clone());
10752                        let _ = local_interp.scope.set_scalar("_0", a);
10753                        let _ = local_interp.scope.set_scalar("_1", b);
10754                        match local_interp.exec_block_no_scope(&reduce_block) {
10755                            Ok(val) => val,
10756                            Err(_) => PerlValue::UNDEF,
10757                        }
10758                    });
10759                pmap_progress.finish();
10760                Ok(result.unwrap_or(PerlValue::UNDEF))
10761            }
10762
10763            ExprKind::PcacheExpr {
10764                block,
10765                list,
10766                progress,
10767            } => {
10768                let show_progress = progress
10769                    .as_ref()
10770                    .map(|p| self.eval_expr(p))
10771                    .transpose()?
10772                    .map(|v| v.is_true())
10773                    .unwrap_or(false);
10774                let list_val = self.eval_expr(list)?;
10775                let items = list_val.to_list();
10776                let block = block.clone();
10777                let subs = self.subs.clone();
10778                let scope_capture = self.scope.capture();
10779                let cache = &*crate::pcache::GLOBAL_PCACHE;
10780                let pmap_progress = PmapProgress::new(show_progress, items.len());
10781                let results: Vec<PerlValue> = items
10782                    .into_par_iter()
10783                    .map(|item| {
10784                        let k = crate::pcache::cache_key(&item);
10785                        if let Some(v) = cache.get(&k) {
10786                            pmap_progress.tick();
10787                            return v.clone();
10788                        }
10789                        let mut local_interp = Interpreter::new();
10790                        local_interp.subs = subs.clone();
10791                        local_interp.scope.restore_capture(&scope_capture);
10792                        local_interp.scope.set_topic(item.clone());
10793                        let val = match local_interp.exec_block_no_scope(&block) {
10794                            Ok(v) => v,
10795                            Err(_) => PerlValue::UNDEF,
10796                        };
10797                        cache.insert(k, val.clone());
10798                        pmap_progress.tick();
10799                        val
10800                    })
10801                    .collect();
10802                pmap_progress.finish();
10803                Ok(PerlValue::array(results))
10804            }
10805
10806            ExprKind::PselectExpr { receivers, timeout } => {
10807                let mut rx_vals = Vec::with_capacity(receivers.len());
10808                for r in receivers {
10809                    rx_vals.push(self.eval_expr(r)?);
10810                }
10811                let dur = if let Some(t) = timeout.as_ref() {
10812                    Some(std::time::Duration::from_secs_f64(
10813                        self.eval_expr(t)?.to_number().max(0.0),
10814                    ))
10815                } else {
10816                    None
10817                };
10818                Ok(crate::pchannel::pselect_recv_with_optional_timeout(
10819                    &rx_vals, dur, line,
10820                )?)
10821            }
10822
10823            // Array ops
10824            ExprKind::Push { array, values } => {
10825                self.eval_push_expr(array.as_ref(), values.as_slice(), line)
10826            }
10827            ExprKind::Pop(array) => self.eval_pop_expr(array.as_ref(), line),
10828            ExprKind::Shift(array) => self.eval_shift_expr(array.as_ref(), line),
10829            ExprKind::Unshift { array, values } => {
10830                self.eval_unshift_expr(array.as_ref(), values.as_slice(), line)
10831            }
10832            ExprKind::Splice {
10833                array,
10834                offset,
10835                length,
10836                replacement,
10837            } => self.eval_splice_expr(
10838                array.as_ref(),
10839                offset.as_deref(),
10840                length.as_deref(),
10841                replacement.as_slice(),
10842                ctx,
10843                line,
10844            ),
10845            ExprKind::Delete(expr) => self.eval_delete_operand(expr.as_ref(), line),
10846            ExprKind::Exists(expr) => self.eval_exists_operand(expr.as_ref(), line),
10847            ExprKind::Keys(expr) => {
10848                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10849                let keys = Self::keys_from_value(val, line)?;
10850                if ctx == WantarrayCtx::List {
10851                    Ok(keys)
10852                } else {
10853                    let n = keys.as_array_vec().map(|a| a.len()).unwrap_or(0);
10854                    Ok(PerlValue::integer(n as i64))
10855                }
10856            }
10857            ExprKind::Values(expr) => {
10858                let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
10859                let vals = Self::values_from_value(val, line)?;
10860                if ctx == WantarrayCtx::List {
10861                    Ok(vals)
10862                } else {
10863                    let n = vals.as_array_vec().map(|a| a.len()).unwrap_or(0);
10864                    Ok(PerlValue::integer(n as i64))
10865                }
10866            }
10867            ExprKind::Each(_) => {
10868                // Simplified: returns empty list (full iterator state would need more work)
10869                Ok(PerlValue::array(vec![]))
10870            }
10871
10872            // String ops
10873            ExprKind::Chomp(expr) => {
10874                let val = self.eval_expr(expr)?;
10875                self.chomp_inplace_execute(val, expr)
10876            }
10877            ExprKind::Chop(expr) => {
10878                let val = self.eval_expr(expr)?;
10879                self.chop_inplace_execute(val, expr)
10880            }
10881            ExprKind::Length(expr) => {
10882                let val = self.eval_expr(expr)?;
10883                Ok(if let Some(a) = val.as_array_vec() {
10884                    PerlValue::integer(a.len() as i64)
10885                } else if let Some(h) = val.as_hash_map() {
10886                    PerlValue::integer(h.len() as i64)
10887                } else if let Some(b) = val.as_bytes_arc() {
10888                    PerlValue::integer(b.len() as i64)
10889                } else {
10890                    PerlValue::integer(val.to_string().len() as i64)
10891                })
10892            }
10893            ExprKind::Substr {
10894                string,
10895                offset,
10896                length,
10897                replacement,
10898            } => self.eval_substr_expr(
10899                string.as_ref(),
10900                offset.as_ref(),
10901                length.as_deref(),
10902                replacement.as_deref(),
10903                line,
10904            ),
10905            ExprKind::Index {
10906                string,
10907                substr,
10908                position,
10909            } => {
10910                let s = self.eval_expr(string)?.to_string();
10911                let sub = self.eval_expr(substr)?.to_string();
10912                let pos = if let Some(p) = position {
10913                    self.eval_expr(p)?.to_int() as usize
10914                } else {
10915                    0
10916                };
10917                let result = s[pos..].find(&sub).map(|i| (i + pos) as i64).unwrap_or(-1);
10918                Ok(PerlValue::integer(result))
10919            }
10920            ExprKind::Rindex {
10921                string,
10922                substr,
10923                position,
10924            } => {
10925                let s = self.eval_expr(string)?.to_string();
10926                let sub = self.eval_expr(substr)?.to_string();
10927                let end = if let Some(p) = position {
10928                    self.eval_expr(p)?.to_int() as usize + sub.len()
10929                } else {
10930                    s.len()
10931                };
10932                let search = &s[..end.min(s.len())];
10933                let result = search.rfind(&sub).map(|i| i as i64).unwrap_or(-1);
10934                Ok(PerlValue::integer(result))
10935            }
10936            ExprKind::Sprintf { format, args } => {
10937                let fmt = self.eval_expr(format)?.to_string();
10938                // sprintf args are Perl list context — splat ranges, arrays, and list-valued
10939                // builtins into individual format arguments.
10940                let mut arg_vals = Vec::new();
10941                for a in args {
10942                    let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
10943                    if let Some(items) = v.as_array_vec() {
10944                        arg_vals.extend(items);
10945                    } else {
10946                        arg_vals.push(v);
10947                    }
10948                }
10949                let s = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
10950                Ok(PerlValue::string(s))
10951            }
10952            ExprKind::JoinExpr { separator, list } => {
10953                let sep = self.eval_expr(separator)?.to_string();
10954                // Like Perl 5, arguments after the separator are evaluated in list context so
10955                // `join(",", uniq @x)` passes list context into `uniq`, and `join(",", localtime())`
10956                // expands `localtime` to nine fields.
10957                let items = if let ExprKind::List(exprs) = &list.kind {
10958                    let saved = self.wantarray_kind;
10959                    self.wantarray_kind = WantarrayCtx::List;
10960                    let mut vals = Vec::new();
10961                    for e in exprs {
10962                        let v = self.eval_expr_ctx(e, self.wantarray_kind)?;
10963                        if let Some(items) = v.as_array_vec() {
10964                            vals.extend(items);
10965                        } else {
10966                            vals.push(v);
10967                        }
10968                    }
10969                    self.wantarray_kind = saved;
10970                    vals
10971                } else {
10972                    let saved = self.wantarray_kind;
10973                    self.wantarray_kind = WantarrayCtx::List;
10974                    let v = self.eval_expr_ctx(list, WantarrayCtx::List)?;
10975                    self.wantarray_kind = saved;
10976                    if let Some(items) = v.as_array_vec() {
10977                        items
10978                    } else {
10979                        vec![v]
10980                    }
10981                };
10982                let mut strs = Vec::with_capacity(items.len());
10983                for v in &items {
10984                    strs.push(self.stringify_value(v.clone(), line)?);
10985                }
10986                Ok(PerlValue::string(strs.join(&sep)))
10987            }
10988            ExprKind::SplitExpr {
10989                pattern,
10990                string,
10991                limit,
10992            } => {
10993                let pat = self.eval_expr(pattern)?.to_string();
10994                let s = self.eval_expr(string)?.to_string();
10995                let lim = if let Some(l) = limit {
10996                    self.eval_expr(l)?.to_int() as usize
10997                } else {
10998                    0
10999                };
11000                let re = self.compile_regex(&pat, "", line)?;
11001                let parts: Vec<PerlValue> = if lim > 0 {
11002                    re.splitn_strings(&s, lim)
11003                        .into_iter()
11004                        .map(PerlValue::string)
11005                        .collect()
11006                } else {
11007                    re.split_strings(&s)
11008                        .into_iter()
11009                        .map(PerlValue::string)
11010                        .collect()
11011                };
11012                Ok(PerlValue::array(parts))
11013            }
11014
11015            // Numeric
11016            ExprKind::Abs(expr) => {
11017                let val = self.eval_expr(expr)?;
11018                if let Some(r) = self.try_overload_unary_dispatch("abs", &val, line) {
11019                    return r;
11020                }
11021                Ok(PerlValue::float(val.to_number().abs()))
11022            }
11023            ExprKind::Int(expr) => {
11024                let val = self.eval_expr(expr)?;
11025                Ok(PerlValue::integer(val.to_number() as i64))
11026            }
11027            ExprKind::Sqrt(expr) => {
11028                let val = self.eval_expr(expr)?;
11029                Ok(PerlValue::float(val.to_number().sqrt()))
11030            }
11031            ExprKind::Sin(expr) => {
11032                let val = self.eval_expr(expr)?;
11033                Ok(PerlValue::float(val.to_number().sin()))
11034            }
11035            ExprKind::Cos(expr) => {
11036                let val = self.eval_expr(expr)?;
11037                Ok(PerlValue::float(val.to_number().cos()))
11038            }
11039            ExprKind::Atan2 { y, x } => {
11040                let yv = self.eval_expr(y)?.to_number();
11041                let xv = self.eval_expr(x)?.to_number();
11042                Ok(PerlValue::float(yv.atan2(xv)))
11043            }
11044            ExprKind::Exp(expr) => {
11045                let val = self.eval_expr(expr)?;
11046                Ok(PerlValue::float(val.to_number().exp()))
11047            }
11048            ExprKind::Log(expr) => {
11049                let val = self.eval_expr(expr)?;
11050                Ok(PerlValue::float(val.to_number().ln()))
11051            }
11052            ExprKind::Rand(upper) => {
11053                let u = match upper {
11054                    Some(e) => self.eval_expr(e)?.to_number(),
11055                    None => 1.0,
11056                };
11057                Ok(PerlValue::float(self.perl_rand(u)))
11058            }
11059            ExprKind::Srand(seed) => {
11060                let s = match seed {
11061                    Some(e) => Some(self.eval_expr(e)?.to_number()),
11062                    None => None,
11063                };
11064                Ok(PerlValue::integer(self.perl_srand(s)))
11065            }
11066            ExprKind::Hex(expr) => {
11067                let val = self.eval_expr(expr)?.to_string();
11068                let clean = val.trim().trim_start_matches("0x").trim_start_matches("0X");
11069                let n = i64::from_str_radix(clean, 16).unwrap_or(0);
11070                Ok(PerlValue::integer(n))
11071            }
11072            ExprKind::Oct(expr) => {
11073                let val = self.eval_expr(expr)?.to_string();
11074                let s = val.trim();
11075                let n = if s.starts_with("0x") || s.starts_with("0X") {
11076                    i64::from_str_radix(&s[2..], 16).unwrap_or(0)
11077                } else if s.starts_with("0b") || s.starts_with("0B") {
11078                    i64::from_str_radix(&s[2..], 2).unwrap_or(0)
11079                } else if s.starts_with("0o") || s.starts_with("0O") {
11080                    i64::from_str_radix(&s[2..], 8).unwrap_or(0)
11081                } else {
11082                    i64::from_str_radix(s.trim_start_matches('0'), 8).unwrap_or(0)
11083                };
11084                Ok(PerlValue::integer(n))
11085            }
11086
11087            // Case
11088            ExprKind::Lc(expr) => Ok(PerlValue::string(
11089                self.eval_expr(expr)?.to_string().to_lowercase(),
11090            )),
11091            ExprKind::Uc(expr) => Ok(PerlValue::string(
11092                self.eval_expr(expr)?.to_string().to_uppercase(),
11093            )),
11094            ExprKind::Lcfirst(expr) => {
11095                let s = self.eval_expr(expr)?.to_string();
11096                let mut chars = s.chars();
11097                let result = match chars.next() {
11098                    Some(c) => c.to_lowercase().to_string() + chars.as_str(),
11099                    None => String::new(),
11100                };
11101                Ok(PerlValue::string(result))
11102            }
11103            ExprKind::Ucfirst(expr) => {
11104                let s = self.eval_expr(expr)?.to_string();
11105                let mut chars = s.chars();
11106                let result = match chars.next() {
11107                    Some(c) => c.to_uppercase().to_string() + chars.as_str(),
11108                    None => String::new(),
11109                };
11110                Ok(PerlValue::string(result))
11111            }
11112            ExprKind::Fc(expr) => Ok(PerlValue::string(default_case_fold_str(
11113                &self.eval_expr(expr)?.to_string(),
11114            ))),
11115            ExprKind::Crypt { plaintext, salt } => {
11116                let p = self.eval_expr(plaintext)?.to_string();
11117                let sl = self.eval_expr(salt)?.to_string();
11118                Ok(PerlValue::string(perl_crypt(&p, &sl)))
11119            }
11120            ExprKind::Pos(e) => {
11121                let key = match e {
11122                    None => "_".to_string(),
11123                    Some(expr) => match &expr.kind {
11124                        ExprKind::ScalarVar(n) => n.clone(),
11125                        _ => self.eval_expr(expr)?.to_string(),
11126                    },
11127                };
11128                Ok(self
11129                    .regex_pos
11130                    .get(&key)
11131                    .copied()
11132                    .flatten()
11133                    .map(|p| PerlValue::integer(p as i64))
11134                    .unwrap_or(PerlValue::UNDEF))
11135            }
11136            ExprKind::Study(expr) => {
11137                let s = self.eval_expr(expr)?.to_string();
11138                Ok(Self::study_return_value(&s))
11139            }
11140
11141            // Type
11142            ExprKind::Defined(expr) => {
11143                // Perl: `defined &foo` / `defined &Pkg::name` — true iff the subroutine exists (no call).
11144                if let ExprKind::SubroutineRef(name) = &expr.kind {
11145                    let exists = self.resolve_sub_by_name(name).is_some();
11146                    return Ok(PerlValue::integer(if exists { 1 } else { 0 }));
11147                }
11148                let val = self.eval_expr(expr)?;
11149                Ok(PerlValue::integer(if val.is_undef() { 0 } else { 1 }))
11150            }
11151            ExprKind::Ref(expr) => {
11152                let val = self.eval_expr(expr)?;
11153                Ok(val.ref_type())
11154            }
11155            ExprKind::ScalarContext(expr) => {
11156                let v = self.eval_expr_ctx(expr, WantarrayCtx::Scalar)?;
11157                Ok(v.scalar_context())
11158            }
11159
11160            // Char
11161            ExprKind::Chr(expr) => {
11162                let n = self.eval_expr(expr)?.to_int() as u32;
11163                Ok(PerlValue::string(
11164                    char::from_u32(n).map(|c| c.to_string()).unwrap_or_default(),
11165                ))
11166            }
11167            ExprKind::Ord(expr) => {
11168                let s = self.eval_expr(expr)?.to_string();
11169                Ok(PerlValue::integer(
11170                    s.chars().next().map(|c| c as i64).unwrap_or(0),
11171                ))
11172            }
11173
11174            // I/O
11175            ExprKind::OpenMyHandle { .. } => Err(PerlError::runtime(
11176                "internal: `open my $fh` handle used outside open()",
11177                line,
11178            )
11179            .into()),
11180            ExprKind::Open { handle, mode, file } => {
11181                if let ExprKind::OpenMyHandle { name } = &handle.kind {
11182                    self.scope
11183                        .declare_scalar_frozen(name, PerlValue::UNDEF, false, None)?;
11184                    self.english_note_lexical_scalar(name);
11185                    let mode_s = self.eval_expr(mode)?.to_string();
11186                    let file_opt = if let Some(f) = file {
11187                        Some(self.eval_expr(f)?.to_string())
11188                    } else {
11189                        None
11190                    };
11191                    let ret = self.open_builtin_execute(name.clone(), mode_s, file_opt, line)?;
11192                    self.scope.set_scalar(name, ret.clone())?;
11193                    return Ok(ret);
11194                }
11195                let handle_s = self.eval_expr(handle)?.to_string();
11196                let handle_name = self.resolve_io_handle_name(&handle_s);
11197                let mode_s = self.eval_expr(mode)?.to_string();
11198                let file_opt = if let Some(f) = file {
11199                    Some(self.eval_expr(f)?.to_string())
11200                } else {
11201                    None
11202                };
11203                self.open_builtin_execute(handle_name, mode_s, file_opt, line)
11204                    .map_err(Into::into)
11205            }
11206            ExprKind::Close(expr) => {
11207                let s = self.eval_expr(expr)?.to_string();
11208                let name = self.resolve_io_handle_name(&s);
11209                self.close_builtin_execute(name).map_err(Into::into)
11210            }
11211            ExprKind::ReadLine(handle) => if ctx == WantarrayCtx::List {
11212                self.readline_builtin_execute_list(handle.as_deref())
11213            } else {
11214                self.readline_builtin_execute(handle.as_deref())
11215            }
11216            .map_err(Into::into),
11217            ExprKind::Eof(expr) => match expr {
11218                None => self.eof_builtin_execute(&[], line).map_err(Into::into),
11219                Some(e) => {
11220                    let name = self.eval_expr(e)?;
11221                    self.eof_builtin_execute(&[name], line).map_err(Into::into)
11222                }
11223            },
11224
11225            ExprKind::Opendir { handle, path } => {
11226                let h = self.eval_expr(handle)?.to_string();
11227                let p = self.eval_expr(path)?.to_string();
11228                Ok(self.opendir_handle(&h, &p))
11229            }
11230            ExprKind::Readdir(e) => {
11231                let h = self.eval_expr(e)?.to_string();
11232                Ok(if ctx == WantarrayCtx::List {
11233                    self.readdir_handle_list(&h)
11234                } else {
11235                    self.readdir_handle(&h)
11236                })
11237            }
11238            ExprKind::Closedir(e) => {
11239                let h = self.eval_expr(e)?.to_string();
11240                Ok(self.closedir_handle(&h))
11241            }
11242            ExprKind::Rewinddir(e) => {
11243                let h = self.eval_expr(e)?.to_string();
11244                Ok(self.rewinddir_handle(&h))
11245            }
11246            ExprKind::Telldir(e) => {
11247                let h = self.eval_expr(e)?.to_string();
11248                Ok(self.telldir_handle(&h))
11249            }
11250            ExprKind::Seekdir { handle, position } => {
11251                let h = self.eval_expr(handle)?.to_string();
11252                let pos = self.eval_expr(position)?.to_int().max(0) as usize;
11253                Ok(self.seekdir_handle(&h, pos))
11254            }
11255
11256            // File tests
11257            ExprKind::FileTest { op, expr } => {
11258                let path = self.eval_expr(expr)?.to_string();
11259                // -M, -A, -C return fractional days (float), not boolean
11260                if matches!(op, 'M' | 'A' | 'C') {
11261                    #[cfg(unix)]
11262                    {
11263                        return match crate::perl_fs::filetest_age_days(&path, *op) {
11264                            Some(days) => Ok(PerlValue::float(days)),
11265                            None => Ok(PerlValue::UNDEF),
11266                        };
11267                    }
11268                    #[cfg(not(unix))]
11269                    return Ok(PerlValue::UNDEF);
11270                }
11271                // -s returns file size (or undef on error)
11272                if *op == 's' {
11273                    return match std::fs::metadata(&path) {
11274                        Ok(m) => Ok(PerlValue::integer(m.len() as i64)),
11275                        Err(_) => Ok(PerlValue::UNDEF),
11276                    };
11277                }
11278                let result = match op {
11279                    'e' => std::path::Path::new(&path).exists(),
11280                    'f' => std::path::Path::new(&path).is_file(),
11281                    'd' => std::path::Path::new(&path).is_dir(),
11282                    'l' => std::path::Path::new(&path).is_symlink(),
11283                    #[cfg(unix)]
11284                    'r' => crate::perl_fs::filetest_effective_access(&path, 4),
11285                    #[cfg(not(unix))]
11286                    'r' => std::fs::metadata(&path).is_ok(),
11287                    #[cfg(unix)]
11288                    'w' => crate::perl_fs::filetest_effective_access(&path, 2),
11289                    #[cfg(not(unix))]
11290                    'w' => std::fs::metadata(&path).is_ok(),
11291                    #[cfg(unix)]
11292                    'x' => crate::perl_fs::filetest_effective_access(&path, 1),
11293                    #[cfg(not(unix))]
11294                    'x' => false,
11295                    #[cfg(unix)]
11296                    'o' => crate::perl_fs::filetest_owned_effective(&path),
11297                    #[cfg(not(unix))]
11298                    'o' => false,
11299                    #[cfg(unix)]
11300                    'R' => crate::perl_fs::filetest_real_access(&path, libc::R_OK),
11301                    #[cfg(not(unix))]
11302                    'R' => false,
11303                    #[cfg(unix)]
11304                    'W' => crate::perl_fs::filetest_real_access(&path, libc::W_OK),
11305                    #[cfg(not(unix))]
11306                    'W' => false,
11307                    #[cfg(unix)]
11308                    'X' => crate::perl_fs::filetest_real_access(&path, libc::X_OK),
11309                    #[cfg(not(unix))]
11310                    'X' => false,
11311                    #[cfg(unix)]
11312                    'O' => crate::perl_fs::filetest_owned_real(&path),
11313                    #[cfg(not(unix))]
11314                    'O' => false,
11315                    'z' => std::fs::metadata(&path)
11316                        .map(|m| m.len() == 0)
11317                        .unwrap_or(true),
11318                    't' => crate::perl_fs::filetest_is_tty(&path),
11319                    #[cfg(unix)]
11320                    'p' => crate::perl_fs::filetest_is_pipe(&path),
11321                    #[cfg(not(unix))]
11322                    'p' => false,
11323                    #[cfg(unix)]
11324                    'S' => crate::perl_fs::filetest_is_socket(&path),
11325                    #[cfg(not(unix))]
11326                    'S' => false,
11327                    #[cfg(unix)]
11328                    'b' => crate::perl_fs::filetest_is_block_device(&path),
11329                    #[cfg(not(unix))]
11330                    'b' => false,
11331                    #[cfg(unix)]
11332                    'c' => crate::perl_fs::filetest_is_char_device(&path),
11333                    #[cfg(not(unix))]
11334                    'c' => false,
11335                    #[cfg(unix)]
11336                    'u' => crate::perl_fs::filetest_is_setuid(&path),
11337                    #[cfg(not(unix))]
11338                    'u' => false,
11339                    #[cfg(unix)]
11340                    'g' => crate::perl_fs::filetest_is_setgid(&path),
11341                    #[cfg(not(unix))]
11342                    'g' => false,
11343                    #[cfg(unix)]
11344                    'k' => crate::perl_fs::filetest_is_sticky(&path),
11345                    #[cfg(not(unix))]
11346                    'k' => false,
11347                    'T' => crate::perl_fs::filetest_is_text(&path),
11348                    'B' => crate::perl_fs::filetest_is_binary(&path),
11349                    _ => false,
11350                };
11351                Ok(PerlValue::integer(if result { 1 } else { 0 }))
11352            }
11353
11354            // System
11355            ExprKind::System(args) => {
11356                let mut cmd_args = Vec::new();
11357                for a in args {
11358                    cmd_args.push(self.eval_expr(a)?.to_string());
11359                }
11360                if cmd_args.is_empty() {
11361                    return Ok(PerlValue::integer(-1));
11362                }
11363                let status = Command::new("sh")
11364                    .arg("-c")
11365                    .arg(cmd_args.join(" "))
11366                    .status();
11367                match status {
11368                    Ok(s) => {
11369                        self.record_child_exit_status(s);
11370                        Ok(PerlValue::integer(s.code().unwrap_or(-1) as i64))
11371                    }
11372                    Err(e) => {
11373                        self.apply_io_error_to_errno(&e);
11374                        Ok(PerlValue::integer(-1))
11375                    }
11376                }
11377            }
11378            ExprKind::Exec(args) => {
11379                let mut cmd_args = Vec::new();
11380                for a in args {
11381                    cmd_args.push(self.eval_expr(a)?.to_string());
11382                }
11383                if cmd_args.is_empty() {
11384                    return Ok(PerlValue::integer(-1));
11385                }
11386                let status = Command::new("sh")
11387                    .arg("-c")
11388                    .arg(cmd_args.join(" "))
11389                    .status();
11390                match status {
11391                    Ok(s) => std::process::exit(s.code().unwrap_or(-1)),
11392                    Err(e) => {
11393                        self.apply_io_error_to_errno(&e);
11394                        Ok(PerlValue::integer(-1))
11395                    }
11396                }
11397            }
11398            ExprKind::Eval(expr) => {
11399                self.eval_nesting += 1;
11400                let out = match &expr.kind {
11401                    ExprKind::CodeRef { body, .. } => match self.exec_block_with_tail(body, ctx) {
11402                        Ok(v) => {
11403                            self.clear_eval_error();
11404                            Ok(v)
11405                        }
11406                        Err(FlowOrError::Error(e)) => {
11407                            self.set_eval_error_from_perl_error(&e);
11408                            Ok(PerlValue::UNDEF)
11409                        }
11410                        Err(FlowOrError::Flow(f)) => Err(FlowOrError::Flow(f)),
11411                    },
11412                    _ => {
11413                        let code = self.eval_expr(expr)?.to_string();
11414                        // Parse and execute the string as Perl code
11415                        match crate::parse_and_run_string(&code, self) {
11416                            Ok(v) => {
11417                                self.clear_eval_error();
11418                                Ok(v)
11419                            }
11420                            Err(e) => {
11421                                self.set_eval_error(e.to_string());
11422                                Ok(PerlValue::UNDEF)
11423                            }
11424                        }
11425                    }
11426                };
11427                self.eval_nesting -= 1;
11428                out
11429            }
11430            ExprKind::Do(expr) => match &expr.kind {
11431                ExprKind::CodeRef { body, .. } => self.exec_block_with_tail(body, ctx),
11432                _ => {
11433                    let val = self.eval_expr(expr)?;
11434                    let filename = val.to_string();
11435                    match read_file_text_perl_compat(&filename) {
11436                        Ok(code) => {
11437                            let code = crate::data_section::strip_perl_end_marker(&code);
11438                            match crate::parse_and_run_string_in_file(code, self, &filename) {
11439                                Ok(v) => Ok(v),
11440                                Err(e) => {
11441                                    self.set_eval_error(e.to_string());
11442                                    Ok(PerlValue::UNDEF)
11443                                }
11444                            }
11445                        }
11446                        Err(e) => {
11447                            self.apply_io_error_to_errno(&e);
11448                            Ok(PerlValue::UNDEF)
11449                        }
11450                    }
11451                }
11452            },
11453            ExprKind::Require(expr) => {
11454                let spec = self.eval_expr(expr)?.to_string();
11455                self.require_execute(&spec, line)
11456                    .map_err(FlowOrError::Error)
11457            }
11458            ExprKind::Exit(code) => {
11459                let c = if let Some(e) = code {
11460                    self.eval_expr(e)?.to_int() as i32
11461                } else {
11462                    0
11463                };
11464                Err(PerlError::new(ErrorKind::Exit(c), "", line, &self.file).into())
11465            }
11466            ExprKind::Chdir(expr) => {
11467                let path = self.eval_expr(expr)?.to_string();
11468                match std::env::set_current_dir(&path) {
11469                    Ok(_) => Ok(PerlValue::integer(1)),
11470                    Err(e) => {
11471                        self.apply_io_error_to_errno(&e);
11472                        Ok(PerlValue::integer(0))
11473                    }
11474                }
11475            }
11476            ExprKind::Mkdir { path, mode: _ } => {
11477                let p = self.eval_expr(path)?.to_string();
11478                match std::fs::create_dir(&p) {
11479                    Ok(_) => Ok(PerlValue::integer(1)),
11480                    Err(e) => {
11481                        self.apply_io_error_to_errno(&e);
11482                        Ok(PerlValue::integer(0))
11483                    }
11484                }
11485            }
11486            ExprKind::Unlink(args) => {
11487                let mut count = 0i64;
11488                for a in args {
11489                    let path = self.eval_expr(a)?.to_string();
11490                    if std::fs::remove_file(&path).is_ok() {
11491                        count += 1;
11492                    }
11493                }
11494                Ok(PerlValue::integer(count))
11495            }
11496            ExprKind::Rename { old, new } => {
11497                let o = self.eval_expr(old)?.to_string();
11498                let n = self.eval_expr(new)?.to_string();
11499                Ok(crate::perl_fs::rename_paths(&o, &n))
11500            }
11501            ExprKind::Chmod(args) => {
11502                let mode = self.eval_expr(&args[0])?.to_int();
11503                let mut paths = Vec::new();
11504                for a in &args[1..] {
11505                    paths.push(self.eval_expr(a)?.to_string());
11506                }
11507                Ok(PerlValue::integer(crate::perl_fs::chmod_paths(
11508                    &paths, mode,
11509                )))
11510            }
11511            ExprKind::Chown(args) => {
11512                let uid = self.eval_expr(&args[0])?.to_int();
11513                let gid = self.eval_expr(&args[1])?.to_int();
11514                let mut paths = Vec::new();
11515                for a in &args[2..] {
11516                    paths.push(self.eval_expr(a)?.to_string());
11517                }
11518                Ok(PerlValue::integer(crate::perl_fs::chown_paths(
11519                    &paths, uid, gid,
11520                )))
11521            }
11522            ExprKind::Stat(e) => {
11523                let path = self.eval_expr(e)?.to_string();
11524                Ok(crate::perl_fs::stat_path(&path, false))
11525            }
11526            ExprKind::Lstat(e) => {
11527                let path = self.eval_expr(e)?.to_string();
11528                Ok(crate::perl_fs::stat_path(&path, true))
11529            }
11530            ExprKind::Link { old, new } => {
11531                let o = self.eval_expr(old)?.to_string();
11532                let n = self.eval_expr(new)?.to_string();
11533                Ok(crate::perl_fs::link_hard(&o, &n))
11534            }
11535            ExprKind::Symlink { old, new } => {
11536                let o = self.eval_expr(old)?.to_string();
11537                let n = self.eval_expr(new)?.to_string();
11538                Ok(crate::perl_fs::link_sym(&o, &n))
11539            }
11540            ExprKind::Readlink(e) => {
11541                let path = self.eval_expr(e)?.to_string();
11542                Ok(crate::perl_fs::read_link(&path))
11543            }
11544            ExprKind::Files(args) => {
11545                let dir = if args.is_empty() {
11546                    ".".to_string()
11547                } else {
11548                    self.eval_expr(&args[0])?.to_string()
11549                };
11550                Ok(crate::perl_fs::list_files(&dir))
11551            }
11552            ExprKind::Filesf(args) => {
11553                let dir = if args.is_empty() {
11554                    ".".to_string()
11555                } else {
11556                    self.eval_expr(&args[0])?.to_string()
11557                };
11558                Ok(crate::perl_fs::list_filesf(&dir))
11559            }
11560            ExprKind::FilesfRecursive(args) => {
11561                let dir = if args.is_empty() {
11562                    ".".to_string()
11563                } else {
11564                    self.eval_expr(&args[0])?.to_string()
11565                };
11566                Ok(PerlValue::iterator(Arc::new(
11567                    crate::value::FsWalkIterator::new(&dir, true),
11568                )))
11569            }
11570            ExprKind::Dirs(args) => {
11571                let dir = if args.is_empty() {
11572                    ".".to_string()
11573                } else {
11574                    self.eval_expr(&args[0])?.to_string()
11575                };
11576                Ok(crate::perl_fs::list_dirs(&dir))
11577            }
11578            ExprKind::DirsRecursive(args) => {
11579                let dir = if args.is_empty() {
11580                    ".".to_string()
11581                } else {
11582                    self.eval_expr(&args[0])?.to_string()
11583                };
11584                Ok(PerlValue::iterator(Arc::new(
11585                    crate::value::FsWalkIterator::new(&dir, false),
11586                )))
11587            }
11588            ExprKind::SymLinks(args) => {
11589                let dir = if args.is_empty() {
11590                    ".".to_string()
11591                } else {
11592                    self.eval_expr(&args[0])?.to_string()
11593                };
11594                Ok(crate::perl_fs::list_sym_links(&dir))
11595            }
11596            ExprKind::Sockets(args) => {
11597                let dir = if args.is_empty() {
11598                    ".".to_string()
11599                } else {
11600                    self.eval_expr(&args[0])?.to_string()
11601                };
11602                Ok(crate::perl_fs::list_sockets(&dir))
11603            }
11604            ExprKind::Pipes(args) => {
11605                let dir = if args.is_empty() {
11606                    ".".to_string()
11607                } else {
11608                    self.eval_expr(&args[0])?.to_string()
11609                };
11610                Ok(crate::perl_fs::list_pipes(&dir))
11611            }
11612            ExprKind::BlockDevices(args) => {
11613                let dir = if args.is_empty() {
11614                    ".".to_string()
11615                } else {
11616                    self.eval_expr(&args[0])?.to_string()
11617                };
11618                Ok(crate::perl_fs::list_block_devices(&dir))
11619            }
11620            ExprKind::CharDevices(args) => {
11621                let dir = if args.is_empty() {
11622                    ".".to_string()
11623                } else {
11624                    self.eval_expr(&args[0])?.to_string()
11625                };
11626                Ok(crate::perl_fs::list_char_devices(&dir))
11627            }
11628            ExprKind::Glob(args) => {
11629                let mut pats = Vec::new();
11630                for a in args {
11631                    pats.push(self.eval_expr(a)?.to_string());
11632                }
11633                Ok(crate::perl_fs::glob_patterns(&pats))
11634            }
11635            ExprKind::GlobPar { args, progress } => {
11636                let mut pats = Vec::new();
11637                for a in args {
11638                    pats.push(self.eval_expr(a)?.to_string());
11639                }
11640                let show_progress = progress
11641                    .as_ref()
11642                    .map(|p| self.eval_expr(p))
11643                    .transpose()?
11644                    .map(|v| v.is_true())
11645                    .unwrap_or(false);
11646                if show_progress {
11647                    Ok(crate::perl_fs::glob_par_patterns_with_progress(&pats, true))
11648                } else {
11649                    Ok(crate::perl_fs::glob_par_patterns(&pats))
11650                }
11651            }
11652            ExprKind::ParSed { args, progress } => {
11653                let has_progress = progress.is_some();
11654                let mut vals: Vec<PerlValue> = Vec::new();
11655                for a in args {
11656                    vals.push(self.eval_expr(a)?);
11657                }
11658                if let Some(p) = progress {
11659                    vals.push(self.eval_expr(p.as_ref())?);
11660                }
11661                Ok(self.builtin_par_sed(&vals, line, has_progress)?)
11662            }
11663            ExprKind::Bless { ref_expr, class } => {
11664                let val = self.eval_expr(ref_expr)?;
11665                let class_name = if let Some(c) = class {
11666                    self.eval_expr(c)?.to_string()
11667                } else {
11668                    self.scope.get_scalar("__PACKAGE__").to_string()
11669                };
11670                Ok(PerlValue::blessed(Arc::new(
11671                    crate::value::BlessedRef::new_blessed(class_name, val),
11672                )))
11673            }
11674            ExprKind::Caller(_) => {
11675                // Simplified: return package, file, line
11676                Ok(PerlValue::array(vec![
11677                    PerlValue::string("main".into()),
11678                    PerlValue::string(self.file.clone()),
11679                    PerlValue::integer(line as i64),
11680                ]))
11681            }
11682            ExprKind::Wantarray => Ok(match self.wantarray_kind {
11683                WantarrayCtx::Void => PerlValue::UNDEF,
11684                WantarrayCtx::Scalar => PerlValue::integer(0),
11685                WantarrayCtx::List => PerlValue::integer(1),
11686            }),
11687
11688            ExprKind::List(exprs) => {
11689                // In scalar context, the comma operator evaluates to the last element.
11690                if ctx == WantarrayCtx::Scalar {
11691                    if let Some(last) = exprs.last() {
11692                        // Evaluate earlier expressions for side effects
11693                        for e in &exprs[..exprs.len() - 1] {
11694                            self.eval_expr(e)?;
11695                        }
11696                        return self.eval_expr(last);
11697                    } else {
11698                        return Ok(PerlValue::UNDEF);
11699                    }
11700                }
11701                let mut vals = Vec::new();
11702                for e in exprs {
11703                    let v = self.eval_expr_ctx(e, WantarrayCtx::List)?;
11704                    if let Some(items) = v.as_array_vec() {
11705                        vals.extend(items);
11706                    } else {
11707                        vals.push(v);
11708                    }
11709                }
11710                if vals.len() == 1 {
11711                    Ok(vals.pop().unwrap())
11712                } else {
11713                    Ok(PerlValue::array(vals))
11714                }
11715            }
11716
11717            // Postfix modifiers
11718            ExprKind::PostfixIf { expr, condition } => {
11719                if self.eval_postfix_condition(condition)? {
11720                    self.eval_expr(expr)
11721                } else {
11722                    Ok(PerlValue::UNDEF)
11723                }
11724            }
11725            ExprKind::PostfixUnless { expr, condition } => {
11726                if !self.eval_postfix_condition(condition)? {
11727                    self.eval_expr(expr)
11728                } else {
11729                    Ok(PerlValue::UNDEF)
11730                }
11731            }
11732            ExprKind::PostfixWhile { expr, condition } => {
11733                // `do { ... } while (COND)` — body runs before the first condition check.
11734                // Parsed as PostfixWhile(Do(CodeRef), cond), not plain postfix-while.
11735                let is_do_block = matches!(
11736                    &expr.kind,
11737                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
11738                );
11739                let mut last = PerlValue::UNDEF;
11740                if is_do_block {
11741                    loop {
11742                        last = self.eval_expr(expr)?;
11743                        if !self.eval_postfix_condition(condition)? {
11744                            break;
11745                        }
11746                    }
11747                } else {
11748                    loop {
11749                        if !self.eval_postfix_condition(condition)? {
11750                            break;
11751                        }
11752                        last = self.eval_expr(expr)?;
11753                    }
11754                }
11755                Ok(last)
11756            }
11757            ExprKind::PostfixUntil { expr, condition } => {
11758                let is_do_block = matches!(
11759                    &expr.kind,
11760                    ExprKind::Do(inner) if matches!(inner.kind, ExprKind::CodeRef { .. })
11761                );
11762                let mut last = PerlValue::UNDEF;
11763                if is_do_block {
11764                    loop {
11765                        last = self.eval_expr(expr)?;
11766                        if self.eval_postfix_condition(condition)? {
11767                            break;
11768                        }
11769                    }
11770                } else {
11771                    loop {
11772                        if self.eval_postfix_condition(condition)? {
11773                            break;
11774                        }
11775                        last = self.eval_expr(expr)?;
11776                    }
11777                }
11778                Ok(last)
11779            }
11780            ExprKind::PostfixForeach { expr, list } => {
11781                let items = self.eval_expr_ctx(list, WantarrayCtx::List)?.to_list();
11782                let mut last = PerlValue::UNDEF;
11783                for item in items {
11784                    self.scope.set_topic(item);
11785                    last = self.eval_expr(expr)?;
11786                }
11787                Ok(last)
11788            }
11789        }
11790    }
11791
11792    // ── Helpers ──
11793
11794    fn overload_key_for_binop(op: BinOp) -> Option<&'static str> {
11795        match op {
11796            BinOp::Add => Some("+"),
11797            BinOp::Sub => Some("-"),
11798            BinOp::Mul => Some("*"),
11799            BinOp::Div => Some("/"),
11800            BinOp::Mod => Some("%"),
11801            BinOp::Pow => Some("**"),
11802            BinOp::Concat => Some("."),
11803            BinOp::StrEq => Some("eq"),
11804            BinOp::NumEq => Some("=="),
11805            BinOp::StrNe => Some("ne"),
11806            BinOp::NumNe => Some("!="),
11807            BinOp::StrLt => Some("lt"),
11808            BinOp::StrGt => Some("gt"),
11809            BinOp::StrLe => Some("le"),
11810            BinOp::StrGe => Some("ge"),
11811            BinOp::NumLt => Some("<"),
11812            BinOp::NumGt => Some(">"),
11813            BinOp::NumLe => Some("<="),
11814            BinOp::NumGe => Some(">="),
11815            BinOp::Spaceship => Some("<=>"),
11816            BinOp::StrCmp => Some("cmp"),
11817            _ => None,
11818        }
11819    }
11820
11821    /// Perl `use overload '""' => ...` — key is `""` (empty) or `""` (two `"` chars from `'""'`).
11822    fn overload_stringify_method(map: &HashMap<String, String>) -> Option<&String> {
11823        map.get("").or_else(|| map.get("\"\""))
11824    }
11825
11826    /// String context for blessed objects with `overload '""'`.
11827    pub(crate) fn stringify_value(
11828        &mut self,
11829        v: PerlValue,
11830        line: usize,
11831    ) -> Result<String, FlowOrError> {
11832        if let Some(r) = self.try_overload_stringify(&v, line) {
11833            let pv = r?;
11834            return Ok(pv.to_string());
11835        }
11836        Ok(v.to_string())
11837    }
11838
11839    /// Like Perl `sprintf`, but `%s` uses [`stringify_value`] so `overload ""` applies.
11840    pub(crate) fn perl_sprintf_stringify(
11841        &mut self,
11842        fmt: &str,
11843        args: &[PerlValue],
11844        line: usize,
11845    ) -> Result<String, FlowOrError> {
11846        perl_sprintf_format_with(fmt, args, |v| self.stringify_value(v.clone(), line))
11847    }
11848
11849    /// Expand a compiled [`crate::format::FormatTemplate`] using current expression evaluation.
11850    pub(crate) fn render_format_template(
11851        &mut self,
11852        tmpl: &crate::format::FormatTemplate,
11853        line: usize,
11854    ) -> Result<String, FlowOrError> {
11855        use crate::format::{FormatRecord, PictureSegment};
11856        let mut buf = String::new();
11857        for rec in &tmpl.records {
11858            match rec {
11859                FormatRecord::Literal(s) => {
11860                    buf.push_str(s);
11861                    buf.push('\n');
11862                }
11863                FormatRecord::Picture { segments, exprs } => {
11864                    let mut vals: Vec<String> = Vec::new();
11865                    for e in exprs {
11866                        let v = self.eval_expr(e)?;
11867                        vals.push(self.stringify_value(v, line)?);
11868                    }
11869                    let mut vi = 0usize;
11870                    let mut line_out = String::new();
11871                    for seg in segments {
11872                        match seg {
11873                            PictureSegment::Literal(t) => line_out.push_str(t),
11874                            PictureSegment::Field {
11875                                width,
11876                                align,
11877                                kind: _,
11878                            } => {
11879                                let s = vals.get(vi).map(|s| s.as_str()).unwrap_or("");
11880                                vi += 1;
11881                                line_out.push_str(&crate::format::pad_field(s, *width, *align));
11882                            }
11883                        }
11884                    }
11885                    buf.push_str(line_out.trim_end());
11886                    buf.push('\n');
11887                }
11888            }
11889        }
11890        Ok(buf)
11891    }
11892
11893    /// Resolve `write FH` / `write $fh` — same handle shapes as `$fh->print` ([`Self::try_native_method`]).
11894    pub(crate) fn resolve_write_output_handle(
11895        &self,
11896        v: &PerlValue,
11897        line: usize,
11898    ) -> PerlResult<String> {
11899        if let Some(n) = v.as_io_handle_name() {
11900            let n = self.resolve_io_handle_name(&n);
11901            if self.is_bound_handle(&n) {
11902                return Ok(n);
11903            }
11904        }
11905        if let Some(s) = v.as_str() {
11906            if self.is_bound_handle(&s) {
11907                return Ok(self.resolve_io_handle_name(&s));
11908            }
11909        }
11910        let s = v.to_string();
11911        if self.is_bound_handle(&s) {
11912            return Ok(self.resolve_io_handle_name(&s));
11913        }
11914        Err(PerlError::runtime(
11915            format!("write: invalid or unopened filehandle {}", s),
11916            line,
11917        ))
11918    }
11919
11920    /// `write` — output one record using `$~` format name in the current package (subset of Perl).
11921    /// With no args, uses [`Self::default_print_handle`] (Perl `select`); with one arg, writes to
11922    /// that handle like `write FH`.
11923    pub(crate) fn write_format_execute(
11924        &mut self,
11925        args: &[PerlValue],
11926        line: usize,
11927    ) -> PerlResult<PerlValue> {
11928        let handle_name = match args.len() {
11929            0 => self.default_print_handle.clone(),
11930            1 => self.resolve_write_output_handle(&args[0], line)?,
11931            _ => {
11932                return Err(PerlError::runtime("write: too many arguments", line));
11933            }
11934        };
11935        let pkg = self.current_package();
11936        let mut fmt_name = self.scope.get_scalar("~").to_string();
11937        if fmt_name.is_empty() {
11938            fmt_name = "STDOUT".to_string();
11939        }
11940        let key = format!("{}::{}", pkg, fmt_name);
11941        let tmpl = self
11942            .format_templates
11943            .get(&key)
11944            .map(Arc::clone)
11945            .ok_or_else(|| {
11946                PerlError::runtime(
11947                    format!("Unknown format `{}` in package `{}`", fmt_name, pkg),
11948                    line,
11949                )
11950            })?;
11951        let out = self
11952            .render_format_template(&tmpl, line)
11953            .map_err(|e| match e {
11954                FlowOrError::Error(e) => e,
11955                FlowOrError::Flow(_) => PerlError::runtime("write: unexpected control flow", line),
11956            })?;
11957        self.write_formatted_print(handle_name.as_str(), &out, line)?;
11958        Ok(PerlValue::integer(1))
11959    }
11960
11961    pub(crate) fn try_overload_stringify(
11962        &mut self,
11963        v: &PerlValue,
11964        line: usize,
11965    ) -> Option<ExecResult> {
11966        // Native class instance: look for method named '""' or 'stringify'
11967        if let Some(c) = v.as_class_inst() {
11968            let method_name = c
11969                .def
11970                .method("stringify")
11971                .or_else(|| c.def.method("\"\""))
11972                .filter(|m| m.body.is_some())?;
11973            let body = method_name.body.clone().unwrap();
11974            let params = method_name.params.clone();
11975            return Some(self.call_class_method(&body, &params, vec![v.clone()], line));
11976        }
11977        let br = v.as_blessed_ref()?;
11978        let class = br.class.clone();
11979        let map = self.overload_table.get(&class)?;
11980        let sub_short = Self::overload_stringify_method(map)?;
11981        let fq = format!("{}::{}", class, sub_short);
11982        let sub = self.subs.get(&fq)?.clone();
11983        Some(self.call_sub(&sub, vec![v.clone()], WantarrayCtx::Scalar, line))
11984    }
11985
11986    /// Map overload operator key to native class method name.
11987    fn overload_method_name_for_key(key: &str) -> Option<&'static str> {
11988        match key {
11989            "+" => Some("op_add"),
11990            "-" => Some("op_sub"),
11991            "*" => Some("op_mul"),
11992            "/" => Some("op_div"),
11993            "%" => Some("op_mod"),
11994            "**" => Some("op_pow"),
11995            "." => Some("op_concat"),
11996            "==" => Some("op_eq"),
11997            "!=" => Some("op_ne"),
11998            "<" => Some("op_lt"),
11999            ">" => Some("op_gt"),
12000            "<=" => Some("op_le"),
12001            ">=" => Some("op_ge"),
12002            "<=>" => Some("op_spaceship"),
12003            "eq" => Some("op_str_eq"),
12004            "ne" => Some("op_str_ne"),
12005            "lt" => Some("op_str_lt"),
12006            "gt" => Some("op_str_gt"),
12007            "le" => Some("op_str_le"),
12008            "ge" => Some("op_str_ge"),
12009            "cmp" => Some("op_cmp"),
12010            _ => None,
12011        }
12012    }
12013
12014    pub(crate) fn try_overload_binop(
12015        &mut self,
12016        op: BinOp,
12017        lv: &PerlValue,
12018        rv: &PerlValue,
12019        line: usize,
12020    ) -> Option<ExecResult> {
12021        let key = Self::overload_key_for_binop(op)?;
12022        // Native class instance overloading
12023        let (ci_def, invocant, other) = if let Some(c) = lv.as_class_inst() {
12024            (Some(c.def.clone()), lv.clone(), rv.clone())
12025        } else if let Some(c) = rv.as_class_inst() {
12026            (Some(c.def.clone()), rv.clone(), lv.clone())
12027        } else {
12028            (None, lv.clone(), rv.clone())
12029        };
12030        if let Some(ref def) = ci_def {
12031            if let Some(method_name) = Self::overload_method_name_for_key(key) {
12032                if let Some((m, _)) = self.find_class_method(def, method_name) {
12033                    if let Some(ref body) = m.body {
12034                        let params = m.params.clone();
12035                        return Some(self.call_class_method(
12036                            body,
12037                            &params,
12038                            vec![invocant, other],
12039                            line,
12040                        ));
12041                    }
12042                }
12043            }
12044        }
12045        // Blessed ref overloading (existing path)
12046        let (class, invocant, other) = if let Some(br) = lv.as_blessed_ref() {
12047            (br.class.clone(), lv.clone(), rv.clone())
12048        } else if let Some(br) = rv.as_blessed_ref() {
12049            (br.class.clone(), rv.clone(), lv.clone())
12050        } else {
12051            return None;
12052        };
12053        let map = self.overload_table.get(&class)?;
12054        let sub_short = if let Some(s) = map.get(key) {
12055            s.clone()
12056        } else if let Some(nm) = map.get("nomethod") {
12057            let fq = format!("{}::{}", class, nm);
12058            let sub = self.subs.get(&fq)?.clone();
12059            return Some(self.call_sub(
12060                &sub,
12061                vec![invocant, other, PerlValue::string(key.to_string())],
12062                WantarrayCtx::Scalar,
12063                line,
12064            ));
12065        } else {
12066            return None;
12067        };
12068        let fq = format!("{}::{}", class, sub_short);
12069        let sub = self.subs.get(&fq)?.clone();
12070        Some(self.call_sub(&sub, vec![invocant, other], WantarrayCtx::Scalar, line))
12071    }
12072
12073    /// Unary overload: keys `neg`, `bool`, `abs`, `0+`, … — or `nomethod` with `(invocant, op_key)`.
12074    pub(crate) fn try_overload_unary_dispatch(
12075        &mut self,
12076        op_key: &str,
12077        val: &PerlValue,
12078        line: usize,
12079    ) -> Option<ExecResult> {
12080        // Native class instance: look for op_neg, op_bool, op_abs, op_numify
12081        if let Some(c) = val.as_class_inst() {
12082            let method_name = match op_key {
12083                "neg" => "op_neg",
12084                "bool" => "op_bool",
12085                "abs" => "op_abs",
12086                "0+" => "op_numify",
12087                _ => return None,
12088            };
12089            if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
12090                if let Some(ref body) = m.body {
12091                    let params = m.params.clone();
12092                    return Some(self.call_class_method(body, &params, vec![val.clone()], line));
12093                }
12094            }
12095            return None;
12096        }
12097        // Blessed ref path
12098        let br = val.as_blessed_ref()?;
12099        let class = br.class.clone();
12100        let map = self.overload_table.get(&class)?;
12101        if let Some(s) = map.get(op_key) {
12102            let fq = format!("{}::{}", class, s);
12103            let sub = self.subs.get(&fq)?.clone();
12104            return Some(self.call_sub(&sub, vec![val.clone()], WantarrayCtx::Scalar, line));
12105        }
12106        if let Some(nm) = map.get("nomethod") {
12107            let fq = format!("{}::{}", class, nm);
12108            let sub = self.subs.get(&fq)?.clone();
12109            return Some(self.call_sub(
12110                &sub,
12111                vec![val.clone(), PerlValue::string(op_key.to_string())],
12112                WantarrayCtx::Scalar,
12113                line,
12114            ));
12115        }
12116        None
12117    }
12118
12119    #[inline]
12120    fn eval_binop(
12121        &mut self,
12122        op: BinOp,
12123        lv: &PerlValue,
12124        rv: &PerlValue,
12125        _line: usize,
12126    ) -> ExecResult {
12127        Ok(match op {
12128            // ── Integer fast paths: avoid f64 conversion when both operands are i64 ──
12129            // Perl `+` is numeric addition only; string concatenation is `.`.
12130            BinOp::Add => {
12131                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12132                    PerlValue::integer(a.wrapping_add(b))
12133                } else {
12134                    PerlValue::float(lv.to_number() + rv.to_number())
12135                }
12136            }
12137            BinOp::Sub => {
12138                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12139                    PerlValue::integer(a.wrapping_sub(b))
12140                } else {
12141                    PerlValue::float(lv.to_number() - rv.to_number())
12142                }
12143            }
12144            BinOp::Mul => {
12145                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12146                    PerlValue::integer(a.wrapping_mul(b))
12147                } else {
12148                    PerlValue::float(lv.to_number() * rv.to_number())
12149                }
12150            }
12151            BinOp::Div => {
12152                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12153                    if b == 0 {
12154                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
12155                    }
12156                    if a % b == 0 {
12157                        PerlValue::integer(a / b)
12158                    } else {
12159                        PerlValue::float(a as f64 / b as f64)
12160                    }
12161                } else {
12162                    let d = rv.to_number();
12163                    if d == 0.0 {
12164                        return Err(PerlError::runtime("Illegal division by zero", _line).into());
12165                    }
12166                    PerlValue::float(lv.to_number() / d)
12167                }
12168            }
12169            BinOp::Mod => {
12170                let d = rv.to_int();
12171                if d == 0 {
12172                    return Err(PerlError::runtime("Illegal modulus zero", _line).into());
12173                }
12174                PerlValue::integer(lv.to_int() % d)
12175            }
12176            BinOp::Pow => {
12177                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12178                    let int_pow = (b >= 0)
12179                        .then(|| u32::try_from(b).ok())
12180                        .flatten()
12181                        .and_then(|bu| a.checked_pow(bu))
12182                        .map(PerlValue::integer);
12183                    int_pow.unwrap_or_else(|| PerlValue::float(lv.to_number().powf(rv.to_number())))
12184                } else {
12185                    PerlValue::float(lv.to_number().powf(rv.to_number()))
12186                }
12187            }
12188            BinOp::Concat => {
12189                let mut s = String::new();
12190                lv.append_to(&mut s);
12191                rv.append_to(&mut s);
12192                PerlValue::string(s)
12193            }
12194            BinOp::NumEq => {
12195                // Struct equality: compare all fields
12196                if let (Some(a), Some(b)) = (lv.as_struct_inst(), rv.as_struct_inst()) {
12197                    if a.def.name != b.def.name {
12198                        PerlValue::integer(0)
12199                    } else {
12200                        let av = a.get_values();
12201                        let bv = b.get_values();
12202                        let eq = av.len() == bv.len()
12203                            && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y));
12204                        PerlValue::integer(if eq { 1 } else { 0 })
12205                    }
12206                } else if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12207                    PerlValue::integer(if a == b { 1 } else { 0 })
12208                } else {
12209                    PerlValue::integer(if lv.to_number() == rv.to_number() {
12210                        1
12211                    } else {
12212                        0
12213                    })
12214                }
12215            }
12216            BinOp::NumNe => {
12217                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12218                    PerlValue::integer(if a != b { 1 } else { 0 })
12219                } else {
12220                    PerlValue::integer(if lv.to_number() != rv.to_number() {
12221                        1
12222                    } else {
12223                        0
12224                    })
12225                }
12226            }
12227            BinOp::NumLt => {
12228                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12229                    PerlValue::integer(if a < b { 1 } else { 0 })
12230                } else {
12231                    PerlValue::integer(if lv.to_number() < rv.to_number() {
12232                        1
12233                    } else {
12234                        0
12235                    })
12236                }
12237            }
12238            BinOp::NumGt => {
12239                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12240                    PerlValue::integer(if a > b { 1 } else { 0 })
12241                } else {
12242                    PerlValue::integer(if lv.to_number() > rv.to_number() {
12243                        1
12244                    } else {
12245                        0
12246                    })
12247                }
12248            }
12249            BinOp::NumLe => {
12250                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12251                    PerlValue::integer(if a <= b { 1 } else { 0 })
12252                } else {
12253                    PerlValue::integer(if lv.to_number() <= rv.to_number() {
12254                        1
12255                    } else {
12256                        0
12257                    })
12258                }
12259            }
12260            BinOp::NumGe => {
12261                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12262                    PerlValue::integer(if a >= b { 1 } else { 0 })
12263                } else {
12264                    PerlValue::integer(if lv.to_number() >= rv.to_number() {
12265                        1
12266                    } else {
12267                        0
12268                    })
12269                }
12270            }
12271            BinOp::Spaceship => {
12272                if let (Some(a), Some(b)) = (lv.as_integer(), rv.as_integer()) {
12273                    PerlValue::integer(if a < b {
12274                        -1
12275                    } else if a > b {
12276                        1
12277                    } else {
12278                        0
12279                    })
12280                } else {
12281                    let a = lv.to_number();
12282                    let b = rv.to_number();
12283                    PerlValue::integer(if a < b {
12284                        -1
12285                    } else if a > b {
12286                        1
12287                    } else {
12288                        0
12289                    })
12290                }
12291            }
12292            BinOp::StrEq => PerlValue::integer(if lv.to_string() == rv.to_string() {
12293                1
12294            } else {
12295                0
12296            }),
12297            BinOp::StrNe => PerlValue::integer(if lv.to_string() != rv.to_string() {
12298                1
12299            } else {
12300                0
12301            }),
12302            BinOp::StrLt => PerlValue::integer(if lv.to_string() < rv.to_string() {
12303                1
12304            } else {
12305                0
12306            }),
12307            BinOp::StrGt => PerlValue::integer(if lv.to_string() > rv.to_string() {
12308                1
12309            } else {
12310                0
12311            }),
12312            BinOp::StrLe => PerlValue::integer(if lv.to_string() <= rv.to_string() {
12313                1
12314            } else {
12315                0
12316            }),
12317            BinOp::StrGe => PerlValue::integer(if lv.to_string() >= rv.to_string() {
12318                1
12319            } else {
12320                0
12321            }),
12322            BinOp::StrCmp => {
12323                let cmp = lv.to_string().cmp(&rv.to_string());
12324                PerlValue::integer(match cmp {
12325                    std::cmp::Ordering::Less => -1,
12326                    std::cmp::Ordering::Greater => 1,
12327                    std::cmp::Ordering::Equal => 0,
12328                })
12329            }
12330            BinOp::BitAnd => {
12331                if let Some(s) = crate::value::set_intersection(lv, rv) {
12332                    s
12333                } else {
12334                    PerlValue::integer(lv.to_int() & rv.to_int())
12335                }
12336            }
12337            BinOp::BitOr => {
12338                if let Some(s) = crate::value::set_union(lv, rv) {
12339                    s
12340                } else {
12341                    PerlValue::integer(lv.to_int() | rv.to_int())
12342                }
12343            }
12344            BinOp::BitXor => PerlValue::integer(lv.to_int() ^ rv.to_int()),
12345            BinOp::ShiftLeft => PerlValue::integer(lv.to_int() << rv.to_int()),
12346            BinOp::ShiftRight => PerlValue::integer(lv.to_int() >> rv.to_int()),
12347            // These should have been handled by short-circuit above
12348            BinOp::LogAnd
12349            | BinOp::LogOr
12350            | BinOp::DefinedOr
12351            | BinOp::LogAndWord
12352            | BinOp::LogOrWord => unreachable!(),
12353            BinOp::BindMatch | BinOp::BindNotMatch => {
12354                unreachable!("regex bind handled in eval_expr BinOp arm")
12355            }
12356        })
12357    }
12358
12359    /// Perl 5 rejects `++@{...}`, `++%{...}`, postfix `@{...}++`, etc. (`Can't modify array/hash
12360    /// dereference in pre/postincrement/decrement`). Do not treat these as numeric ops on aggregate
12361    /// length — that was silently wrong vs `perl`.
12362    fn err_modify_symbolic_aggregate_deref_inc_dec(
12363        kind: Sigil,
12364        is_pre: bool,
12365        is_inc: bool,
12366        line: usize,
12367    ) -> FlowOrError {
12368        let agg = match kind {
12369            Sigil::Array => "array",
12370            Sigil::Hash => "hash",
12371            _ => unreachable!("expected symbolic @{{}} or %{{}} deref"),
12372        };
12373        let op = match (is_pre, is_inc) {
12374            (true, true) => "preincrement (++)",
12375            (true, false) => "predecrement (--)",
12376            (false, true) => "postincrement (++)",
12377            (false, false) => "postdecrement (--)",
12378        };
12379        FlowOrError::Error(PerlError::runtime(
12380            format!("Can't modify {agg} dereference in {op}"),
12381            line,
12382        ))
12383    }
12384
12385    /// `$$r++` / `$$r--` — returns old value; shared by the VM.
12386    pub(crate) fn symbolic_scalar_ref_postfix(
12387        &mut self,
12388        ref_val: PerlValue,
12389        decrement: bool,
12390        line: usize,
12391    ) -> Result<PerlValue, FlowOrError> {
12392        let old = self.symbolic_deref(ref_val.clone(), Sigil::Scalar, line)?;
12393        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12394        self.assign_scalar_ref_deref(ref_val, new_val, line)?;
12395        Ok(old)
12396    }
12397
12398    /// `$$r = $val` — assign through a scalar reference (or special name ref); shared by
12399    /// [`Self::assign_value`] and the VM.
12400    pub(crate) fn assign_scalar_ref_deref(
12401        &mut self,
12402        ref_val: PerlValue,
12403        val: PerlValue,
12404        line: usize,
12405    ) -> ExecResult {
12406        if let Some(name) = ref_val.as_scalar_binding_name() {
12407            self.set_special_var(&name, &val)
12408                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12409            return Ok(PerlValue::UNDEF);
12410        }
12411        if let Some(r) = ref_val.as_scalar_ref() {
12412            *r.write() = val;
12413            return Ok(PerlValue::UNDEF);
12414        }
12415        Err(PerlError::runtime("Can't assign to non-scalar reference", line).into())
12416    }
12417
12418    /// `@{ EXPR } = LIST` — array ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Array`]).
12419    pub(crate) fn assign_symbolic_array_ref_deref(
12420        &mut self,
12421        ref_val: PerlValue,
12422        val: PerlValue,
12423        line: usize,
12424    ) -> ExecResult {
12425        if let Some(a) = ref_val.as_array_ref() {
12426            *a.write() = val.to_list();
12427            return Ok(PerlValue::UNDEF);
12428        }
12429        if let Some(name) = ref_val.as_array_binding_name() {
12430            self.scope
12431                .set_array(&name, val.to_list())
12432                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12433            return Ok(PerlValue::UNDEF);
12434        }
12435        if let Some(s) = ref_val.as_str() {
12436            if self.strict_refs {
12437                return Err(PerlError::runtime(
12438                    format!(
12439                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
12440                        s
12441                    ),
12442                    line,
12443                )
12444                .into());
12445            }
12446            self.scope
12447                .set_array(&s, val.to_list())
12448                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12449            return Ok(PerlValue::UNDEF);
12450        }
12451        Err(PerlError::runtime("Can't assign to non-array reference", line).into())
12452    }
12453
12454    /// `*{ EXPR } = RHS` — symbolic glob name string (like `*{ $name } = …`); coderef via
12455    /// [`Self::assign_typeglob_value`] or glob-to-glob copy via [`Self::copy_typeglob_slots`].
12456    pub(crate) fn assign_symbolic_typeglob_ref_deref(
12457        &mut self,
12458        ref_val: PerlValue,
12459        val: PerlValue,
12460        line: usize,
12461    ) -> ExecResult {
12462        let lhs_name = if let Some(s) = ref_val.as_str() {
12463            if self.strict_refs {
12464                return Err(PerlError::runtime(
12465                    format!(
12466                        "Can't use string (\"{}\") as a symbol ref while \"strict refs\" in use",
12467                        s
12468                    ),
12469                    line,
12470                )
12471                .into());
12472            }
12473            s.to_string()
12474        } else {
12475            return Err(
12476                PerlError::runtime("Can't assign to non-glob symbolic reference", line).into(),
12477            );
12478        };
12479        let is_coderef = val.as_code_ref().is_some()
12480            || val
12481                .as_scalar_ref()
12482                .map(|r| r.read().as_code_ref().is_some())
12483                .unwrap_or(false);
12484        if is_coderef {
12485            return self.assign_typeglob_value(&lhs_name, val, line);
12486        }
12487        let rhs_key = val.to_string();
12488        self.copy_typeglob_slots(&lhs_name, &rhs_key, line)
12489            .map_err(FlowOrError::Error)?;
12490        Ok(PerlValue::UNDEF)
12491    }
12492
12493    /// `%{ EXPR } = LIST` — hash ref or package name string (mirrors [`Self::symbolic_deref`] for [`Sigil::Hash`]).
12494    pub(crate) fn assign_symbolic_hash_ref_deref(
12495        &mut self,
12496        ref_val: PerlValue,
12497        val: PerlValue,
12498        line: usize,
12499    ) -> ExecResult {
12500        let items = val.to_list();
12501        let mut map = IndexMap::new();
12502        let mut i = 0;
12503        while i + 1 < items.len() {
12504            map.insert(items[i].to_string(), items[i + 1].clone());
12505            i += 2;
12506        }
12507        if let Some(h) = ref_val.as_hash_ref() {
12508            *h.write() = map;
12509            return Ok(PerlValue::UNDEF);
12510        }
12511        if let Some(name) = ref_val.as_hash_binding_name() {
12512            self.touch_env_hash(&name);
12513            self.scope
12514                .set_hash(&name, map)
12515                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12516            return Ok(PerlValue::UNDEF);
12517        }
12518        if let Some(s) = ref_val.as_str() {
12519            if self.strict_refs {
12520                return Err(PerlError::runtime(
12521                    format!(
12522                        "Can't use string (\"{}\") as a HASH ref while \"strict refs\" in use",
12523                        s
12524                    ),
12525                    line,
12526                )
12527                .into());
12528            }
12529            self.touch_env_hash(&s);
12530            self.scope
12531                .set_hash(&s, map)
12532                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12533            return Ok(PerlValue::UNDEF);
12534        }
12535        Err(PerlError::runtime("Can't assign to non-hash reference", line).into())
12536    }
12537
12538    /// `$href->{key} = $val` and blessed hash slots — shared by [`Self::assign_value`] and the VM.
12539    pub(crate) fn assign_arrow_hash_deref(
12540        &mut self,
12541        container: PerlValue,
12542        key: String,
12543        val: PerlValue,
12544        line: usize,
12545    ) -> ExecResult {
12546        if let Some(b) = container.as_blessed_ref() {
12547            let mut data = b.data.write();
12548            if let Some(r) = data.as_hash_ref() {
12549                r.write().insert(key, val);
12550                return Ok(PerlValue::UNDEF);
12551            }
12552            if let Some(mut map) = data.as_hash_map() {
12553                map.insert(key, val);
12554                *data = PerlValue::hash(map);
12555                return Ok(PerlValue::UNDEF);
12556            }
12557            return Err(PerlError::runtime("Can't assign into non-hash blessed ref", line).into());
12558        }
12559        if let Some(r) = container.as_hash_ref() {
12560            r.write().insert(key, val);
12561            return Ok(PerlValue::UNDEF);
12562        }
12563        if let Some(name) = container.as_hash_binding_name() {
12564            self.touch_env_hash(&name);
12565            self.scope
12566                .set_hash_element(&name, &key, val)
12567                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12568            return Ok(PerlValue::UNDEF);
12569        }
12570        Err(PerlError::runtime("Can't assign to arrow hash deref on non-hash(-ref)", line).into())
12571    }
12572
12573    /// For `$aref->[ix]` / `@$r[ix]` arrow-array ops: the container must be the array **reference** (scalar),
12574    /// not `@{...}` / `@$r` expansion (which yields a plain array value).
12575    pub(crate) fn eval_arrow_array_base(
12576        &mut self,
12577        expr: &Expr,
12578        _line: usize,
12579    ) -> Result<PerlValue, FlowOrError> {
12580        match &expr.kind {
12581            ExprKind::Deref {
12582                expr: inner,
12583                kind: Sigil::Array | Sigil::Scalar,
12584            } => self.eval_expr(inner),
12585            _ => self.eval_expr(expr),
12586        }
12587    }
12588
12589    /// For `$href->{k}` / `$$r{k}`: container is the hashref scalar, not `%{ $r }` expansion.
12590    pub(crate) fn eval_arrow_hash_base(
12591        &mut self,
12592        expr: &Expr,
12593        _line: usize,
12594    ) -> Result<PerlValue, FlowOrError> {
12595        match &expr.kind {
12596            ExprKind::Deref {
12597                expr: inner,
12598                kind: Sigil::Scalar,
12599            } => self.eval_expr(inner),
12600            _ => self.eval_expr(expr),
12601        }
12602    }
12603
12604    /// Read `$aref->[$i]` — same indexing as the VM [`crate::bytecode::Op::ArrowArray`].
12605    pub(crate) fn read_arrow_array_element(
12606        &self,
12607        container: PerlValue,
12608        idx: i64,
12609        line: usize,
12610    ) -> Result<PerlValue, FlowOrError> {
12611        if let Some(a) = container.as_array_ref() {
12612            let arr = a.read();
12613            let i = if idx < 0 {
12614                (arr.len() as i64 + idx) as usize
12615            } else {
12616                idx as usize
12617            };
12618            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12619        }
12620        if let Some(name) = container.as_array_binding_name() {
12621            return Ok(self.scope.get_array_element(&name, idx));
12622        }
12623        if let Some(arr) = container.as_array_vec() {
12624            let i = if idx < 0 {
12625                (arr.len() as i64 + idx) as usize
12626            } else {
12627                idx as usize
12628            };
12629            return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12630        }
12631        // Blessed arrayref (e.g. `List::Util::_Pair`) — Perl allows `->[N]` on
12632        // blessed arrayrefs; `pairs` returns blessed `_Pair` objects that the
12633        // doc shows being indexed via `$_->[0]` / `$_->[1]`.
12634        if let Some(b) = container.as_blessed_ref() {
12635            let inner = b.data.read().clone();
12636            if let Some(a) = inner.as_array_ref() {
12637                let arr = a.read();
12638                let i = if idx < 0 {
12639                    (arr.len() as i64 + idx) as usize
12640                } else {
12641                    idx as usize
12642                };
12643                return Ok(arr.get(i).cloned().unwrap_or(PerlValue::UNDEF));
12644            }
12645        }
12646        Err(PerlError::runtime("Can't use arrow deref on non-array-ref", line).into())
12647    }
12648
12649    /// Read `$href->{key}` — same as the VM [`crate::bytecode::Op::ArrowHash`].
12650    pub(crate) fn read_arrow_hash_element(
12651        &mut self,
12652        container: PerlValue,
12653        key: &str,
12654        line: usize,
12655    ) -> Result<PerlValue, FlowOrError> {
12656        if let Some(r) = container.as_hash_ref() {
12657            let h = r.read();
12658            return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12659        }
12660        if let Some(name) = container.as_hash_binding_name() {
12661            self.touch_env_hash(&name);
12662            return Ok(self.scope.get_hash_element(&name, key));
12663        }
12664        if let Some(b) = container.as_blessed_ref() {
12665            let data = b.data.read();
12666            if let Some(v) = data.hash_get(key) {
12667                return Ok(v);
12668            }
12669            if let Some(r) = data.as_hash_ref() {
12670                let h = r.read();
12671                return Ok(h.get(key).cloned().unwrap_or(PerlValue::UNDEF));
12672            }
12673            return Err(PerlError::runtime(
12674                "Can't access hash field on non-hash blessed ref",
12675                line,
12676            )
12677            .into());
12678        }
12679        // Struct field access via hash deref syntax: $struct->{field}
12680        if let Some(s) = container.as_struct_inst() {
12681            if let Some(idx) = s.def.field_index(key) {
12682                return Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF));
12683            }
12684            return Err(PerlError::runtime(
12685                format!("struct {} has no field `{}`", s.def.name, key),
12686                line,
12687            )
12688            .into());
12689        }
12690        // Class instance field access via hash deref: $obj->{field}
12691        if let Some(c) = container.as_class_inst() {
12692            if let Some(idx) = c.def.field_index(key) {
12693                return Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF));
12694            }
12695            return Err(PerlError::runtime(
12696                format!("class {} has no field `{}`", c.def.name, key),
12697                line,
12698            )
12699            .into());
12700        }
12701        Err(PerlError::runtime("Can't use arrow deref on non-hash-ref", line).into())
12702    }
12703
12704    /// `$aref->[$i]++` / `$aref->[$i]--` — returns old value; shared by the VM.
12705    pub(crate) fn arrow_array_postfix(
12706        &mut self,
12707        container: PerlValue,
12708        idx: i64,
12709        decrement: bool,
12710        line: usize,
12711    ) -> Result<PerlValue, FlowOrError> {
12712        let old = self.read_arrow_array_element(container.clone(), idx, line)?;
12713        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12714        self.assign_arrow_array_deref(container, idx, new_val, line)?;
12715        Ok(old)
12716    }
12717
12718    /// `$href->{k}++` / `$href->{k}--` — returns old value; shared by the VM.
12719    pub(crate) fn arrow_hash_postfix(
12720        &mut self,
12721        container: PerlValue,
12722        key: String,
12723        decrement: bool,
12724        line: usize,
12725    ) -> Result<PerlValue, FlowOrError> {
12726        let old = self.read_arrow_hash_element(container.clone(), key.as_str(), line)?;
12727        let new_val = PerlValue::integer(old.to_int() + if decrement { -1 } else { 1 });
12728        self.assign_arrow_hash_deref(container, key, new_val, line)?;
12729        Ok(old)
12730    }
12731
12732    /// `BAREWORD` as an rvalue — matches `ExprKind::Bareword` in the tree walker. If a nullary
12733    /// subroutine by that name is defined, call it; otherwise stringify (bareword-as-string).
12734    /// `strict subs` is enforced transitively: if the bareword is used where a sub is called
12735    /// explicitly (`&foo` / `foo()`) and the sub is undefined, `call_named_sub` emits the
12736    /// `strict subs` error — bare rvalue position is lenient (matches tree semantics, which
12737    /// diverges slightly from Perl 5's compile-time `Bareword "..." not allowed while "strict
12738    /// subs" in use`).
12739    pub(crate) fn resolve_bareword_rvalue(
12740        &mut self,
12741        name: &str,
12742        want: WantarrayCtx,
12743        line: usize,
12744    ) -> Result<PerlValue, FlowOrError> {
12745        if name == "__PACKAGE__" {
12746            return Ok(PerlValue::string(self.current_package()));
12747        }
12748        if let Some(sub) = self.resolve_sub_by_name(name) {
12749            return self.call_sub(&sub, vec![], want, line);
12750        }
12751        // Try zero-arg builtins so `"#{red}"` resolves color codes etc.
12752        if let Some(r) = crate::builtins::try_builtin(self, name, &[], line) {
12753            return r.map_err(Into::into);
12754        }
12755        Ok(PerlValue::string(name.to_string()))
12756    }
12757
12758    /// `@$aref[i1,i2,...]` rvalue — read a slice through an array reference as a list.
12759    /// Shared by the VM [`crate::bytecode::Op::ArrowArraySlice`] path already, and by the new
12760    /// compound / inc-dec / assign helpers below.
12761    pub(crate) fn arrow_array_slice_values(
12762        &mut self,
12763        container: PerlValue,
12764        indices: &[i64],
12765        line: usize,
12766    ) -> Result<PerlValue, FlowOrError> {
12767        let mut out = Vec::with_capacity(indices.len());
12768        for &idx in indices {
12769            let v = self.read_arrow_array_element(container.clone(), idx, line)?;
12770            out.push(v);
12771        }
12772        Ok(PerlValue::array(out))
12773    }
12774
12775    /// `@$aref[i1,i2,...] = LIST` — element-wise assignment matching the tree-walker
12776    /// `assign_value` path for multi-index `ArrowDeref { Array, List }`. Shared by the VM
12777    /// [`crate::bytecode::Op::SetArrowArraySlice`].
12778    pub(crate) fn assign_arrow_array_slice(
12779        &mut self,
12780        container: PerlValue,
12781        indices: Vec<i64>,
12782        val: PerlValue,
12783        line: usize,
12784    ) -> Result<PerlValue, FlowOrError> {
12785        if indices.is_empty() {
12786            return Err(PerlError::runtime("assign to empty array slice", line).into());
12787        }
12788        let vals = val.to_list();
12789        for (i, idx) in indices.iter().enumerate() {
12790            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
12791            self.assign_arrow_array_deref(container.clone(), *idx, v, line)?;
12792        }
12793        Ok(PerlValue::UNDEF)
12794    }
12795
12796    /// Flatten `@a[IX,...]` subscripts to integer indices (range / list specs expand like the VM).
12797    pub(crate) fn flatten_array_slice_index_specs(
12798        &mut self,
12799        indices: &[Expr],
12800    ) -> Result<Vec<i64>, FlowOrError> {
12801        let mut out = Vec::new();
12802        for idx_expr in indices {
12803            let v = if matches!(idx_expr.kind, ExprKind::Range { .. }) {
12804                self.eval_expr_ctx(idx_expr, WantarrayCtx::List)?
12805            } else {
12806                self.eval_expr(idx_expr)?
12807            };
12808            if let Some(list) = v.as_array_vec() {
12809                for idx in list {
12810                    out.push(idx.to_int());
12811                }
12812            } else {
12813                out.push(v.to_int());
12814            }
12815        }
12816        Ok(out)
12817    }
12818
12819    /// `@name[i1,i2,...] = LIST` — element-wise assignment (VM [`crate::bytecode::Op::SetNamedArraySlice`]).
12820    pub(crate) fn assign_named_array_slice(
12821        &mut self,
12822        stash_array_name: &str,
12823        indices: Vec<i64>,
12824        val: PerlValue,
12825        line: usize,
12826    ) -> Result<PerlValue, FlowOrError> {
12827        if indices.is_empty() {
12828            return Err(PerlError::runtime("assign to empty array slice", line).into());
12829        }
12830        let vals = val.to_list();
12831        for (i, idx) in indices.iter().enumerate() {
12832            let v = vals.get(i).cloned().unwrap_or(PerlValue::UNDEF);
12833            self.scope
12834                .set_array_element(stash_array_name, *idx, v)
12835                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12836        }
12837        Ok(PerlValue::UNDEF)
12838    }
12839
12840    /// `@$aref[i1,i2,...] OP= rhs` — Perl 5 applies the compound op only to the **last** index.
12841    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceCompound`].
12842    pub(crate) fn compound_assign_arrow_array_slice(
12843        &mut self,
12844        container: PerlValue,
12845        indices: Vec<i64>,
12846        op: BinOp,
12847        rhs: PerlValue,
12848        line: usize,
12849    ) -> Result<PerlValue, FlowOrError> {
12850        if indices.is_empty() {
12851            return Err(PerlError::runtime("assign to empty array slice", line).into());
12852        }
12853        let last_idx = *indices.last().expect("non-empty indices");
12854        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
12855        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
12856        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
12857        Ok(new_val)
12858    }
12859
12860    /// `++@$aref[i1,i2,...]` / `--...` / `...++` / `...--` — Perl updates only the **last** index;
12861    /// pre forms return the new value, post forms return the old **last** element.
12862    /// `kind` byte: 0=PreInc, 1=PreDec, 2=PostInc, 3=PostDec.
12863    /// Shared by VM [`crate::bytecode::Op::ArrowArraySliceIncDec`].
12864    pub(crate) fn arrow_array_slice_inc_dec(
12865        &mut self,
12866        container: PerlValue,
12867        indices: Vec<i64>,
12868        kind: u8,
12869        line: usize,
12870    ) -> Result<PerlValue, FlowOrError> {
12871        if indices.is_empty() {
12872            return Err(
12873                PerlError::runtime("array slice increment needs at least one index", line).into(),
12874            );
12875        }
12876        let last_idx = *indices.last().expect("non-empty indices");
12877        let last_old = self.read_arrow_array_element(container.clone(), last_idx, line)?;
12878        let new_val = if kind & 1 == 0 {
12879            PerlValue::integer(last_old.to_int() + 1)
12880        } else {
12881            PerlValue::integer(last_old.to_int() - 1)
12882        };
12883        self.assign_arrow_array_deref(container, last_idx, new_val.clone(), line)?;
12884        Ok(if kind < 2 { new_val } else { last_old })
12885    }
12886
12887    /// `++@name[i1,i2,...]` / `--...` / `...++` / `...--` on a stash-qualified array name.
12888    /// Same semantics as [`Self::arrow_array_slice_inc_dec`] (only the **last** index is updated).
12889    pub(crate) fn named_array_slice_inc_dec(
12890        &mut self,
12891        stash_array_name: &str,
12892        indices: Vec<i64>,
12893        kind: u8,
12894        line: usize,
12895    ) -> Result<PerlValue, FlowOrError> {
12896        let last_idx = *indices.last().ok_or_else(|| {
12897            PerlError::runtime("array slice increment needs at least one index", line)
12898        })?;
12899        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
12900        let new_val = if kind & 1 == 0 {
12901            PerlValue::integer(last_old.to_int() + 1)
12902        } else {
12903            PerlValue::integer(last_old.to_int() - 1)
12904        };
12905        self.scope
12906            .set_array_element(stash_array_name, last_idx, new_val.clone())
12907            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12908        Ok(if kind < 2 { new_val } else { last_old })
12909    }
12910
12911    /// `@name[i1,i2,...] OP= rhs` — only the **last** index is updated (VM [`crate::bytecode::Op::NamedArraySliceCompound`]).
12912    pub(crate) fn compound_assign_named_array_slice(
12913        &mut self,
12914        stash_array_name: &str,
12915        indices: Vec<i64>,
12916        op: BinOp,
12917        rhs: PerlValue,
12918        line: usize,
12919    ) -> Result<PerlValue, FlowOrError> {
12920        if indices.is_empty() {
12921            return Err(PerlError::runtime("assign to empty array slice", line).into());
12922        }
12923        let last_idx = *indices.last().expect("non-empty indices");
12924        let last_old = self.scope.get_array_element(stash_array_name, last_idx);
12925        let new_val = self.eval_binop(op, &last_old, &rhs, line)?;
12926        self.scope
12927            .set_array_element(stash_array_name, last_idx, new_val.clone())
12928            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12929        Ok(new_val)
12930    }
12931
12932    /// `$aref->[$i] = $val` — shared by [`Self::assign_value`] and the VM.
12933    pub(crate) fn assign_arrow_array_deref(
12934        &mut self,
12935        container: PerlValue,
12936        idx: i64,
12937        val: PerlValue,
12938        line: usize,
12939    ) -> ExecResult {
12940        if let Some(a) = container.as_array_ref() {
12941            let mut arr = a.write();
12942            let i = if idx < 0 {
12943                (arr.len() as i64 + idx) as usize
12944            } else {
12945                idx as usize
12946            };
12947            if i >= arr.len() {
12948                arr.resize(i + 1, PerlValue::UNDEF);
12949            }
12950            arr[i] = val;
12951            return Ok(PerlValue::UNDEF);
12952        }
12953        if let Some(name) = container.as_array_binding_name() {
12954            self.scope
12955                .set_array_element(&name, idx, val)
12956                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
12957            return Ok(PerlValue::UNDEF);
12958        }
12959        Err(PerlError::runtime("Can't assign to arrow array deref on non-array-ref", line).into())
12960    }
12961
12962    /// `*name = $coderef` — install subroutine alias (tree [`assign_value`] and VM [`crate::bytecode::Op::TypeglobAssignFromValue`]).
12963    pub(crate) fn assign_typeglob_value(
12964        &mut self,
12965        name: &str,
12966        val: PerlValue,
12967        line: usize,
12968    ) -> ExecResult {
12969        let sub = if let Some(c) = val.as_code_ref() {
12970            Some(c)
12971        } else if let Some(r) = val.as_scalar_ref() {
12972            r.read().as_code_ref().map(|c| Arc::clone(&c))
12973        } else {
12974            None
12975        };
12976        if let Some(sub) = sub {
12977            let lhs_sub = self.qualify_typeglob_sub_key(name);
12978            self.subs.insert(lhs_sub, sub);
12979            return Ok(PerlValue::UNDEF);
12980        }
12981        Err(PerlError::runtime(
12982            "typeglob assignment requires a subroutine reference (e.g. *foo = \\&bar) or another typeglob (*foo = *bar)",
12983            line,
12984        )
12985        .into())
12986    }
12987
12988    fn assign_value(&mut self, target: &Expr, val: PerlValue) -> ExecResult {
12989        match &target.kind {
12990            ExprKind::ScalarVar(name) => {
12991                let stor = self.tree_scalar_storage_name(name);
12992                if self.scope.is_scalar_frozen(&stor) {
12993                    return Err(FlowOrError::Error(PerlError::runtime(
12994                        format!("Modification of a frozen value: ${}", name),
12995                        target.line,
12996                    )));
12997                }
12998                if let Some(obj) = self.tied_scalars.get(&stor).cloned() {
12999                    let class = obj
13000                        .as_blessed_ref()
13001                        .map(|b| b.class.clone())
13002                        .unwrap_or_default();
13003                    let full = format!("{}::STORE", class);
13004                    if let Some(sub) = self.subs.get(&full).cloned() {
13005                        let arg_vals = vec![obj, val];
13006                        return match self.call_sub(
13007                            &sub,
13008                            arg_vals,
13009                            WantarrayCtx::Scalar,
13010                            target.line,
13011                        ) {
13012                            Ok(_) => Ok(PerlValue::UNDEF),
13013                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13014                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13015                        };
13016                    }
13017                }
13018                self.set_special_var(&stor, &val)
13019                    .map_err(|e| FlowOrError::Error(e.at_line(target.line)))?;
13020                Ok(PerlValue::UNDEF)
13021            }
13022            ExprKind::ArrayVar(name) => {
13023                if self.scope.is_array_frozen(name) {
13024                    return Err(PerlError::runtime(
13025                        format!("Modification of a frozen value: @{}", name),
13026                        target.line,
13027                    )
13028                    .into());
13029                }
13030                if self.strict_vars
13031                    && !name.contains("::")
13032                    && !self.scope.array_binding_exists(name)
13033                {
13034                    return Err(PerlError::runtime(
13035                        format!(
13036                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
13037                            name, name
13038                        ),
13039                        target.line,
13040                    )
13041                    .into());
13042                }
13043                self.scope.set_array(name, val.to_list())?;
13044                Ok(PerlValue::UNDEF)
13045            }
13046            ExprKind::HashVar(name) => {
13047                if self.strict_vars && !name.contains("::") && !self.scope.hash_binding_exists(name)
13048                {
13049                    return Err(PerlError::runtime(
13050                        format!(
13051                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13052                            name, name
13053                        ),
13054                        target.line,
13055                    )
13056                    .into());
13057                }
13058                let items = val.to_list();
13059                let mut map = IndexMap::new();
13060                let mut i = 0;
13061                while i + 1 < items.len() {
13062                    map.insert(items[i].to_string(), items[i + 1].clone());
13063                    i += 2;
13064                }
13065                self.scope.set_hash(name, map)?;
13066                Ok(PerlValue::UNDEF)
13067            }
13068            ExprKind::ArrayElement { array, index } => {
13069                if self.strict_vars
13070                    && !array.contains("::")
13071                    && !self.scope.array_binding_exists(array)
13072                {
13073                    return Err(PerlError::runtime(
13074                        format!(
13075                            "Global symbol \"@{}\" requires explicit package name (did you forget to declare \"my @{}\"?)",
13076                            array, array
13077                        ),
13078                        target.line,
13079                    )
13080                    .into());
13081                }
13082                if self.scope.is_array_frozen(array) {
13083                    return Err(PerlError::runtime(
13084                        format!("Modification of a frozen value: @{}", array),
13085                        target.line,
13086                    )
13087                    .into());
13088                }
13089                let idx = self.eval_expr(index)?.to_int();
13090                let aname = self.stash_array_name_for_package(array);
13091                if let Some(obj) = self.tied_arrays.get(&aname).cloned() {
13092                    let class = obj
13093                        .as_blessed_ref()
13094                        .map(|b| b.class.clone())
13095                        .unwrap_or_default();
13096                    let full = format!("{}::STORE", class);
13097                    if let Some(sub) = self.subs.get(&full).cloned() {
13098                        let arg_vals = vec![obj, PerlValue::integer(idx), val];
13099                        return match self.call_sub(
13100                            &sub,
13101                            arg_vals,
13102                            WantarrayCtx::Scalar,
13103                            target.line,
13104                        ) {
13105                            Ok(_) => Ok(PerlValue::UNDEF),
13106                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13107                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13108                        };
13109                    }
13110                }
13111                self.scope.set_array_element(&aname, idx, val)?;
13112                Ok(PerlValue::UNDEF)
13113            }
13114            ExprKind::ArraySlice { array, indices } => {
13115                if indices.is_empty() {
13116                    return Err(
13117                        PerlError::runtime("assign to empty array slice", target.line).into(),
13118                    );
13119                }
13120                self.check_strict_array_var(array, target.line)?;
13121                if self.scope.is_array_frozen(array) {
13122                    return Err(PerlError::runtime(
13123                        format!("Modification of a frozen value: @{}", array),
13124                        target.line,
13125                    )
13126                    .into());
13127                }
13128                let aname = self.stash_array_name_for_package(array);
13129                let flat = self.flatten_array_slice_index_specs(indices)?;
13130                self.assign_named_array_slice(&aname, flat, val, target.line)
13131            }
13132            ExprKind::HashElement { hash, key } => {
13133                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
13134                {
13135                    return Err(PerlError::runtime(
13136                        format!(
13137                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13138                            hash, hash
13139                        ),
13140                        target.line,
13141                    )
13142                    .into());
13143                }
13144                if self.scope.is_hash_frozen(hash) {
13145                    return Err(PerlError::runtime(
13146                        format!("Modification of a frozen value: %%{}", hash),
13147                        target.line,
13148                    )
13149                    .into());
13150                }
13151                let k = self.eval_expr(key)?.to_string();
13152                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
13153                    let class = obj
13154                        .as_blessed_ref()
13155                        .map(|b| b.class.clone())
13156                        .unwrap_or_default();
13157                    let full = format!("{}::STORE", class);
13158                    if let Some(sub) = self.subs.get(&full).cloned() {
13159                        let arg_vals = vec![obj, PerlValue::string(k), val];
13160                        return match self.call_sub(
13161                            &sub,
13162                            arg_vals,
13163                            WantarrayCtx::Scalar,
13164                            target.line,
13165                        ) {
13166                            Ok(_) => Ok(PerlValue::UNDEF),
13167                            Err(FlowOrError::Flow(_)) => Ok(PerlValue::UNDEF),
13168                            Err(FlowOrError::Error(e)) => Err(FlowOrError::Error(e)),
13169                        };
13170                    }
13171                }
13172                self.scope.set_hash_element(hash, &k, val)?;
13173                Ok(PerlValue::UNDEF)
13174            }
13175            ExprKind::HashSlice { hash, keys } => {
13176                if keys.is_empty() {
13177                    return Err(
13178                        PerlError::runtime("assign to empty hash slice", target.line).into(),
13179                    );
13180                }
13181                if self.strict_vars && !hash.contains("::") && !self.scope.hash_binding_exists(hash)
13182                {
13183                    return Err(PerlError::runtime(
13184                        format!(
13185                            "Global symbol \"%{}\" requires explicit package name (did you forget to declare \"my %{}\"?)",
13186                            hash, hash
13187                        ),
13188                        target.line,
13189                    )
13190                    .into());
13191                }
13192                if self.scope.is_hash_frozen(hash) {
13193                    return Err(PerlError::runtime(
13194                        format!("Modification of a frozen value: %%{}", hash),
13195                        target.line,
13196                    )
13197                    .into());
13198                }
13199                let mut key_vals = Vec::with_capacity(keys.len());
13200                for key_expr in keys {
13201                    let v = if matches!(key_expr.kind, ExprKind::Range { .. }) {
13202                        self.eval_expr_ctx(key_expr, WantarrayCtx::List)?
13203                    } else {
13204                        self.eval_expr(key_expr)?
13205                    };
13206                    key_vals.push(v);
13207                }
13208                self.assign_named_hash_slice(hash, key_vals, val, target.line)
13209            }
13210            ExprKind::Typeglob(name) => self.assign_typeglob_value(name, val, target.line),
13211            ExprKind::TypeglobExpr(e) => {
13212                let name = self.eval_expr(e)?.to_string();
13213                let synthetic = Expr {
13214                    kind: ExprKind::Typeglob(name),
13215                    line: target.line,
13216                };
13217                self.assign_value(&synthetic, val)
13218            }
13219            ExprKind::AnonymousListSlice { source, indices } => {
13220                if let ExprKind::Deref {
13221                    expr: inner,
13222                    kind: Sigil::Array,
13223                } = &source.kind
13224                {
13225                    let container = self.eval_arrow_array_base(inner, target.line)?;
13226                    let vals = val.to_list();
13227                    let n = indices.len().min(vals.len());
13228                    for i in 0..n {
13229                        let idx = self.eval_expr(&indices[i])?.to_int();
13230                        self.assign_arrow_array_deref(
13231                            container.clone(),
13232                            idx,
13233                            vals[i].clone(),
13234                            target.line,
13235                        )?;
13236                    }
13237                    return Ok(PerlValue::UNDEF);
13238                }
13239                Err(
13240                    PerlError::runtime("assign to list slice: unsupported base", target.line)
13241                        .into(),
13242                )
13243            }
13244            ExprKind::ArrowDeref {
13245                expr,
13246                index,
13247                kind: DerefKind::Hash,
13248            } => {
13249                let key = self.eval_expr(index)?.to_string();
13250                let container = self.eval_expr(expr)?;
13251                self.assign_arrow_hash_deref(container, key, val, target.line)
13252            }
13253            ExprKind::ArrowDeref {
13254                expr,
13255                index,
13256                kind: DerefKind::Array,
13257            } => {
13258                let container = self.eval_arrow_array_base(expr, target.line)?;
13259                if let ExprKind::List(indices) = &index.kind {
13260                    let vals = val.to_list();
13261                    let n = indices.len().min(vals.len());
13262                    for i in 0..n {
13263                        let idx = self.eval_expr(&indices[i])?.to_int();
13264                        self.assign_arrow_array_deref(
13265                            container.clone(),
13266                            idx,
13267                            vals[i].clone(),
13268                            target.line,
13269                        )?;
13270                    }
13271                    return Ok(PerlValue::UNDEF);
13272                }
13273                let idx = self.eval_expr(index)?.to_int();
13274                self.assign_arrow_array_deref(container, idx, val, target.line)
13275            }
13276            ExprKind::HashSliceDeref { container, keys } => {
13277                let href = self.eval_expr(container)?;
13278                let mut key_vals = Vec::with_capacity(keys.len());
13279                for key_expr in keys {
13280                    key_vals.push(self.eval_expr(key_expr)?);
13281                }
13282                self.assign_hash_slice_deref(href, key_vals, val, target.line)
13283            }
13284            ExprKind::Deref {
13285                expr,
13286                kind: Sigil::Scalar,
13287            } => {
13288                let ref_val = self.eval_expr(expr)?;
13289                self.assign_scalar_ref_deref(ref_val, val, target.line)
13290            }
13291            ExprKind::Deref {
13292                expr,
13293                kind: Sigil::Array,
13294            } => {
13295                let ref_val = self.eval_expr(expr)?;
13296                self.assign_symbolic_array_ref_deref(ref_val, val, target.line)
13297            }
13298            ExprKind::Deref {
13299                expr,
13300                kind: Sigil::Hash,
13301            } => {
13302                let ref_val = self.eval_expr(expr)?;
13303                self.assign_symbolic_hash_ref_deref(ref_val, val, target.line)
13304            }
13305            ExprKind::Deref {
13306                expr,
13307                kind: Sigil::Typeglob,
13308            } => {
13309                let ref_val = self.eval_expr(expr)?;
13310                self.assign_symbolic_typeglob_ref_deref(ref_val, val, target.line)
13311            }
13312            ExprKind::Pos(inner) => {
13313                let key = match inner {
13314                    None => "_".to_string(),
13315                    Some(expr) => match &expr.kind {
13316                        ExprKind::ScalarVar(n) => n.clone(),
13317                        _ => self.eval_expr(expr)?.to_string(),
13318                    },
13319                };
13320                if val.is_undef() {
13321                    self.regex_pos.insert(key, None);
13322                } else {
13323                    let u = val.to_int().max(0) as usize;
13324                    self.regex_pos.insert(key, Some(u));
13325                }
13326                Ok(PerlValue::UNDEF)
13327            }
13328            // List assignment: `($a, $b, ...) = (val1, val2, ...)`
13329            // RHS is already fully evaluated — distribute elements to targets.
13330            ExprKind::List(targets) => {
13331                let items = val.to_list();
13332                for (i, t) in targets.iter().enumerate() {
13333                    let v = items.get(i).cloned().unwrap_or(PerlValue::UNDEF);
13334                    self.assign_value(t, v)?;
13335                }
13336                Ok(PerlValue::UNDEF)
13337            }
13338            // `($f = EXPR) =~ s///` — assignment returns the target as an lvalue;
13339            // write the substitution result back to the assignment target.
13340            ExprKind::Assign { target, .. } => self.assign_value(target, val),
13341            _ => Ok(PerlValue::UNDEF),
13342        }
13343    }
13344
13345    /// True when [`get_special_var`] must run instead of [`Scope::get_scalar`].
13346    pub(crate) fn is_special_scalar_name_for_get(name: &str) -> bool {
13347        (name.starts_with('#') && name.len() > 1)
13348            || name.starts_with('^')
13349            || matches!(
13350                name,
13351                "$$" | "0"
13352                    | "!"
13353                    | "@"
13354                    | "/"
13355                    | "\\"
13356                    | ","
13357                    | "."
13358                    | "]"
13359                    | ";"
13360                    | "ARGV"
13361                    | "^I"
13362                    | "^D"
13363                    | "^P"
13364                    | "^S"
13365                    | "^W"
13366                    | "^O"
13367                    | "^T"
13368                    | "^V"
13369                    | "^E"
13370                    | "^H"
13371                    | "^WARNING_BITS"
13372                    | "^GLOBAL_PHASE"
13373                    | "^MATCH"
13374                    | "^PREMATCH"
13375                    | "^POSTMATCH"
13376                    | "^LAST_SUBMATCH_RESULT"
13377                    | "<"
13378                    | ">"
13379                    | "("
13380                    | ")"
13381                    | "?"
13382                    | "|"
13383                    | "\""
13384                    | "+"
13385                    | "%"
13386                    | "="
13387                    | "-"
13388                    | ":"
13389                    | "*"
13390                    | "INC"
13391            )
13392            || crate::english::is_known_alias(name)
13393    }
13394
13395    /// Map English long names (`ARG` → [`crate::english::scalar_alias`]) when [`Self::english_enabled`],
13396    /// except for names registered in [`Self::english_lexical_scalars`] (lexical `my`/`our`/…).
13397    /// Match aliases (`MATCH`/`PREMATCH`/`POSTMATCH`) are suppressed when
13398    /// [`Self::english_no_match_vars`] is set.
13399    #[inline]
13400    pub(crate) fn english_scalar_name<'a>(&self, name: &'a str) -> &'a str {
13401        if !self.english_enabled {
13402            return name;
13403        }
13404        if self
13405            .english_lexical_scalars
13406            .iter()
13407            .any(|s| s.contains(name))
13408        {
13409            return name;
13410        }
13411        if let Some(short) = crate::english::scalar_alias(name, self.english_no_match_vars) {
13412            return short;
13413        }
13414        name
13415    }
13416
13417    /// True when [`set_special_var`] must run instead of [`Scope::set_scalar`].
13418    pub(crate) fn is_special_scalar_name_for_set(name: &str) -> bool {
13419        name.starts_with('^')
13420            || matches!(
13421                name,
13422                "0" | "/"
13423                    | "\\"
13424                    | ","
13425                    | ";"
13426                    | "\""
13427                    | "%"
13428                    | "="
13429                    | "-"
13430                    | ":"
13431                    | "*"
13432                    | "INC"
13433                    | "^I"
13434                    | "^D"
13435                    | "^P"
13436                    | "^W"
13437                    | "^H"
13438                    | "^WARNING_BITS"
13439                    | "$$"
13440                    | "]"
13441                    | "^S"
13442                    | "ARGV"
13443                    | "|"
13444                    | "+"
13445                    | "?"
13446                    | "!"
13447                    | "@"
13448                    | "."
13449            )
13450            || crate::english::is_known_alias(name)
13451    }
13452
13453    pub(crate) fn get_special_var(&self, name: &str) -> PerlValue {
13454        // AWK-style aliases always available (no `-MEnglish` needed) — disabled in --compat
13455        let name = if !crate::compat_mode() {
13456            match name {
13457                "NR" => ".",
13458                "RS" => "/",
13459                "OFS" => ",",
13460                "ORS" => "\\",
13461                "NF" => {
13462                    let len = self.scope.array_len("F");
13463                    return PerlValue::integer(len as i64);
13464                }
13465                _ => self.english_scalar_name(name),
13466            }
13467        } else {
13468            self.english_scalar_name(name)
13469        };
13470        match name {
13471            "$$" => PerlValue::integer(std::process::id() as i64),
13472            "_" => self.scope.get_scalar("_"),
13473            "^MATCH" => PerlValue::string(self.last_match.clone()),
13474            "^PREMATCH" => PerlValue::string(self.prematch.clone()),
13475            "^POSTMATCH" => PerlValue::string(self.postmatch.clone()),
13476            "^LAST_SUBMATCH_RESULT" => PerlValue::string(self.last_paren_match.clone()),
13477            "0" => PerlValue::string(self.program_name.clone()),
13478            "!" => PerlValue::errno_dual(self.errno_code, self.errno.clone()),
13479            "@" => {
13480                if let Some(ref v) = self.eval_error_value {
13481                    v.clone()
13482                } else {
13483                    PerlValue::errno_dual(self.eval_error_code, self.eval_error.clone())
13484                }
13485            }
13486            "/" => match &self.irs {
13487                Some(s) => PerlValue::string(s.clone()),
13488                None => PerlValue::UNDEF,
13489            },
13490            "\\" => PerlValue::string(self.ors.clone()),
13491            "," => PerlValue::string(self.ofs.clone()),
13492            "." => {
13493                // Perl: `$.` is undefined until a line is read (or `-n`/`-p` advances `line_number`).
13494                if self.last_readline_handle.is_empty() {
13495                    if self.line_number == 0 {
13496                        PerlValue::UNDEF
13497                    } else {
13498                        PerlValue::integer(self.line_number)
13499                    }
13500                } else {
13501                    PerlValue::integer(
13502                        *self
13503                            .handle_line_numbers
13504                            .get(&self.last_readline_handle)
13505                            .unwrap_or(&0),
13506                    )
13507                }
13508            }
13509            "]" => PerlValue::float(perl_bracket_version()),
13510            ";" => PerlValue::string(self.subscript_sep.clone()),
13511            "ARGV" => PerlValue::string(self.argv_current_file.clone()),
13512            "^I" => PerlValue::string(self.inplace_edit.clone()),
13513            "^D" => PerlValue::integer(self.debug_flags),
13514            "^P" => PerlValue::integer(self.perl_debug_flags),
13515            "^S" => PerlValue::integer(if self.eval_nesting > 0 { 1 } else { 0 }),
13516            "^W" => PerlValue::integer(if self.warnings { 1 } else { 0 }),
13517            "^O" => PerlValue::string(perl_osname()),
13518            "^T" => PerlValue::integer(self.script_start_time),
13519            "^V" => PerlValue::string(perl_version_v_string()),
13520            "^E" => PerlValue::string(extended_os_error_string()),
13521            "^H" => PerlValue::integer(self.compile_hints),
13522            "^WARNING_BITS" => PerlValue::integer(self.warning_bits),
13523            "^GLOBAL_PHASE" => PerlValue::string(self.global_phase.clone()),
13524            "<" | ">" => PerlValue::integer(unix_id_for_special(name)),
13525            "(" | ")" => PerlValue::string(unix_group_list_for_special(name)),
13526            "?" => PerlValue::integer(self.child_exit_status),
13527            "|" => PerlValue::integer(if self.output_autoflush { 1 } else { 0 }),
13528            "\"" => PerlValue::string(self.list_separator.clone()),
13529            "+" => PerlValue::string(self.last_paren_match.clone()),
13530            "%" => PerlValue::integer(self.format_page_number),
13531            "=" => PerlValue::integer(self.format_lines_per_page),
13532            "-" => PerlValue::integer(self.format_lines_left),
13533            ":" => PerlValue::string(self.format_line_break_chars.clone()),
13534            "*" => PerlValue::integer(if self.multiline_match { 1 } else { 0 }),
13535            "^" => PerlValue::string(self.format_top_name.clone()),
13536            "INC" => PerlValue::integer(self.inc_hook_index),
13537            "^A" => PerlValue::string(self.accumulator_format.clone()),
13538            "^C" => PerlValue::integer(if self.sigint_pending_caret.replace(false) {
13539                1
13540            } else {
13541                0
13542            }),
13543            "^F" => PerlValue::integer(self.max_system_fd),
13544            "^L" => PerlValue::string(self.formfeed_string.clone()),
13545            "^M" => PerlValue::string(self.emergency_memory.clone()),
13546            "^N" => PerlValue::string(self.last_subpattern_name.clone()),
13547            "^X" => PerlValue::string(self.executable_path.clone()),
13548            // perlvar ${^…} — stubs with sane defaults where Perl exposes constants.
13549            "^TAINT" | "^TAINTED" => PerlValue::integer(0),
13550            "^UNICODE" => PerlValue::integer(if self.utf8_pragma { 1 } else { 0 }),
13551            "^OPEN" => PerlValue::integer(if self.open_pragma_utf8 { 1 } else { 0 }),
13552            "^UTF8LOCALE" => PerlValue::integer(0),
13553            "^UTF8CACHE" => PerlValue::integer(-1),
13554            _ if name.starts_with('^') && name.len() > 1 => self
13555                .special_caret_scalars
13556                .get(name)
13557                .cloned()
13558                .unwrap_or(PerlValue::UNDEF),
13559            _ if name.starts_with('#') && name.len() > 1 => {
13560                let arr = &name[1..];
13561                let aname = self.stash_array_name_for_package(arr);
13562                let len = self.scope.array_len(&aname);
13563                PerlValue::integer(len as i64 - 1)
13564            }
13565            _ => self.scope.get_scalar(name),
13566        }
13567    }
13568
13569    pub(crate) fn set_special_var(&mut self, name: &str, val: &PerlValue) -> Result<(), PerlError> {
13570        let name = self.english_scalar_name(name);
13571        match name {
13572            "!" => {
13573                let code = val.to_int() as i32;
13574                self.errno_code = code;
13575                self.errno = if code == 0 {
13576                    String::new()
13577                } else {
13578                    std::io::Error::from_raw_os_error(code).to_string()
13579                };
13580            }
13581            "@" => {
13582                if let Some((code, msg)) = val.errno_dual_parts() {
13583                    self.eval_error_code = code;
13584                    self.eval_error = msg;
13585                } else {
13586                    self.eval_error = val.to_string();
13587                    let mut code = val.to_int() as i32;
13588                    if code == 0 && !self.eval_error.is_empty() {
13589                        code = 1;
13590                    }
13591                    self.eval_error_code = code;
13592                }
13593            }
13594            "." => {
13595                // perlvar: assigning to `$.` sets the line number for the last-read filehandle,
13596                // or the global counter when no handle has been read yet (`-n`/`-p` / pre-read).
13597                let n = val.to_int();
13598                if self.last_readline_handle.is_empty() {
13599                    self.line_number = n;
13600                } else {
13601                    self.handle_line_numbers
13602                        .insert(self.last_readline_handle.clone(), n);
13603                }
13604            }
13605            "0" => self.program_name = val.to_string(),
13606            "/" => {
13607                self.irs = if val.is_undef() {
13608                    None
13609                } else {
13610                    Some(val.to_string())
13611                }
13612            }
13613            "\\" => self.ors = val.to_string(),
13614            "," => self.ofs = val.to_string(),
13615            ";" => self.subscript_sep = val.to_string(),
13616            "\"" => self.list_separator = val.to_string(),
13617            "%" => self.format_page_number = val.to_int(),
13618            "=" => self.format_lines_per_page = val.to_int(),
13619            "-" => self.format_lines_left = val.to_int(),
13620            ":" => self.format_line_break_chars = val.to_string(),
13621            "*" => self.multiline_match = val.to_int() != 0,
13622            "^" => self.format_top_name = val.to_string(),
13623            "INC" => self.inc_hook_index = val.to_int(),
13624            "^A" => self.accumulator_format = val.to_string(),
13625            "^F" => self.max_system_fd = val.to_int(),
13626            "^L" => self.formfeed_string = val.to_string(),
13627            "^M" => self.emergency_memory = val.to_string(),
13628            "^I" => self.inplace_edit = val.to_string(),
13629            "^D" => self.debug_flags = val.to_int(),
13630            "^P" => self.perl_debug_flags = val.to_int(),
13631            "^W" => self.warnings = val.to_int() != 0,
13632            "^H" => self.compile_hints = val.to_int(),
13633            "^WARNING_BITS" => self.warning_bits = val.to_int(),
13634            "|" => {
13635                self.output_autoflush = val.to_int() != 0;
13636                if self.output_autoflush {
13637                    let _ = io::stdout().flush();
13638                }
13639            }
13640            // Read-only or pid-backed
13641            "$$"
13642            | "]"
13643            | "^S"
13644            | "ARGV"
13645            | "?"
13646            | "^O"
13647            | "^T"
13648            | "^V"
13649            | "^E"
13650            | "^GLOBAL_PHASE"
13651            | "^MATCH"
13652            | "^PREMATCH"
13653            | "^POSTMATCH"
13654            | "^LAST_SUBMATCH_RESULT"
13655            | "^C"
13656            | "^N"
13657            | "^X"
13658            | "^TAINT"
13659            | "^TAINTED"
13660            | "^UNICODE"
13661            | "^UTF8LOCALE"
13662            | "^UTF8CACHE"
13663            | "+"
13664            | "<"
13665            | ">"
13666            | "("
13667            | ")" => {}
13668            _ if name.starts_with('^') && name.len() > 1 => {
13669                self.special_caret_scalars
13670                    .insert(name.to_string(), val.clone());
13671            }
13672            _ => self.scope.set_scalar(name, val.clone())?,
13673        }
13674        Ok(())
13675    }
13676
13677    fn extract_array_name(&self, expr: &Expr) -> Result<String, FlowOrError> {
13678        match &expr.kind {
13679            ExprKind::ArrayVar(name) => Ok(name.clone()),
13680            ExprKind::ScalarVar(name) => Ok(name.clone()), // @_ written as shift of implicit
13681            _ => Err(PerlError::runtime("Expected array", expr.line).into()),
13682        }
13683    }
13684
13685    /// `pop (expr)` / `scalar @arr` / one-element list — peel to the real array operand.
13686    fn peel_array_builtin_operand(expr: &Expr) -> &Expr {
13687        match &expr.kind {
13688            ExprKind::ScalarContext(inner) => Self::peel_array_builtin_operand(inner),
13689            ExprKind::List(es) if es.len() == 1 => Self::peel_array_builtin_operand(&es[0]),
13690            _ => expr,
13691        }
13692    }
13693
13694    /// `@$aref` / `@{...}` after optional peeling — for tree `SpliceExpr` / `pop` fallbacks.
13695    fn try_eval_array_deref_container(
13696        &mut self,
13697        expr: &Expr,
13698    ) -> Result<Option<PerlValue>, FlowOrError> {
13699        let e = Self::peel_array_builtin_operand(expr);
13700        if let ExprKind::Deref {
13701            expr: inner,
13702            kind: Sigil::Array,
13703        } = &e.kind
13704        {
13705            return Ok(Some(self.eval_expr(inner)?));
13706        }
13707        Ok(None)
13708    }
13709
13710    /// Current package (`main` when `__PACKAGE__` is unset or empty).
13711    fn current_package(&self) -> String {
13712        let s = self.scope.get_scalar("__PACKAGE__").to_string();
13713        if s.is_empty() {
13714            "main".to_string()
13715        } else {
13716            s
13717        }
13718    }
13719
13720    /// `Foo->VERSION` / `$blessed->VERSION` — read `$VERSION` with `__PACKAGE__` set to the invocant
13721    /// package (our `$VERSION` is not stored under `Foo::VERSION` keys yet).
13722    pub(crate) fn package_version_scalar(
13723        &mut self,
13724        package: &str,
13725    ) -> PerlResult<Option<PerlValue>> {
13726        let saved_pkg = self.scope.get_scalar("__PACKAGE__");
13727        let _ = self
13728            .scope
13729            .set_scalar("__PACKAGE__", PerlValue::string(package.to_string()));
13730        let ver = self.get_special_var("VERSION");
13731        let _ = self.scope.set_scalar("__PACKAGE__", saved_pkg);
13732        Ok(if ver.is_undef() { None } else { Some(ver) })
13733    }
13734
13735    /// Walk C3 MRO from `start_package` and return the first `Package::AUTOLOAD` (`AUTOLOAD` in `main`).
13736    pub(crate) fn resolve_autoload_sub(&self, start_package: &str) -> Option<Arc<PerlSub>> {
13737        let root = if start_package.is_empty() {
13738            "main"
13739        } else {
13740            start_package
13741        };
13742        for pkg in self.mro_linearize(root) {
13743            let key = if pkg == "main" {
13744                "AUTOLOAD".to_string()
13745            } else {
13746                format!("{}::AUTOLOAD", pkg)
13747            };
13748            if let Some(s) = self.subs.get(&key) {
13749                return Some(s.clone());
13750            }
13751        }
13752        None
13753    }
13754
13755    /// If an `AUTOLOAD` exists in the invocant's inheritance chain, set `$AUTOLOAD` to the fully
13756    /// qualified missing sub or method name and invoke the handler (same argument list as the
13757    /// missing call). For plain subs, `method_invocant_class` is `None` and the search starts from
13758    /// the package prefix of the missing name (or current package).
13759    pub(crate) fn try_autoload_call(
13760        &mut self,
13761        missing_name: &str,
13762        args: Vec<PerlValue>,
13763        line: usize,
13764        want: WantarrayCtx,
13765        method_invocant_class: Option<&str>,
13766    ) -> Option<ExecResult> {
13767        let pkg = self.current_package();
13768        let full = if missing_name.contains("::") {
13769            missing_name.to_string()
13770        } else {
13771            format!("{}::{}", pkg, missing_name)
13772        };
13773        let start_pkg = method_invocant_class.unwrap_or_else(|| {
13774            full.rsplit_once("::")
13775                .map(|(p, _)| p)
13776                .filter(|p| !p.is_empty())
13777                .unwrap_or("main")
13778        });
13779        let sub = self.resolve_autoload_sub(start_pkg)?;
13780        if let Err(e) = self
13781            .scope
13782            .set_scalar("AUTOLOAD", PerlValue::string(full.clone()))
13783        {
13784            return Some(Err(e.into()));
13785        }
13786        Some(self.call_sub(&sub, args, want, line))
13787    }
13788
13789    pub(crate) fn with_topic_default_args(&self, args: Vec<PerlValue>) -> Vec<PerlValue> {
13790        if args.is_empty() {
13791            vec![self.scope.get_scalar("_").clone()]
13792        } else {
13793            args
13794        }
13795    }
13796
13797    /// `$coderef(...)` / `&$name(...)` / `&$cr` with caller `@_` — shared by tree [`ExprKind::IndirectCall`]
13798    /// and [`crate::bytecode::Op::IndirectCall`].
13799    pub(crate) fn dispatch_indirect_call(
13800        &mut self,
13801        target: PerlValue,
13802        arg_vals: Vec<PerlValue>,
13803        want: WantarrayCtx,
13804        line: usize,
13805    ) -> ExecResult {
13806        if let Some(sub) = target.as_code_ref() {
13807            return self.call_sub(&sub, arg_vals, want, line);
13808        }
13809        if let Some(name) = target.as_str() {
13810            return self.call_named_sub(&name, arg_vals, line, want);
13811        }
13812        Err(PerlError::runtime("Can't use non-code reference as a subroutine", line).into())
13813    }
13814
13815    /// Bare `uniq` / `distinct` (alias of `uniq`) / `shuffle` / `chunked` / `windowed` / `zip` /
13816    /// `sum` / `sum0` /
13817    /// `product` / `min` / `max` / `mean` / `median` / `mode` / `stddev` / `variance` /
13818    /// `any` / `all` / `none` / `first` (Ruby `detect` / `find` parse to `first`; same as `List::Util` after
13819    /// [`crate::list_util::ensure_list_util`]).
13820    pub(crate) fn call_bare_list_util(
13821        &mut self,
13822        name: &str,
13823        args: Vec<PerlValue>,
13824        line: usize,
13825        want: WantarrayCtx,
13826    ) -> ExecResult {
13827        crate::list_util::ensure_list_util(self);
13828        let fq = match name {
13829            "uniq" | "distinct" | "uq" => "List::Util::uniq",
13830            "uniqstr" => "List::Util::uniqstr",
13831            "uniqint" => "List::Util::uniqint",
13832            "uniqnum" => "List::Util::uniqnum",
13833            "shuffle" | "shuf" => "List::Util::shuffle",
13834            "sample" => "List::Util::sample",
13835            "chunked" | "chk" => "List::Util::chunked",
13836            "windowed" | "win" => "List::Util::windowed",
13837            "zip" | "zp" => "List::Util::zip",
13838            "zip_longest" => "List::Util::zip_longest",
13839            "zip_shortest" => "List::Util::zip_shortest",
13840            "mesh" => "List::Util::mesh",
13841            "mesh_longest" => "List::Util::mesh_longest",
13842            "mesh_shortest" => "List::Util::mesh_shortest",
13843            "any" => "List::Util::any",
13844            "all" => "List::Util::all",
13845            "none" => "List::Util::none",
13846            "notall" => "List::Util::notall",
13847            "first" | "fst" => "List::Util::first",
13848            "reduce" | "rd" => "List::Util::reduce",
13849            "reductions" => "List::Util::reductions",
13850            "sum" => "List::Util::sum",
13851            "sum0" => "List::Util::sum0",
13852            "product" => "List::Util::product",
13853            "min" => "List::Util::min",
13854            "max" => "List::Util::max",
13855            "minstr" => "List::Util::minstr",
13856            "maxstr" => "List::Util::maxstr",
13857            "mean" => "List::Util::mean",
13858            "median" | "med" => "List::Util::median",
13859            "mode" => "List::Util::mode",
13860            "stddev" | "std" => "List::Util::stddev",
13861            "variance" | "var" => "List::Util::variance",
13862            "pairs" => "List::Util::pairs",
13863            "unpairs" => "List::Util::unpairs",
13864            "pairkeys" => "List::Util::pairkeys",
13865            "pairvalues" => "List::Util::pairvalues",
13866            "pairgrep" => "List::Util::pairgrep",
13867            "pairmap" => "List::Util::pairmap",
13868            "pairfirst" => "List::Util::pairfirst",
13869            _ => {
13870                return Err(PerlError::runtime(
13871                    format!("internal: not a bare list-util alias: {name}"),
13872                    line,
13873                )
13874                .into());
13875            }
13876        };
13877        let Some(sub) = self.subs.get(fq).cloned() else {
13878            return Err(PerlError::runtime(
13879                format!("internal: missing native stub for {fq}"),
13880                line,
13881            )
13882            .into());
13883        };
13884        let args = self.with_topic_default_args(args);
13885        self.call_sub(&sub, args, want, line)
13886    }
13887
13888    fn call_named_sub(
13889        &mut self,
13890        name: &str,
13891        args: Vec<PerlValue>,
13892        line: usize,
13893        want: WantarrayCtx,
13894    ) -> ExecResult {
13895        if let Some(sub) = self.resolve_sub_by_name(name) {
13896            let args = self.with_topic_default_args(args);
13897            return self.call_sub(&sub, args, want, line);
13898        }
13899        match name {
13900            "uniq" | "distinct" | "uq" | "uniqstr" | "uniqint" | "uniqnum" | "shuffle" | "shuf"
13901            | "sample" | "chunked" | "chk" | "windowed" | "win" | "zip" | "zp" | "zip_shortest"
13902            | "zip_longest" | "mesh" | "mesh_shortest" | "mesh_longest" | "any" | "all"
13903            | "none" | "notall" | "first" | "fst" | "reduce" | "rd" | "reductions" | "sum"
13904            | "sum0" | "product" | "min" | "max" | "minstr" | "maxstr" | "mean" | "median"
13905            | "med" | "mode" | "stddev" | "std" | "variance" | "var" | "pairs" | "unpairs"
13906            | "pairkeys" | "pairvalues" | "pairgrep" | "pairmap" | "pairfirst" => {
13907                self.call_bare_list_util(name, args, line, want)
13908            }
13909            "deque" => {
13910                if !args.is_empty() {
13911                    return Err(PerlError::runtime("deque() takes no arguments", line).into());
13912                }
13913                Ok(PerlValue::deque(Arc::new(Mutex::new(VecDeque::new()))))
13914            }
13915            "defer__internal" => {
13916                if args.len() != 1 {
13917                    return Err(PerlError::runtime(
13918                        "defer__internal expects one coderef argument",
13919                        line,
13920                    )
13921                    .into());
13922                }
13923                self.scope.push_defer(args[0].clone());
13924                Ok(PerlValue::UNDEF)
13925            }
13926            "heap" => {
13927                if args.len() != 1 {
13928                    return Err(
13929                        PerlError::runtime("heap() expects one comparator sub", line).into(),
13930                    );
13931                }
13932                if let Some(sub) = args[0].as_code_ref() {
13933                    Ok(PerlValue::heap(Arc::new(Mutex::new(PerlHeap {
13934                        items: Vec::new(),
13935                        cmp: Arc::clone(&sub),
13936                    }))))
13937                } else {
13938                    Err(PerlError::runtime("heap() requires a code reference", line).into())
13939                }
13940            }
13941            "pipeline" => {
13942                let mut items = Vec::new();
13943                for v in args {
13944                    if let Some(a) = v.as_array_vec() {
13945                        items.extend(a);
13946                    } else {
13947                        items.push(v);
13948                    }
13949                }
13950                Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
13951                    source: items,
13952                    ops: Vec::new(),
13953                    has_scalar_terminal: false,
13954                    par_stream: false,
13955                    streaming: false,
13956                    streaming_workers: 0,
13957                    streaming_buffer: 256,
13958                }))))
13959            }
13960            "par_pipeline" => {
13961                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
13962                    return crate::par_pipeline::run_par_pipeline(self, &args, line)
13963                        .map_err(Into::into);
13964                }
13965                Ok(self.builtin_par_pipeline_stream(&args, line)?)
13966            }
13967            "par_pipeline_stream" => {
13968                if crate::par_pipeline::is_named_par_pipeline_args(&args) {
13969                    return crate::par_pipeline::run_par_pipeline_streaming(self, &args, line)
13970                        .map_err(Into::into);
13971                }
13972                Ok(self.builtin_par_pipeline_stream_new(&args, line)?)
13973            }
13974            "ppool" => {
13975                if args.len() != 1 {
13976                    return Err(PerlError::runtime(
13977                        "ppool() expects one argument (worker count)",
13978                        line,
13979                    )
13980                    .into());
13981                }
13982                crate::ppool::create_pool(args[0].to_int().max(0) as usize).map_err(Into::into)
13983            }
13984            "barrier" => {
13985                if args.len() != 1 {
13986                    return Err(PerlError::runtime(
13987                        "barrier() expects one argument (party count)",
13988                        line,
13989                    )
13990                    .into());
13991                }
13992                let n = args[0].to_int().max(1) as usize;
13993                Ok(PerlValue::barrier(PerlBarrier(Arc::new(Barrier::new(n)))))
13994            }
13995            "cluster" => {
13996                let items = if args.len() == 1 {
13997                    args[0].to_list()
13998                } else {
13999                    args.to_vec()
14000                };
14001                let c = RemoteCluster::from_list_args(&items)
14002                    .map_err(|msg| PerlError::runtime(msg, line))?;
14003                Ok(PerlValue::remote_cluster(Arc::new(c)))
14004            }
14005            _ => {
14006                // Late static binding: static::method() resolves to runtime class of $self
14007                if let Some(method_name) = name.strip_prefix("static::") {
14008                    let self_val = self.scope.get_scalar("self");
14009                    if let Some(c) = self_val.as_class_inst() {
14010                        if let Some((m, _)) = self.find_class_method(&c.def, method_name) {
14011                            if let Some(ref body) = m.body {
14012                                let params = m.params.clone();
14013                                let mut call_args = vec![self_val.clone()];
14014                                call_args.extend(args);
14015                                return match self.call_class_method(body, &params, call_args, line)
14016                                {
14017                                    Ok(v) => Ok(v),
14018                                    Err(FlowOrError::Error(e)) => Err(e.into()),
14019                                    Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14020                                    Err(e) => Err(e),
14021                                };
14022                            }
14023                        }
14024                        return Err(PerlError::runtime(
14025                            format!(
14026                                "static::{} — method not found on class {}",
14027                                method_name, c.def.name
14028                            ),
14029                            line,
14030                        )
14031                        .into());
14032                    }
14033                    return Err(PerlError::runtime(
14034                        "static:: can only be used inside a class method",
14035                        line,
14036                    )
14037                    .into());
14038                }
14039                // Check for struct constructor: Point(x => 1, y => 2) or Point(1, 2)
14040                if let Some(def) = self.struct_defs.get(name).cloned() {
14041                    return self.struct_construct(&def, args, line);
14042                }
14043                // Check for class constructor: Dog(name => "Rex") or Dog("Rex", 5)
14044                if let Some(def) = self.class_defs.get(name).cloned() {
14045                    return self.class_construct(&def, args, line);
14046                }
14047                // Check for enum variant constructor: Color::Red or Maybe::Some(value)
14048                if let Some((enum_name, variant_name)) = name.rsplit_once("::") {
14049                    if let Some(def) = self.enum_defs.get(enum_name).cloned() {
14050                        return self.enum_construct(&def, variant_name, args, line);
14051                    }
14052                }
14053                // Check for static class method or static field: Math::add(...) / Counter::count()
14054                if let Some((class_name, member_name)) = name.rsplit_once("::") {
14055                    if let Some(def) = self.class_defs.get(class_name).cloned() {
14056                        // Static method
14057                        if let Some(m) = def.method(member_name) {
14058                            if m.is_static {
14059                                if let Some(ref body) = m.body {
14060                                    let params = m.params.clone();
14061                                    return match self.call_static_class_method(
14062                                        body,
14063                                        &params,
14064                                        args.clone(),
14065                                        line,
14066                                    ) {
14067                                        Ok(v) => Ok(v),
14068                                        Err(FlowOrError::Error(e)) => Err(e.into()),
14069                                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14070                                        Err(e) => Err(e),
14071                                    };
14072                                }
14073                            }
14074                        }
14075                        // Static field access: getter (0 args) or setter (1 arg)
14076                        if def.static_fields.iter().any(|sf| sf.name == member_name) {
14077                            let key = format!("{}::{}", class_name, member_name);
14078                            match args.len() {
14079                                0 => {
14080                                    let val = self.scope.get_scalar(&key);
14081                                    return Ok(val);
14082                                }
14083                                1 => {
14084                                    let _ = self.scope.set_scalar(&key, args[0].clone());
14085                                    return Ok(args[0].clone());
14086                                }
14087                                _ => {
14088                                    return Err(PerlError::runtime(
14089                                        format!(
14090                                            "static field `{}::{}` takes 0 or 1 arguments",
14091                                            class_name, member_name
14092                                        ),
14093                                        line,
14094                                    )
14095                                    .into());
14096                                }
14097                            }
14098                        }
14099                    }
14100                }
14101                let args = self.with_topic_default_args(args);
14102                if let Some(r) = self.try_autoload_call(name, args, line, want, None) {
14103                    return r;
14104                }
14105                Err(PerlError::runtime(self.undefined_subroutine_call_message(name), line).into())
14106            }
14107        }
14108    }
14109
14110    /// Construct a struct instance from function-call syntax: Point(x => 1, y => 2) or Point(1, 2).
14111    pub(crate) fn struct_construct(
14112        &mut self,
14113        def: &Arc<StructDef>,
14114        args: Vec<PerlValue>,
14115        line: usize,
14116    ) -> ExecResult {
14117        // Detect if args are named (key => value pairs) or positional
14118        // Named: even count and every odd index (0, 2, 4...) looks like a string field name
14119        let is_named = args.len() >= 2
14120            && args.len().is_multiple_of(2)
14121            && args.iter().step_by(2).all(|v| {
14122                let s = v.to_string();
14123                def.field_index(&s).is_some()
14124            });
14125
14126        let provided = if is_named {
14127            // Named construction: Point(x => 1, y => 2)
14128            let mut pairs = Vec::new();
14129            let mut i = 0;
14130            while i + 1 < args.len() {
14131                let k = args[i].to_string();
14132                let v = args[i + 1].clone();
14133                pairs.push((k, v));
14134                i += 2;
14135            }
14136            pairs
14137        } else {
14138            // Positional construction: Point(1, 2) fills fields in declaration order
14139            def.fields
14140                .iter()
14141                .zip(args.iter())
14142                .map(|(f, v)| (f.name.clone(), v.clone()))
14143                .collect()
14144        };
14145
14146        // Evaluate default expressions
14147        let mut defaults = Vec::with_capacity(def.fields.len());
14148        for field in &def.fields {
14149            if let Some(ref expr) = field.default {
14150                let val = self.eval_expr(expr)?;
14151                defaults.push(Some(val));
14152            } else {
14153                defaults.push(None);
14154            }
14155        }
14156
14157        Ok(crate::native_data::struct_new_with_defaults(
14158            def, &provided, &defaults, line,
14159        )?)
14160    }
14161
14162    /// Construct a class instance from function-call syntax: Dog(name => "Rex") or Dog("Rex", 5).
14163    pub(crate) fn class_construct(
14164        &mut self,
14165        def: &Arc<ClassDef>,
14166        args: Vec<PerlValue>,
14167        _line: usize,
14168    ) -> ExecResult {
14169        use crate::value::ClassInstance;
14170
14171        // Prevent instantiation of abstract classes
14172        if def.is_abstract {
14173            return Err(PerlError::runtime(
14174                format!("cannot instantiate abstract class `{}`", def.name),
14175                _line,
14176            )
14177            .into());
14178        }
14179
14180        // Collect all fields from inheritance chain (parent fields first)
14181        let all_fields = self.collect_class_fields(def);
14182
14183        // Check if args are named
14184        let is_named = args.len() >= 2
14185            && args.len().is_multiple_of(2)
14186            && args.iter().step_by(2).all(|v| {
14187                let s = v.to_string();
14188                all_fields.iter().any(|(name, _, _)| name == &s)
14189            });
14190
14191        let provided: Vec<(String, PerlValue)> = if is_named {
14192            let mut pairs = Vec::new();
14193            let mut i = 0;
14194            while i + 1 < args.len() {
14195                let k = args[i].to_string();
14196                let v = args[i + 1].clone();
14197                pairs.push((k, v));
14198                i += 2;
14199            }
14200            pairs
14201        } else {
14202            all_fields
14203                .iter()
14204                .zip(args.iter())
14205                .map(|((name, _, _), v)| (name.clone(), v.clone()))
14206                .collect()
14207        };
14208
14209        // Build values array for all fields (inherited + own) with type checking
14210        let mut values = Vec::with_capacity(all_fields.len());
14211        for (name, default, ty) in &all_fields {
14212            let val = if let Some((_, val)) = provided.iter().find(|(k, _)| k == name) {
14213                val.clone()
14214            } else if let Some(ref expr) = default {
14215                self.eval_expr(expr)?
14216            } else {
14217                PerlValue::UNDEF
14218            };
14219            ty.check_value(&val).map_err(|msg| {
14220                PerlError::type_error(
14221                    format!("class {} field `{}`: {}", def.name, name, msg),
14222                    _line,
14223                )
14224            })?;
14225            values.push(val);
14226        }
14227
14228        let instance = PerlValue::class_inst(Arc::new(ClassInstance::new(Arc::clone(def), values)));
14229
14230        // Call BUILD hooks: parent BUILD first, then child BUILD
14231        let build_chain = self.collect_build_chain(def);
14232        if !build_chain.is_empty() {
14233            for (body, params) in &build_chain {
14234                let call_args = vec![instance.clone()];
14235                match self.call_class_method(body, params, call_args, _line) {
14236                    Ok(_) => {}
14237                    Err(FlowOrError::Flow(Flow::Return(_))) => {}
14238                    Err(e) => return Err(e),
14239                }
14240            }
14241        }
14242
14243        Ok(instance)
14244    }
14245
14246    /// Collect BUILD methods from parent to child order.
14247    fn collect_build_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14248        let mut chain = Vec::new();
14249        // Parent BUILD first
14250        for parent_name in &def.extends {
14251            if let Some(parent_def) = self.class_defs.get(parent_name) {
14252                chain.extend(self.collect_build_chain(parent_def));
14253            }
14254        }
14255        // Own BUILD
14256        if let Some(m) = def.method("BUILD") {
14257            if let Some(ref body) = m.body {
14258                chain.push((body.clone(), m.params.clone()));
14259            }
14260        }
14261        chain
14262    }
14263
14264    /// Collect all fields from a class and its parent hierarchy (parent fields first).
14265    /// Returns (name, default, type, visibility, owning_class_name).
14266    fn collect_class_fields(
14267        &self,
14268        def: &ClassDef,
14269    ) -> Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> {
14270        self.collect_class_fields_full(def)
14271            .into_iter()
14272            .map(|(name, default, ty, _, _)| (name, default, ty))
14273            .collect()
14274    }
14275
14276    /// Like collect_class_fields but includes visibility and owning class name.
14277    fn collect_class_fields_full(
14278        &self,
14279        def: &ClassDef,
14280    ) -> Vec<(
14281        String,
14282        Option<Expr>,
14283        crate::ast::PerlTypeName,
14284        crate::ast::Visibility,
14285        String,
14286    )> {
14287        let mut all_fields = Vec::new();
14288
14289        for parent_name in &def.extends {
14290            if let Some(parent_def) = self.class_defs.get(parent_name) {
14291                let parent_fields = self.collect_class_fields_full(parent_def);
14292                all_fields.extend(parent_fields);
14293            }
14294        }
14295
14296        for field in &def.fields {
14297            all_fields.push((
14298                field.name.clone(),
14299                field.default.clone(),
14300                field.ty.clone(),
14301                field.visibility,
14302                def.name.clone(),
14303            ));
14304        }
14305
14306        all_fields
14307    }
14308
14309    /// Collect all method names from class and parents (deduplicates, child overrides parent).
14310    fn collect_class_method_names(&self, def: &ClassDef, names: &mut Vec<String>) {
14311        // Parent methods first
14312        for parent_name in &def.extends {
14313            if let Some(parent_def) = self.class_defs.get(parent_name) {
14314                self.collect_class_method_names(parent_def, names);
14315            }
14316        }
14317        // Own methods (add if not already present — child overrides parent name)
14318        for m in &def.methods {
14319            if !m.is_static && !names.contains(&m.name) {
14320                names.push(m.name.clone());
14321            }
14322        }
14323    }
14324
14325    /// Collect DESTROY methods from child to parent order (reverse of BUILD).
14326    fn collect_destroy_chain(&self, def: &ClassDef) -> Vec<(Block, Vec<SubSigParam>)> {
14327        let mut chain = Vec::new();
14328        // Own DESTROY first
14329        if let Some(m) = def.method("DESTROY") {
14330            if let Some(ref body) = m.body {
14331                chain.push((body.clone(), m.params.clone()));
14332            }
14333        }
14334        // Then parent DESTROY
14335        for parent_name in &def.extends {
14336            if let Some(parent_def) = self.class_defs.get(parent_name) {
14337                chain.extend(self.collect_destroy_chain(parent_def));
14338            }
14339        }
14340        chain
14341    }
14342
14343    /// Check if `child` class inherits (directly or transitively) from `ancestor`.
14344    fn class_inherits_from(&self, child: &str, ancestor: &str) -> bool {
14345        if let Some(def) = self.class_defs.get(child) {
14346            for parent in &def.extends {
14347                if parent == ancestor || self.class_inherits_from(parent, ancestor) {
14348                    return true;
14349                }
14350            }
14351        }
14352        false
14353    }
14354
14355    /// Find a method in a class or its parent hierarchy (child methods override parent).
14356    fn find_class_method(&self, def: &ClassDef, method: &str) -> Option<(ClassMethod, String)> {
14357        // First check the current class
14358        if let Some(m) = def.method(method) {
14359            return Some((m.clone(), def.name.clone()));
14360        }
14361        // Then check parent classes
14362        for parent_name in &def.extends {
14363            if let Some(parent_def) = self.class_defs.get(parent_name) {
14364                if let Some(result) = self.find_class_method(parent_def, method) {
14365                    return Some(result);
14366                }
14367            }
14368        }
14369        None
14370    }
14371
14372    /// Construct an enum variant: `Enum::Variant` or `Enum::Variant(data)`.
14373    pub(crate) fn enum_construct(
14374        &mut self,
14375        def: &Arc<EnumDef>,
14376        variant_name: &str,
14377        args: Vec<PerlValue>,
14378        line: usize,
14379    ) -> ExecResult {
14380        let variant_idx = def.variant_index(variant_name).ok_or_else(|| {
14381            FlowOrError::Error(PerlError::runtime(
14382                format!("unknown variant `{}` for enum `{}`", variant_name, def.name),
14383                line,
14384            ))
14385        })?;
14386        let variant = &def.variants[variant_idx];
14387        let data = if variant.ty.is_some() {
14388            if args.is_empty() {
14389                return Err(PerlError::runtime(
14390                    format!(
14391                        "enum variant `{}::{}` requires data",
14392                        def.name, variant_name
14393                    ),
14394                    line,
14395                )
14396                .into());
14397            }
14398            if args.len() == 1 {
14399                args.into_iter().next().unwrap()
14400            } else {
14401                PerlValue::array(args)
14402            }
14403        } else {
14404            if !args.is_empty() {
14405                return Err(PerlError::runtime(
14406                    format!(
14407                        "enum variant `{}::{}` does not take data",
14408                        def.name, variant_name
14409                    ),
14410                    line,
14411                )
14412                .into());
14413            }
14414            PerlValue::UNDEF
14415        };
14416        let inst = crate::value::EnumInstance::new(Arc::clone(def), variant_idx, data);
14417        Ok(PerlValue::enum_inst(Arc::new(inst)))
14418    }
14419
14420    /// True if `name` is a registered or standard process-global handle.
14421    pub(crate) fn is_bound_handle(&self, name: &str) -> bool {
14422        matches!(name, "STDIN" | "STDOUT" | "STDERR")
14423            || self.input_handles.contains_key(name)
14424            || self.output_handles.contains_key(name)
14425            || self.io_file_slots.contains_key(name)
14426            || self.pipe_children.contains_key(name)
14427    }
14428
14429    /// IO::File-style methods on handle values (`$fh->print`, `STDOUT->say`, …).
14430    pub(crate) fn io_handle_method(
14431        &mut self,
14432        name: &str,
14433        method: &str,
14434        args: &[PerlValue],
14435        line: usize,
14436    ) -> PerlResult<PerlValue> {
14437        match method {
14438            "print" => self.io_handle_print(name, args, false, line),
14439            "say" => self.io_handle_print(name, args, true, line),
14440            "printf" => self.io_handle_printf(name, args, line),
14441            "getline" | "readline" => {
14442                if !args.is_empty() {
14443                    return Err(PerlError::runtime(
14444                        format!("{}: too many arguments", method),
14445                        line,
14446                    ));
14447                }
14448                self.readline_builtin_execute(Some(name))
14449            }
14450            "close" => {
14451                if !args.is_empty() {
14452                    return Err(PerlError::runtime("close: too many arguments", line));
14453                }
14454                self.close_builtin_execute(name.to_string())
14455            }
14456            "eof" => {
14457                if !args.is_empty() {
14458                    return Err(PerlError::runtime("eof: too many arguments", line));
14459                }
14460                let at_eof = !self.has_input_handle(name);
14461                Ok(PerlValue::integer(if at_eof { 1 } else { 0 }))
14462            }
14463            "getc" => {
14464                if !args.is_empty() {
14465                    return Err(PerlError::runtime("getc: too many arguments", line));
14466                }
14467                match crate::builtins::try_builtin(
14468                    self,
14469                    "getc",
14470                    &[PerlValue::string(name.to_string())],
14471                    line,
14472                ) {
14473                    Some(r) => r,
14474                    None => Err(PerlError::runtime("getc: not available", line)),
14475                }
14476            }
14477            "binmode" => match crate::builtins::try_builtin(
14478                self,
14479                "binmode",
14480                &[PerlValue::string(name.to_string())],
14481                line,
14482            ) {
14483                Some(r) => r,
14484                None => Err(PerlError::runtime("binmode: not available", line)),
14485            },
14486            "fileno" => match crate::builtins::try_builtin(
14487                self,
14488                "fileno",
14489                &[PerlValue::string(name.to_string())],
14490                line,
14491            ) {
14492                Some(r) => r,
14493                None => Err(PerlError::runtime("fileno: not available", line)),
14494            },
14495            "flush" => {
14496                if !args.is_empty() {
14497                    return Err(PerlError::runtime("flush: too many arguments", line));
14498                }
14499                self.io_handle_flush(name, line)
14500            }
14501            _ => Err(PerlError::runtime(
14502                format!("Unknown method for filehandle: {}", method),
14503                line,
14504            )),
14505        }
14506    }
14507
14508    fn io_handle_flush(&mut self, handle_name: &str, line: usize) -> PerlResult<PerlValue> {
14509        match handle_name {
14510            "STDOUT" => {
14511                let _ = IoWrite::flush(&mut io::stdout());
14512            }
14513            "STDERR" => {
14514                let _ = IoWrite::flush(&mut io::stderr());
14515            }
14516            name => {
14517                if let Some(writer) = self.output_handles.get_mut(name) {
14518                    let _ = IoWrite::flush(&mut *writer);
14519                } else {
14520                    return Err(PerlError::runtime(
14521                        format!("flush on unopened filehandle {}", name),
14522                        line,
14523                    ));
14524                }
14525            }
14526        }
14527        Ok(PerlValue::integer(1))
14528    }
14529
14530    fn io_handle_print(
14531        &mut self,
14532        handle_name: &str,
14533        args: &[PerlValue],
14534        newline: bool,
14535        line: usize,
14536    ) -> PerlResult<PerlValue> {
14537        if newline && (self.feature_bits & FEAT_SAY) == 0 {
14538            return Err(PerlError::runtime(
14539                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
14540                line,
14541            ));
14542        }
14543        let mut output = String::new();
14544        if args.is_empty() {
14545            // Match Perl: print with no LIST prints $_ (same overload rules as other args here: `to_string`).
14546            output.push_str(&self.scope.get_scalar("_").to_string());
14547        } else {
14548            for (i, val) in args.iter().enumerate() {
14549                if i > 0 && !self.ofs.is_empty() {
14550                    output.push_str(&self.ofs);
14551                }
14552                output.push_str(&val.to_string());
14553            }
14554        }
14555        if newline {
14556            output.push('\n');
14557        }
14558        output.push_str(&self.ors);
14559
14560        self.write_formatted_print(handle_name, &output, line)?;
14561        Ok(PerlValue::integer(1))
14562    }
14563
14564    /// Write a fully formatted `print`/`say` record (`LIST`, optional `say` newline, `$\`) to a handle.
14565    /// `handle_name` must already be [`Self::resolve_io_handle_name`]-resolved.
14566    pub(crate) fn write_formatted_print(
14567        &mut self,
14568        handle_name: &str,
14569        output: &str,
14570        line: usize,
14571    ) -> PerlResult<()> {
14572        match handle_name {
14573            "STDOUT" => {
14574                if !self.suppress_stdout {
14575                    print!("{}", output);
14576                    if self.output_autoflush {
14577                        let _ = io::stdout().flush();
14578                    }
14579                }
14580            }
14581            "STDERR" => {
14582                eprint!("{}", output);
14583                let _ = io::stderr().flush();
14584            }
14585            name => {
14586                if let Some(writer) = self.output_handles.get_mut(name) {
14587                    let _ = writer.write_all(output.as_bytes());
14588                    if self.output_autoflush {
14589                        let _ = writer.flush();
14590                    }
14591                } else {
14592                    return Err(PerlError::runtime(
14593                        format!("print on unopened filehandle {}", name),
14594                        line,
14595                    ));
14596                }
14597            }
14598        }
14599        Ok(())
14600    }
14601
14602    fn io_handle_printf(
14603        &mut self,
14604        handle_name: &str,
14605        args: &[PerlValue],
14606        line: usize,
14607    ) -> PerlResult<PerlValue> {
14608        let (fmt, rest): (String, &[PerlValue]) = if args.is_empty() {
14609            let s = match self.stringify_value(self.scope.get_scalar("_").clone(), line) {
14610                Ok(s) => s,
14611                Err(FlowOrError::Error(e)) => return Err(e),
14612                Err(FlowOrError::Flow(_)) => {
14613                    return Err(PerlError::runtime(
14614                        "printf: unexpected control flow in sprintf",
14615                        line,
14616                    ));
14617                }
14618            };
14619            (s, &[])
14620        } else {
14621            (args[0].to_string(), &args[1..])
14622        };
14623        let output = match self.perl_sprintf_stringify(&fmt, rest, line) {
14624            Ok(s) => s,
14625            Err(FlowOrError::Error(e)) => return Err(e),
14626            Err(FlowOrError::Flow(_)) => {
14627                return Err(PerlError::runtime(
14628                    "printf: unexpected control flow in sprintf",
14629                    line,
14630                ));
14631            }
14632        };
14633        match handle_name {
14634            "STDOUT" => {
14635                if !self.suppress_stdout {
14636                    print!("{}", output);
14637                    if self.output_autoflush {
14638                        let _ = IoWrite::flush(&mut io::stdout());
14639                    }
14640                }
14641            }
14642            "STDERR" => {
14643                eprint!("{}", output);
14644                let _ = IoWrite::flush(&mut io::stderr());
14645            }
14646            name => {
14647                if let Some(writer) = self.output_handles.get_mut(name) {
14648                    let _ = writer.write_all(output.as_bytes());
14649                    if self.output_autoflush {
14650                        let _ = writer.flush();
14651                    }
14652                } else {
14653                    return Err(PerlError::runtime(
14654                        format!("printf on unopened filehandle {}", name),
14655                        line,
14656                    ));
14657                }
14658            }
14659        }
14660        Ok(PerlValue::integer(1))
14661    }
14662
14663    /// `deque` / `heap` method dispatch (`$q->push_back`, `$pq->pop`, …).
14664    pub(crate) fn try_native_method(
14665        &mut self,
14666        receiver: &PerlValue,
14667        method: &str,
14668        args: &[PerlValue],
14669        line: usize,
14670    ) -> Option<PerlResult<PerlValue>> {
14671        if let Some(name) = receiver.as_io_handle_name() {
14672            return Some(self.io_handle_method(&name, method, args, line));
14673        }
14674        if let Some(ref s) = receiver.as_str() {
14675            if self.is_bound_handle(s) {
14676                return Some(self.io_handle_method(s, method, args, line));
14677            }
14678        }
14679        if let Some(c) = receiver.as_sqlite_conn() {
14680            return Some(crate::native_data::sqlite_dispatch(&c, method, args, line));
14681        }
14682        if let Some(s) = receiver.as_struct_inst() {
14683            // Field access: $p->x or $p->x(value)
14684            if let Some(idx) = s.def.field_index(method) {
14685                match args.len() {
14686                    0 => {
14687                        return Some(Ok(s.get_field(idx).unwrap_or(PerlValue::UNDEF)));
14688                    }
14689                    1 => {
14690                        let field = &s.def.fields[idx];
14691                        let new_val = args[0].clone();
14692                        if let Err(msg) = field.ty.check_value(&new_val) {
14693                            return Some(Err(PerlError::type_error(
14694                                format!("struct {} field `{}`: {}", s.def.name, field.name, msg),
14695                                line,
14696                            )));
14697                        }
14698                        s.set_field(idx, new_val.clone());
14699                        return Some(Ok(new_val));
14700                    }
14701                    _ => {
14702                        return Some(Err(PerlError::runtime(
14703                            format!(
14704                                "struct field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
14705                                method,
14706                                args.len()
14707                            ),
14708                            line,
14709                        )));
14710                    }
14711                }
14712            }
14713            // Built-in struct methods
14714            match method {
14715                "with" => {
14716                    // Functional update: $p->with(x => 5) returns new instance with changed field
14717                    let mut new_values = s.get_values();
14718                    let mut i = 0;
14719                    while i + 1 < args.len() {
14720                        let k = args[i].to_string();
14721                        let v = args[i + 1].clone();
14722                        if let Some(idx) = s.def.field_index(&k) {
14723                            let field = &s.def.fields[idx];
14724                            if let Err(msg) = field.ty.check_value(&v) {
14725                                return Some(Err(PerlError::type_error(
14726                                    format!(
14727                                        "struct {} field `{}`: {}",
14728                                        s.def.name, field.name, msg
14729                                    ),
14730                                    line,
14731                                )));
14732                            }
14733                            new_values[idx] = v;
14734                        } else {
14735                            return Some(Err(PerlError::runtime(
14736                                format!("struct {}: unknown field `{}`", s.def.name, k),
14737                                line,
14738                            )));
14739                        }
14740                        i += 2;
14741                    }
14742                    return Some(Ok(PerlValue::struct_inst(Arc::new(
14743                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
14744                    ))));
14745                }
14746                "to_hash" => {
14747                    // Destructure to hash: $p->to_hash returns { x => ..., y => ... }
14748                    if !args.is_empty() {
14749                        return Some(Err(PerlError::runtime(
14750                            "struct to_hash takes no arguments",
14751                            line,
14752                        )));
14753                    }
14754                    let mut map = IndexMap::new();
14755                    let values = s.get_values();
14756                    for (i, field) in s.def.fields.iter().enumerate() {
14757                        map.insert(field.name.clone(), values[i].clone());
14758                    }
14759                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
14760                }
14761                "fields" => {
14762                    // Field list: $p->fields returns field names
14763                    if !args.is_empty() {
14764                        return Some(Err(PerlError::runtime(
14765                            "struct fields takes no arguments",
14766                            line,
14767                        )));
14768                    }
14769                    let names: Vec<PerlValue> = s
14770                        .def
14771                        .fields
14772                        .iter()
14773                        .map(|f| PerlValue::string(f.name.clone()))
14774                        .collect();
14775                    return Some(Ok(PerlValue::array(names)));
14776                }
14777                "clone" => {
14778                    // Clone: $p->clone deep copies
14779                    if !args.is_empty() {
14780                        return Some(Err(PerlError::runtime(
14781                            "struct clone takes no arguments",
14782                            line,
14783                        )));
14784                    }
14785                    let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
14786                    return Some(Ok(PerlValue::struct_inst(Arc::new(
14787                        crate::value::StructInstance::new(Arc::clone(&s.def), new_values),
14788                    ))));
14789                }
14790                _ => {}
14791            }
14792            // User-defined struct method
14793            if let Some(m) = s.def.method(method) {
14794                let body = m.body.clone();
14795                let params = m.params.clone();
14796                // Build args: $self is the receiver, then the passed args
14797                let mut call_args = vec![receiver.clone()];
14798                call_args.extend(args.iter().cloned());
14799                return Some(
14800                    match self.call_struct_method(&body, &params, call_args, line) {
14801                        Ok(v) => Ok(v),
14802                        Err(FlowOrError::Error(e)) => Err(e),
14803                        Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
14804                        Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
14805                            "unexpected control flow in struct method",
14806                            line,
14807                        )),
14808                    },
14809                );
14810            }
14811            return None;
14812        }
14813        // Class instance method dispatch
14814        if let Some(c) = receiver.as_class_inst() {
14815            // Collect all fields from inheritance chain (with visibility)
14816            let all_fields_full = self.collect_class_fields_full(&c.def);
14817            let all_fields: Vec<(String, Option<Expr>, crate::ast::PerlTypeName)> = all_fields_full
14818                .iter()
14819                .map(|(n, d, t, _, _)| (n.clone(), d.clone(), t.clone()))
14820                .collect();
14821
14822            // Field access: $obj->name or $obj->name(value)
14823            if let Some(idx) = all_fields_full
14824                .iter()
14825                .position(|(name, _, _, _, _)| name == method)
14826            {
14827                let (_, _, ref ty, vis, ref owner_class) = all_fields_full[idx];
14828
14829                // Enforce field visibility
14830                match vis {
14831                    crate::ast::Visibility::Private => {
14832                        // Only accessible from within the owning class's methods
14833                        let caller_class = self
14834                            .scope
14835                            .get_scalar("self")
14836                            .as_class_inst()
14837                            .map(|ci| ci.def.name.clone());
14838                        if caller_class.as_deref() != Some(owner_class.as_str()) {
14839                            return Some(Err(PerlError::runtime(
14840                                format!("field `{}` of class {} is private", method, owner_class),
14841                                line,
14842                            )));
14843                        }
14844                    }
14845                    crate::ast::Visibility::Protected => {
14846                        // Accessible from owning class or subclasses
14847                        let caller_class = self
14848                            .scope
14849                            .get_scalar("self")
14850                            .as_class_inst()
14851                            .map(|ci| ci.def.name.clone());
14852                        let allowed = caller_class.as_deref().is_some_and(|caller| {
14853                            caller == owner_class || self.class_inherits_from(caller, owner_class)
14854                        });
14855                        if !allowed {
14856                            return Some(Err(PerlError::runtime(
14857                                format!("field `{}` of class {} is protected", method, owner_class),
14858                                line,
14859                            )));
14860                        }
14861                    }
14862                    crate::ast::Visibility::Public => {}
14863                }
14864
14865                match args.len() {
14866                    0 => {
14867                        return Some(Ok(c.get_field(idx).unwrap_or(PerlValue::UNDEF)));
14868                    }
14869                    1 => {
14870                        let new_val = args[0].clone();
14871                        if let Err(msg) = ty.check_value(&new_val) {
14872                            return Some(Err(PerlError::type_error(
14873                                format!("class {} field `{}`: {}", c.def.name, method, msg),
14874                                line,
14875                            )));
14876                        }
14877                        c.set_field(idx, new_val.clone());
14878                        return Some(Ok(new_val));
14879                    }
14880                    _ => {
14881                        return Some(Err(PerlError::runtime(
14882                            format!(
14883                                "class field `{}` takes 0 arguments (getter) or 1 argument (setter), got {}",
14884                                method,
14885                                args.len()
14886                            ),
14887                            line,
14888                        )));
14889                    }
14890                }
14891            }
14892            // Built-in class methods (use all_fields for inheritance)
14893            match method {
14894                "with" => {
14895                    let mut new_values = c.get_values();
14896                    let mut i = 0;
14897                    while i + 1 < args.len() {
14898                        let k = args[i].to_string();
14899                        let v = args[i + 1].clone();
14900                        if let Some(idx) = all_fields.iter().position(|(name, _, _)| name == &k) {
14901                            let (_, _, ref ty) = all_fields[idx];
14902                            if let Err(msg) = ty.check_value(&v) {
14903                                return Some(Err(PerlError::type_error(
14904                                    format!("class {} field `{}`: {}", c.def.name, k, msg),
14905                                    line,
14906                                )));
14907                            }
14908                            new_values[idx] = v;
14909                        } else {
14910                            return Some(Err(PerlError::runtime(
14911                                format!("class {}: unknown field `{}`", c.def.name, k),
14912                                line,
14913                            )));
14914                        }
14915                        i += 2;
14916                    }
14917                    return Some(Ok(PerlValue::class_inst(Arc::new(
14918                        crate::value::ClassInstance::new(Arc::clone(&c.def), new_values),
14919                    ))));
14920                }
14921                "to_hash" => {
14922                    if !args.is_empty() {
14923                        return Some(Err(PerlError::runtime(
14924                            "class to_hash takes no arguments",
14925                            line,
14926                        )));
14927                    }
14928                    let mut map = IndexMap::new();
14929                    let values = c.get_values();
14930                    for (i, (name, _, _)) in all_fields.iter().enumerate() {
14931                        if let Some(v) = values.get(i) {
14932                            map.insert(name.clone(), v.clone());
14933                        }
14934                    }
14935                    return Some(Ok(PerlValue::hash_ref(Arc::new(RwLock::new(map)))));
14936                }
14937                "fields" => {
14938                    if !args.is_empty() {
14939                        return Some(Err(PerlError::runtime(
14940                            "class fields takes no arguments",
14941                            line,
14942                        )));
14943                    }
14944                    let names: Vec<PerlValue> = all_fields
14945                        .iter()
14946                        .map(|(name, _, _)| PerlValue::string(name.clone()))
14947                        .collect();
14948                    return Some(Ok(PerlValue::array(names)));
14949                }
14950                "clone" => {
14951                    if !args.is_empty() {
14952                        return Some(Err(PerlError::runtime(
14953                            "class clone takes no arguments",
14954                            line,
14955                        )));
14956                    }
14957                    let new_values = c.get_values().iter().map(|v| v.deep_clone()).collect();
14958                    return Some(Ok(PerlValue::class_inst(Arc::new(
14959                        crate::value::ClassInstance::new(Arc::clone(&c.def), new_values),
14960                    ))));
14961                }
14962                "isa" => {
14963                    if args.len() != 1 {
14964                        return Some(Err(PerlError::runtime("isa requires one argument", line)));
14965                    }
14966                    let class_name = args[0].to_string();
14967                    let is_a = c.def.name == class_name || c.def.extends.contains(&class_name);
14968                    return Some(Ok(if is_a {
14969                        PerlValue::integer(1)
14970                    } else {
14971                        PerlValue::string(String::new())
14972                    }));
14973                }
14974                "does" => {
14975                    if args.len() != 1 {
14976                        return Some(Err(PerlError::runtime("does requires one argument", line)));
14977                    }
14978                    let trait_name = args[0].to_string();
14979                    let implements = c.def.implements.contains(&trait_name);
14980                    return Some(Ok(if implements {
14981                        PerlValue::integer(1)
14982                    } else {
14983                        PerlValue::string(String::new())
14984                    }));
14985                }
14986                "methods" => {
14987                    if !args.is_empty() {
14988                        return Some(Err(PerlError::runtime("methods takes no arguments", line)));
14989                    }
14990                    let mut names = Vec::new();
14991                    self.collect_class_method_names(&c.def, &mut names);
14992                    let values: Vec<PerlValue> = names.into_iter().map(PerlValue::string).collect();
14993                    return Some(Ok(PerlValue::array(values)));
14994                }
14995                "superclass" => {
14996                    if !args.is_empty() {
14997                        return Some(Err(PerlError::runtime(
14998                            "superclass takes no arguments",
14999                            line,
15000                        )));
15001                    }
15002                    let parents: Vec<PerlValue> = c
15003                        .def
15004                        .extends
15005                        .iter()
15006                        .map(|s| PerlValue::string(s.clone()))
15007                        .collect();
15008                    return Some(Ok(PerlValue::array(parents)));
15009                }
15010                "destroy" => {
15011                    // Explicit destructor call — runs DESTROY chain child-first
15012                    let destroy_chain = self.collect_destroy_chain(&c.def);
15013                    for (body, params) in &destroy_chain {
15014                        let call_args = vec![receiver.clone()];
15015                        match self.call_class_method(body, params, call_args, line) {
15016                            Ok(_) => {}
15017                            Err(FlowOrError::Flow(Flow::Return(_))) => {}
15018                            Err(FlowOrError::Error(e)) => return Some(Err(e)),
15019                            Err(_) => {}
15020                        }
15021                    }
15022                    return Some(Ok(PerlValue::UNDEF));
15023                }
15024                _ => {}
15025            }
15026            // User-defined class method (search inheritance chain)
15027            if let Some((m, ref owner_class)) = self.find_class_method(&c.def, method) {
15028                // Check visibility
15029                match m.visibility {
15030                    crate::ast::Visibility::Private => {
15031                        let caller_class = self
15032                            .scope
15033                            .get_scalar("self")
15034                            .as_class_inst()
15035                            .map(|ci| ci.def.name.clone());
15036                        if caller_class.as_deref() != Some(owner_class.as_str()) {
15037                            return Some(Err(PerlError::runtime(
15038                                format!("method `{}` of class {} is private", method, owner_class),
15039                                line,
15040                            )));
15041                        }
15042                    }
15043                    crate::ast::Visibility::Protected => {
15044                        let caller_class = self
15045                            .scope
15046                            .get_scalar("self")
15047                            .as_class_inst()
15048                            .map(|ci| ci.def.name.clone());
15049                        let allowed = caller_class.as_deref().is_some_and(|caller| {
15050                            caller == owner_class.as_str()
15051                                || self.class_inherits_from(caller, owner_class)
15052                        });
15053                        if !allowed {
15054                            return Some(Err(PerlError::runtime(
15055                                format!(
15056                                    "method `{}` of class {} is protected",
15057                                    method, owner_class
15058                                ),
15059                                line,
15060                            )));
15061                        }
15062                    }
15063                    crate::ast::Visibility::Public => {}
15064                }
15065                if let Some(ref body) = m.body {
15066                    let params = m.params.clone();
15067                    let mut call_args = vec![receiver.clone()];
15068                    call_args.extend(args.iter().cloned());
15069                    return Some(
15070                        match self.call_class_method(body, &params, call_args, line) {
15071                            Ok(v) => Ok(v),
15072                            Err(FlowOrError::Error(e)) => Err(e),
15073                            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
15074                            Err(FlowOrError::Flow(_)) => Err(PerlError::runtime(
15075                                "unexpected control flow in class method",
15076                                line,
15077                            )),
15078                        },
15079                    );
15080                }
15081            }
15082            return None;
15083        }
15084        if let Some(d) = receiver.as_dataframe() {
15085            return Some(self.dataframe_method(d, method, args, line));
15086        }
15087        if let Some(s) = crate::value::set_payload(receiver) {
15088            return Some(self.set_method(s, method, args, line));
15089        }
15090        if let Some(d) = receiver.as_deque() {
15091            return Some(self.deque_method(d, method, args, line));
15092        }
15093        if let Some(h) = receiver.as_heap_pq() {
15094            return Some(self.heap_method(h, method, args, line));
15095        }
15096        if let Some(p) = receiver.as_pipeline() {
15097            return Some(self.pipeline_method(p, method, args, line));
15098        }
15099        if let Some(c) = receiver.as_capture() {
15100            return Some(self.capture_method(c, method, args, line));
15101        }
15102        if let Some(p) = receiver.as_ppool() {
15103            return Some(self.ppool_method(p, method, args, line));
15104        }
15105        if let Some(b) = receiver.as_barrier() {
15106            return Some(self.barrier_method(b, method, args, line));
15107        }
15108        if let Some(g) = receiver.as_generator() {
15109            if method == "next" {
15110                if !args.is_empty() {
15111                    return Some(Err(PerlError::runtime(
15112                        "generator->next takes no arguments",
15113                        line,
15114                    )));
15115                }
15116                return Some(self.generator_next(&g));
15117            }
15118            return None;
15119        }
15120        if let Some(arc) = receiver.as_atomic_arc() {
15121            let inner = arc.lock().clone();
15122            if let Some(d) = inner.as_deque() {
15123                return Some(self.deque_method(d, method, args, line));
15124            }
15125            if let Some(h) = inner.as_heap_pq() {
15126                return Some(self.heap_method(h, method, args, line));
15127            }
15128        }
15129        None
15130    }
15131
15132    /// `dataframe(path)` — `filter`, `group_by`, `sum`, `nrow`, `ncol`.
15133    fn dataframe_method(
15134        &mut self,
15135        d: Arc<Mutex<PerlDataFrame>>,
15136        method: &str,
15137        args: &[PerlValue],
15138        line: usize,
15139    ) -> PerlResult<PerlValue> {
15140        match method {
15141            "nrow" | "nrows" => {
15142                if !args.is_empty() {
15143                    return Err(PerlError::runtime(
15144                        format!("dataframe {} takes no arguments", method),
15145                        line,
15146                    ));
15147                }
15148                Ok(PerlValue::integer(d.lock().nrows() as i64))
15149            }
15150            "ncol" | "ncols" => {
15151                if !args.is_empty() {
15152                    return Err(PerlError::runtime(
15153                        format!("dataframe {} takes no arguments", method),
15154                        line,
15155                    ));
15156                }
15157                Ok(PerlValue::integer(d.lock().ncols() as i64))
15158            }
15159            "filter" => {
15160                if args.len() != 1 {
15161                    return Err(PerlError::runtime(
15162                        "dataframe filter expects 1 argument (sub)",
15163                        line,
15164                    ));
15165                }
15166                let Some(sub) = args[0].as_code_ref() else {
15167                    return Err(PerlError::runtime(
15168                        "dataframe filter expects a code reference",
15169                        line,
15170                    ));
15171                };
15172                let df_guard = d.lock();
15173                let n = df_guard.nrows();
15174                let mut keep = vec![false; n];
15175                for (r, row_keep) in keep.iter_mut().enumerate().take(n) {
15176                    let row = df_guard.row_hashref(r);
15177                    self.scope_push_hook();
15178                    self.scope.set_topic(row);
15179                    if let Some(ref env) = sub.closure_env {
15180                        self.scope.restore_capture(env);
15181                    }
15182                    let pass = match self.exec_block_no_scope(&sub.body) {
15183                        Ok(v) => v.is_true(),
15184                        Err(_) => false,
15185                    };
15186                    self.scope_pop_hook();
15187                    *row_keep = pass;
15188                }
15189                let columns = df_guard.columns.clone();
15190                let cols: Vec<Vec<PerlValue>> = (0..df_guard.ncols())
15191                    .map(|i| {
15192                        let mut out = Vec::new();
15193                        for (r, pass_row) in keep.iter().enumerate().take(n) {
15194                            if *pass_row {
15195                                out.push(df_guard.cols[i][r].clone());
15196                            }
15197                        }
15198                        out
15199                    })
15200                    .collect();
15201                let group_by = df_guard.group_by.clone();
15202                drop(df_guard);
15203                let new_df = PerlDataFrame {
15204                    columns,
15205                    cols,
15206                    group_by,
15207                };
15208                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
15209            }
15210            "group_by" => {
15211                if args.len() != 1 {
15212                    return Err(PerlError::runtime(
15213                        "dataframe group_by expects 1 column name",
15214                        line,
15215                    ));
15216                }
15217                let key = args[0].to_string();
15218                let inner = d.lock();
15219                if inner.col_index(&key).is_none() {
15220                    return Err(PerlError::runtime(
15221                        format!("dataframe group_by: unknown column \"{}\"", key),
15222                        line,
15223                    ));
15224                }
15225                let new_df = PerlDataFrame {
15226                    columns: inner.columns.clone(),
15227                    cols: inner.cols.clone(),
15228                    group_by: Some(key),
15229                };
15230                Ok(PerlValue::dataframe(Arc::new(Mutex::new(new_df))))
15231            }
15232            "sum" => {
15233                if args.len() != 1 {
15234                    return Err(PerlError::runtime(
15235                        "dataframe sum expects 1 column name",
15236                        line,
15237                    ));
15238                }
15239                let col_name = args[0].to_string();
15240                let inner = d.lock();
15241                let val_idx = inner.col_index(&col_name).ok_or_else(|| {
15242                    PerlError::runtime(
15243                        format!("dataframe sum: unknown column \"{}\"", col_name),
15244                        line,
15245                    )
15246                })?;
15247                match &inner.group_by {
15248                    Some(gcol) => {
15249                        let gi = inner.col_index(gcol).ok_or_else(|| {
15250                            PerlError::runtime(
15251                                format!("dataframe sum: unknown group column \"{}\"", gcol),
15252                                line,
15253                            )
15254                        })?;
15255                        let mut acc: IndexMap<String, f64> = IndexMap::new();
15256                        for r in 0..inner.nrows() {
15257                            let k = inner.cols[gi][r].to_string();
15258                            let v = inner.cols[val_idx][r].to_number();
15259                            *acc.entry(k).or_insert(0.0) += v;
15260                        }
15261                        let keys: Vec<String> = acc.keys().cloned().collect();
15262                        let sums: Vec<f64> = acc.values().copied().collect();
15263                        let cols = vec![
15264                            keys.into_iter().map(PerlValue::string).collect(),
15265                            sums.into_iter().map(PerlValue::float).collect(),
15266                        ];
15267                        let columns = vec![gcol.clone(), format!("sum_{}", col_name)];
15268                        let out = PerlDataFrame {
15269                            columns,
15270                            cols,
15271                            group_by: None,
15272                        };
15273                        Ok(PerlValue::dataframe(Arc::new(Mutex::new(out))))
15274                    }
15275                    None => {
15276                        let total: f64 = (0..inner.nrows())
15277                            .map(|r| inner.cols[val_idx][r].to_number())
15278                            .sum();
15279                        Ok(PerlValue::float(total))
15280                    }
15281                }
15282            }
15283            _ => Err(PerlError::runtime(
15284                format!("Unknown method for dataframe: {}", method),
15285                line,
15286            )),
15287        }
15288    }
15289
15290    /// Native `Set` values (`set(LIST)`, `Set->new`, `$a | $b`): membership and views (immutable).
15291    fn set_method(
15292        &self,
15293        s: Arc<crate::value::PerlSet>,
15294        method: &str,
15295        args: &[PerlValue],
15296        line: usize,
15297    ) -> PerlResult<PerlValue> {
15298        match method {
15299            "has" | "contains" | "member" => {
15300                if args.len() != 1 {
15301                    return Err(PerlError::runtime(
15302                        "set->has expects one argument (element)",
15303                        line,
15304                    ));
15305                }
15306                let k = crate::value::set_member_key(&args[0]);
15307                Ok(PerlValue::integer(if s.contains_key(&k) { 1 } else { 0 }))
15308            }
15309            "size" | "len" | "count" => {
15310                if !args.is_empty() {
15311                    return Err(PerlError::runtime("set->size takes no arguments", line));
15312                }
15313                Ok(PerlValue::integer(s.len() as i64))
15314            }
15315            "values" | "list" | "elements" => {
15316                if !args.is_empty() {
15317                    return Err(PerlError::runtime("set->values takes no arguments", line));
15318                }
15319                Ok(PerlValue::array(s.values().cloned().collect()))
15320            }
15321            _ => Err(PerlError::runtime(
15322                format!("Unknown method for set: {}", method),
15323                line,
15324            )),
15325        }
15326    }
15327
15328    fn deque_method(
15329        &mut self,
15330        d: Arc<Mutex<VecDeque<PerlValue>>>,
15331        method: &str,
15332        args: &[PerlValue],
15333        line: usize,
15334    ) -> PerlResult<PerlValue> {
15335        match method {
15336            "push_back" => {
15337                if args.len() != 1 {
15338                    return Err(PerlError::runtime("push_back expects 1 argument", line));
15339                }
15340                d.lock().push_back(args[0].clone());
15341                Ok(PerlValue::integer(d.lock().len() as i64))
15342            }
15343            "push_front" => {
15344                if args.len() != 1 {
15345                    return Err(PerlError::runtime("push_front expects 1 argument", line));
15346                }
15347                d.lock().push_front(args[0].clone());
15348                Ok(PerlValue::integer(d.lock().len() as i64))
15349            }
15350            "pop_back" => Ok(d.lock().pop_back().unwrap_or(PerlValue::UNDEF)),
15351            "pop_front" => Ok(d.lock().pop_front().unwrap_or(PerlValue::UNDEF)),
15352            "size" | "len" => Ok(PerlValue::integer(d.lock().len() as i64)),
15353            _ => Err(PerlError::runtime(
15354                format!("Unknown method for deque: {}", method),
15355                line,
15356            )),
15357        }
15358    }
15359
15360    fn heap_method(
15361        &mut self,
15362        h: Arc<Mutex<PerlHeap>>,
15363        method: &str,
15364        args: &[PerlValue],
15365        line: usize,
15366    ) -> PerlResult<PerlValue> {
15367        match method {
15368            "push" => {
15369                if args.len() != 1 {
15370                    return Err(PerlError::runtime("heap push expects 1 argument", line));
15371                }
15372                let mut g = h.lock();
15373                let n = g.items.len();
15374                g.items.push(args[0].clone());
15375                let cmp = g.cmp.clone();
15376                drop(g);
15377                let mut g = h.lock();
15378                self.heap_sift_up(&mut g.items, &cmp, n);
15379                Ok(PerlValue::integer(g.items.len() as i64))
15380            }
15381            "pop" => {
15382                let mut g = h.lock();
15383                if g.items.is_empty() {
15384                    return Ok(PerlValue::UNDEF);
15385                }
15386                let cmp = g.cmp.clone();
15387                let n = g.items.len();
15388                g.items.swap(0, n - 1);
15389                let v = g.items.pop().unwrap();
15390                if !g.items.is_empty() {
15391                    self.heap_sift_down(&mut g.items, &cmp, 0);
15392                }
15393                Ok(v)
15394            }
15395            "peek" => Ok(h.lock().items.first().cloned().unwrap_or(PerlValue::UNDEF)),
15396            _ => Err(PerlError::runtime(
15397                format!("Unknown method for heap: {}", method),
15398                line,
15399            )),
15400        }
15401    }
15402
15403    fn ppool_method(
15404        &mut self,
15405        pool: PerlPpool,
15406        method: &str,
15407        args: &[PerlValue],
15408        line: usize,
15409    ) -> PerlResult<PerlValue> {
15410        match method {
15411            "submit" => pool.submit(self, args, line),
15412            "collect" => {
15413                if !args.is_empty() {
15414                    return Err(PerlError::runtime("collect() takes no arguments", line));
15415                }
15416                pool.collect(line)
15417            }
15418            _ => Err(PerlError::runtime(
15419                format!("Unknown method for ppool: {}", method),
15420                line,
15421            )),
15422        }
15423    }
15424
15425    fn barrier_method(
15426        &self,
15427        barrier: PerlBarrier,
15428        method: &str,
15429        args: &[PerlValue],
15430        line: usize,
15431    ) -> PerlResult<PerlValue> {
15432        match method {
15433            "wait" => {
15434                if !args.is_empty() {
15435                    return Err(PerlError::runtime("wait() takes no arguments", line));
15436                }
15437                let _ = barrier.0.wait();
15438                Ok(PerlValue::integer(1))
15439            }
15440            _ => Err(PerlError::runtime(
15441                format!("Unknown method for barrier: {}", method),
15442                line,
15443            )),
15444        }
15445    }
15446
15447    fn capture_method(
15448        &self,
15449        c: Arc<CaptureResult>,
15450        method: &str,
15451        args: &[PerlValue],
15452        line: usize,
15453    ) -> PerlResult<PerlValue> {
15454        if !args.is_empty() {
15455            return Err(PerlError::runtime(
15456                format!("capture: {} takes no arguments", method),
15457                line,
15458            ));
15459        }
15460        match method {
15461            "stdout" => Ok(PerlValue::string(c.stdout.clone())),
15462            "stderr" => Ok(PerlValue::string(c.stderr.clone())),
15463            "exitcode" => Ok(PerlValue::integer(c.exitcode)),
15464            "failed" => Ok(PerlValue::integer(if c.exitcode != 0 { 1 } else { 0 })),
15465            _ => Err(PerlError::runtime(
15466                format!("Unknown method for capture: {}", method),
15467                line,
15468            )),
15469        }
15470    }
15471
15472    pub(crate) fn builtin_par_pipeline_stream(
15473        &mut self,
15474        args: &[PerlValue],
15475        _line: usize,
15476    ) -> PerlResult<PerlValue> {
15477        let mut items = Vec::new();
15478        for v in args {
15479            if let Some(a) = v.as_array_vec() {
15480                items.extend(a);
15481            } else {
15482                items.push(v.clone());
15483            }
15484        }
15485        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15486            source: items,
15487            ops: Vec::new(),
15488            has_scalar_terminal: false,
15489            par_stream: true,
15490            streaming: false,
15491            streaming_workers: 0,
15492            streaming_buffer: 256,
15493        }))))
15494    }
15495
15496    /// `par_pipeline_stream(@list, workers => N, buffer => N)` — create a streaming pipeline
15497    /// that wires ops through bounded channels on `collect()`.
15498    pub(crate) fn builtin_par_pipeline_stream_new(
15499        &mut self,
15500        args: &[PerlValue],
15501        _line: usize,
15502    ) -> PerlResult<PerlValue> {
15503        let mut items = Vec::new();
15504        let mut workers: usize = 0;
15505        let mut buffer: usize = 256;
15506        // Separate list items from keyword args (workers => N, buffer => N).
15507        let mut i = 0;
15508        while i < args.len() {
15509            let s = args[i].to_string();
15510            if (s == "workers" || s == "buffer") && i + 1 < args.len() {
15511                let val = args[i + 1].to_int().max(1) as usize;
15512                if s == "workers" {
15513                    workers = val;
15514                } else {
15515                    buffer = val;
15516                }
15517                i += 2;
15518            } else if let Some(a) = args[i].as_array_vec() {
15519                items.extend(a);
15520                i += 1;
15521            } else {
15522                items.push(args[i].clone());
15523                i += 1;
15524            }
15525        }
15526        Ok(PerlValue::pipeline(Arc::new(Mutex::new(PipelineInner {
15527            source: items,
15528            ops: Vec::new(),
15529            has_scalar_terminal: false,
15530            par_stream: false,
15531            streaming: true,
15532            streaming_workers: workers,
15533            streaming_buffer: buffer,
15534        }))))
15535    }
15536
15537    /// `sub { $_ * k }` used when a map stage is lowered to [`crate::bytecode::Op::MapIntMul`].
15538    pub(crate) fn pipeline_int_mul_sub(k: i64) -> Arc<PerlSub> {
15539        let line = 1usize;
15540        let body = vec![Statement {
15541            label: None,
15542            kind: StmtKind::Expression(Expr {
15543                kind: ExprKind::BinOp {
15544                    left: Box::new(Expr {
15545                        kind: ExprKind::ScalarVar("_".into()),
15546                        line,
15547                    }),
15548                    op: BinOp::Mul,
15549                    right: Box::new(Expr {
15550                        kind: ExprKind::Integer(k),
15551                        line,
15552                    }),
15553                },
15554                line,
15555            }),
15556            line,
15557        }];
15558        Arc::new(PerlSub {
15559            name: "__pipeline_int_mul__".into(),
15560            params: vec![],
15561            body,
15562            closure_env: None,
15563            prototype: None,
15564            fib_like: None,
15565        })
15566    }
15567
15568    pub(crate) fn anon_coderef_from_block(&mut self, block: &Block) -> Arc<PerlSub> {
15569        let captured = self.scope.capture();
15570        Arc::new(PerlSub {
15571            name: "__ANON__".into(),
15572            params: vec![],
15573            body: block.clone(),
15574            closure_env: Some(captured),
15575            prototype: None,
15576            fib_like: None,
15577        })
15578    }
15579
15580    pub(crate) fn builtin_collect_execute(
15581        &mut self,
15582        args: &[PerlValue],
15583        line: usize,
15584    ) -> PerlResult<PerlValue> {
15585        if args.is_empty() {
15586            return Err(PerlError::runtime(
15587                "collect() expects at least one argument",
15588                line,
15589            ));
15590        }
15591        // `Op::Call` uses `pop_call_operands_flattened`: a single array actual becomes
15592        // many operands. Treat multi-arg as one materialized list (eager `|> … |> collect()`).
15593        if args.len() == 1 {
15594            if let Some(p) = args[0].as_pipeline() {
15595                return self.pipeline_collect(&p, line);
15596            }
15597            return Ok(PerlValue::array(args[0].to_list()));
15598        }
15599        Ok(PerlValue::array(args.to_vec()))
15600    }
15601
15602    pub(crate) fn pipeline_push(
15603        &self,
15604        p: &Arc<Mutex<PipelineInner>>,
15605        op: PipelineOp,
15606        line: usize,
15607    ) -> PerlResult<()> {
15608        let mut g = p.lock();
15609        if g.has_scalar_terminal {
15610            return Err(PerlError::runtime(
15611                "pipeline: cannot chain after preduce / preduce_init / pmap_reduce (must be last before collect)",
15612                line,
15613            ));
15614        }
15615        if matches!(
15616            &op,
15617            PipelineOp::PReduce { .. }
15618                | PipelineOp::PReduceInit { .. }
15619                | PipelineOp::PMapReduce { .. }
15620        ) {
15621            g.has_scalar_terminal = true;
15622        }
15623        g.ops.push(op);
15624        Ok(())
15625    }
15626
15627    fn pipeline_parse_sub_progress(
15628        args: &[PerlValue],
15629        line: usize,
15630        name: &str,
15631    ) -> PerlResult<(Arc<PerlSub>, bool)> {
15632        if args.is_empty() {
15633            return Err(PerlError::runtime(
15634                format!("pipeline {}: expects at least 1 argument (code ref)", name),
15635                line,
15636            ));
15637        }
15638        let Some(sub) = args[0].as_code_ref() else {
15639            return Err(PerlError::runtime(
15640                format!("pipeline {}: first argument must be a code reference", name),
15641                line,
15642            ));
15643        };
15644        let progress = args.get(1).map(|x| x.is_true()).unwrap_or(false);
15645        if args.len() > 2 {
15646            return Err(PerlError::runtime(
15647                format!(
15648                    "pipeline {}: at most 2 arguments (sub, optional progress flag)",
15649                    name
15650                ),
15651                line,
15652            ));
15653        }
15654        Ok((sub, progress))
15655    }
15656
15657    pub(crate) fn pipeline_method(
15658        &mut self,
15659        p: Arc<Mutex<PipelineInner>>,
15660        method: &str,
15661        args: &[PerlValue],
15662        line: usize,
15663    ) -> PerlResult<PerlValue> {
15664        match method {
15665            "filter" | "f" | "grep" => {
15666                if args.len() != 1 {
15667                    return Err(PerlError::runtime(
15668                        "pipeline filter/grep expects 1 argument (sub)",
15669                        line,
15670                    ));
15671                }
15672                let Some(sub) = args[0].as_code_ref() else {
15673                    return Err(PerlError::runtime(
15674                        "pipeline filter/grep expects a code reference",
15675                        line,
15676                    ));
15677                };
15678                self.pipeline_push(&p, PipelineOp::Filter(sub), line)?;
15679                Ok(PerlValue::pipeline(Arc::clone(&p)))
15680            }
15681            "map" => {
15682                if args.len() != 1 {
15683                    return Err(PerlError::runtime(
15684                        "pipeline map expects 1 argument (sub)",
15685                        line,
15686                    ));
15687                }
15688                let Some(sub) = args[0].as_code_ref() else {
15689                    return Err(PerlError::runtime(
15690                        "pipeline map expects a code reference",
15691                        line,
15692                    ));
15693                };
15694                self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
15695                Ok(PerlValue::pipeline(Arc::clone(&p)))
15696            }
15697            "tap" | "peek" => {
15698                if args.len() != 1 {
15699                    return Err(PerlError::runtime(
15700                        "pipeline tap/peek expects 1 argument (sub)",
15701                        line,
15702                    ));
15703                }
15704                let Some(sub) = args[0].as_code_ref() else {
15705                    return Err(PerlError::runtime(
15706                        "pipeline tap/peek expects a code reference",
15707                        line,
15708                    ));
15709                };
15710                self.pipeline_push(&p, PipelineOp::Tap(sub), line)?;
15711                Ok(PerlValue::pipeline(Arc::clone(&p)))
15712            }
15713            "take" => {
15714                if args.len() != 1 {
15715                    return Err(PerlError::runtime("pipeline take expects 1 argument", line));
15716                }
15717                let n = args[0].to_int();
15718                self.pipeline_push(&p, PipelineOp::Take(n), line)?;
15719                Ok(PerlValue::pipeline(Arc::clone(&p)))
15720            }
15721            "pmap" => {
15722                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pmap")?;
15723                self.pipeline_push(&p, PipelineOp::PMap { sub, progress }, line)?;
15724                Ok(PerlValue::pipeline(Arc::clone(&p)))
15725            }
15726            "pgrep" => {
15727                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pgrep")?;
15728                self.pipeline_push(&p, PipelineOp::PGrep { sub, progress }, line)?;
15729                Ok(PerlValue::pipeline(Arc::clone(&p)))
15730            }
15731            "pfor" => {
15732                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pfor")?;
15733                self.pipeline_push(&p, PipelineOp::PFor { sub, progress }, line)?;
15734                Ok(PerlValue::pipeline(Arc::clone(&p)))
15735            }
15736            "pmap_chunked" => {
15737                if args.len() < 2 {
15738                    return Err(PerlError::runtime(
15739                        "pipeline pmap_chunked expects chunk size and a code reference",
15740                        line,
15741                    ));
15742                }
15743                let chunk = args[0].to_int().max(1);
15744                let Some(sub) = args[1].as_code_ref() else {
15745                    return Err(PerlError::runtime(
15746                        "pipeline pmap_chunked: second argument must be a code reference",
15747                        line,
15748                    ));
15749                };
15750                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
15751                if args.len() > 3 {
15752                    return Err(PerlError::runtime(
15753                        "pipeline pmap_chunked: chunk, sub, optional progress (at most 3 args)",
15754                        line,
15755                    ));
15756                }
15757                self.pipeline_push(
15758                    &p,
15759                    PipelineOp::PMapChunked {
15760                        chunk,
15761                        sub,
15762                        progress,
15763                    },
15764                    line,
15765                )?;
15766                Ok(PerlValue::pipeline(Arc::clone(&p)))
15767            }
15768            "psort" => {
15769                let (cmp, progress) = match args.len() {
15770                    0 => (None, false),
15771                    1 => {
15772                        if let Some(s) = args[0].as_code_ref() {
15773                            (Some(s), false)
15774                        } else {
15775                            (None, args[0].is_true())
15776                        }
15777                    }
15778                    2 => {
15779                        let Some(s) = args[0].as_code_ref() else {
15780                            return Err(PerlError::runtime(
15781                                "pipeline psort: with two arguments, the first must be a comparator sub",
15782                                line,
15783                            ));
15784                        };
15785                        (Some(s), args[1].is_true())
15786                    }
15787                    _ => {
15788                        return Err(PerlError::runtime(
15789                            "pipeline psort: 0 args, 1 (sub or progress), or 2 (sub, progress)",
15790                            line,
15791                        ));
15792                    }
15793                };
15794                self.pipeline_push(&p, PipelineOp::PSort { cmp, progress }, line)?;
15795                Ok(PerlValue::pipeline(Arc::clone(&p)))
15796            }
15797            "pcache" => {
15798                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "pcache")?;
15799                self.pipeline_push(&p, PipelineOp::PCache { sub, progress }, line)?;
15800                Ok(PerlValue::pipeline(Arc::clone(&p)))
15801            }
15802            "preduce" => {
15803                let (sub, progress) = Self::pipeline_parse_sub_progress(args, line, "preduce")?;
15804                self.pipeline_push(&p, PipelineOp::PReduce { sub, progress }, line)?;
15805                Ok(PerlValue::pipeline(Arc::clone(&p)))
15806            }
15807            "preduce_init" => {
15808                if args.len() < 2 {
15809                    return Err(PerlError::runtime(
15810                        "pipeline preduce_init expects init value and a code reference",
15811                        line,
15812                    ));
15813                }
15814                let init = args[0].clone();
15815                let Some(sub) = args[1].as_code_ref() else {
15816                    return Err(PerlError::runtime(
15817                        "pipeline preduce_init: second argument must be a code reference",
15818                        line,
15819                    ));
15820                };
15821                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
15822                if args.len() > 3 {
15823                    return Err(PerlError::runtime(
15824                        "pipeline preduce_init: init, sub, optional progress (at most 3 args)",
15825                        line,
15826                    ));
15827                }
15828                self.pipeline_push(
15829                    &p,
15830                    PipelineOp::PReduceInit {
15831                        init,
15832                        sub,
15833                        progress,
15834                    },
15835                    line,
15836                )?;
15837                Ok(PerlValue::pipeline(Arc::clone(&p)))
15838            }
15839            "pmap_reduce" => {
15840                if args.len() < 2 {
15841                    return Err(PerlError::runtime(
15842                        "pipeline pmap_reduce expects map sub and reduce sub",
15843                        line,
15844                    ));
15845                }
15846                let Some(map) = args[0].as_code_ref() else {
15847                    return Err(PerlError::runtime(
15848                        "pipeline pmap_reduce: first argument must be a code reference (map)",
15849                        line,
15850                    ));
15851                };
15852                let Some(reduce) = args[1].as_code_ref() else {
15853                    return Err(PerlError::runtime(
15854                        "pipeline pmap_reduce: second argument must be a code reference (reduce)",
15855                        line,
15856                    ));
15857                };
15858                let progress = args.get(2).map(|x| x.is_true()).unwrap_or(false);
15859                if args.len() > 3 {
15860                    return Err(PerlError::runtime(
15861                        "pipeline pmap_reduce: map, reduce, optional progress (at most 3 args)",
15862                        line,
15863                    ));
15864                }
15865                self.pipeline_push(
15866                    &p,
15867                    PipelineOp::PMapReduce {
15868                        map,
15869                        reduce,
15870                        progress,
15871                    },
15872                    line,
15873                )?;
15874                Ok(PerlValue::pipeline(Arc::clone(&p)))
15875            }
15876            "collect" => {
15877                if !args.is_empty() {
15878                    return Err(PerlError::runtime(
15879                        "pipeline collect takes no arguments",
15880                        line,
15881                    ));
15882                }
15883                self.pipeline_collect(&p, line)
15884            }
15885            _ => {
15886                // Any other name: resolve as a subroutine (`sub name { ... }` in scope) and treat
15887                // like `->map` — `$_` is each element (same as `map { } @_` over the stream).
15888                if let Some(sub) = self.resolve_sub_by_name(method) {
15889                    if !args.is_empty() {
15890                        return Err(PerlError::runtime(
15891                            format!(
15892                                "pipeline ->{}: resolved subroutine takes no arguments; use a no-arg call or built-in ->map(sub {{ ... }}) / ->filter(sub {{ ... }})",
15893                                method
15894                            ),
15895                            line,
15896                        ));
15897                    }
15898                    self.pipeline_push(&p, PipelineOp::Map(sub), line)?;
15899                    Ok(PerlValue::pipeline(Arc::clone(&p)))
15900                } else {
15901                    Err(PerlError::runtime(
15902                        format!("Unknown method for pipeline: {}", method),
15903                        line,
15904                    ))
15905                }
15906            }
15907        }
15908    }
15909
15910    fn pipeline_parallel_map(
15911        &mut self,
15912        items: Vec<PerlValue>,
15913        sub: &Arc<PerlSub>,
15914        progress: bool,
15915    ) -> Vec<PerlValue> {
15916        let subs = self.subs.clone();
15917        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
15918        let pmap_progress = PmapProgress::new(progress, items.len());
15919        let results: Vec<PerlValue> = items
15920            .into_par_iter()
15921            .map(|item| {
15922                let mut local_interp = Interpreter::new();
15923                local_interp.subs = subs.clone();
15924                local_interp.scope.restore_capture(&scope_capture);
15925                local_interp
15926                    .scope
15927                    .restore_atomics(&atomic_arrays, &atomic_hashes);
15928                local_interp.enable_parallel_guard();
15929                local_interp.scope.set_topic(item);
15930                local_interp.scope_push_hook();
15931                let val = match local_interp.exec_block_no_scope(&sub.body) {
15932                    Ok(val) => val,
15933                    Err(_) => PerlValue::UNDEF,
15934                };
15935                local_interp.scope_pop_hook();
15936                pmap_progress.tick();
15937                val
15938            })
15939            .collect();
15940        pmap_progress.finish();
15941        results
15942    }
15943
15944    /// Order-preserving parallel filter for `par_pipeline(LIST)` (same capture rules as `pgrep`).
15945    fn pipeline_par_stream_filter(
15946        &mut self,
15947        items: Vec<PerlValue>,
15948        sub: &Arc<PerlSub>,
15949    ) -> Vec<PerlValue> {
15950        if items.is_empty() {
15951            return items;
15952        }
15953        let subs = self.subs.clone();
15954        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
15955        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
15956        let mut kept: Vec<(usize, PerlValue)> = indexed
15957            .into_par_iter()
15958            .filter_map(|(i, item)| {
15959                let mut local_interp = Interpreter::new();
15960                local_interp.subs = subs.clone();
15961                local_interp.scope.restore_capture(&scope_capture);
15962                local_interp
15963                    .scope
15964                    .restore_atomics(&atomic_arrays, &atomic_hashes);
15965                local_interp.enable_parallel_guard();
15966                local_interp.scope.set_topic(item.clone());
15967                local_interp.scope_push_hook();
15968                let keep = match local_interp.exec_block_no_scope(&sub.body) {
15969                    Ok(val) => val.is_true(),
15970                    Err(_) => false,
15971                };
15972                local_interp.scope_pop_hook();
15973                if keep {
15974                    Some((i, item))
15975                } else {
15976                    None
15977                }
15978            })
15979            .collect();
15980        kept.sort_by_key(|(i, _)| *i);
15981        kept.into_iter().map(|(_, x)| x).collect()
15982    }
15983
15984    /// Order-preserving parallel map for `par_pipeline(LIST)` (same capture rules as `pmap`).
15985    fn pipeline_par_stream_map(
15986        &mut self,
15987        items: Vec<PerlValue>,
15988        sub: &Arc<PerlSub>,
15989    ) -> Vec<PerlValue> {
15990        if items.is_empty() {
15991            return items;
15992        }
15993        let subs = self.subs.clone();
15994        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
15995        let indexed: Vec<(usize, PerlValue)> = items.into_iter().enumerate().collect();
15996        let mut mapped: Vec<(usize, PerlValue)> = indexed
15997            .into_par_iter()
15998            .map(|(i, item)| {
15999                let mut local_interp = Interpreter::new();
16000                local_interp.subs = subs.clone();
16001                local_interp.scope.restore_capture(&scope_capture);
16002                local_interp
16003                    .scope
16004                    .restore_atomics(&atomic_arrays, &atomic_hashes);
16005                local_interp.enable_parallel_guard();
16006                local_interp.scope.set_topic(item);
16007                local_interp.scope_push_hook();
16008                let val = match local_interp.exec_block_no_scope(&sub.body) {
16009                    Ok(val) => val,
16010                    Err(_) => PerlValue::UNDEF,
16011                };
16012                local_interp.scope_pop_hook();
16013                (i, val)
16014            })
16015            .collect();
16016        mapped.sort_by_key(|(i, _)| *i);
16017        mapped.into_iter().map(|(_, x)| x).collect()
16018    }
16019
16020    fn pipeline_collect(
16021        &mut self,
16022        p: &Arc<Mutex<PipelineInner>>,
16023        line: usize,
16024    ) -> PerlResult<PerlValue> {
16025        let (mut v, ops, par_stream, streaming, streaming_workers, streaming_buffer) = {
16026            let g = p.lock();
16027            (
16028                g.source.clone(),
16029                g.ops.clone(),
16030                g.par_stream,
16031                g.streaming,
16032                g.streaming_workers,
16033                g.streaming_buffer,
16034            )
16035        };
16036        if streaming {
16037            return self.pipeline_collect_streaming(
16038                v,
16039                &ops,
16040                streaming_workers,
16041                streaming_buffer,
16042                line,
16043            );
16044        }
16045        for op in ops {
16046            match op {
16047                PipelineOp::Filter(sub) => {
16048                    if par_stream {
16049                        v = self.pipeline_par_stream_filter(v, &sub);
16050                    } else {
16051                        let mut out = Vec::new();
16052                        for item in v {
16053                            self.scope_push_hook();
16054                            self.scope.set_topic(item.clone());
16055                            if let Some(ref env) = sub.closure_env {
16056                                self.scope.restore_capture(env);
16057                            }
16058                            let keep = match self.exec_block_no_scope(&sub.body) {
16059                                Ok(val) => val.is_true(),
16060                                Err(_) => false,
16061                            };
16062                            self.scope_pop_hook();
16063                            if keep {
16064                                out.push(item);
16065                            }
16066                        }
16067                        v = out;
16068                    }
16069                }
16070                PipelineOp::Map(sub) => {
16071                    if par_stream {
16072                        v = self.pipeline_par_stream_map(v, &sub);
16073                    } else {
16074                        let mut out = Vec::new();
16075                        for item in v {
16076                            self.scope_push_hook();
16077                            self.scope.set_topic(item);
16078                            if let Some(ref env) = sub.closure_env {
16079                                self.scope.restore_capture(env);
16080                            }
16081                            let mapped = match self.exec_block_no_scope(&sub.body) {
16082                                Ok(val) => val,
16083                                Err(_) => PerlValue::UNDEF,
16084                            };
16085                            self.scope_pop_hook();
16086                            out.push(mapped);
16087                        }
16088                        v = out;
16089                    }
16090                }
16091                PipelineOp::Tap(sub) => {
16092                    match self.call_sub(&sub, v.clone(), WantarrayCtx::Void, line) {
16093                        Ok(_) => {}
16094                        Err(FlowOrError::Error(e)) => return Err(e),
16095                        Err(FlowOrError::Flow(_)) => {
16096                            return Err(PerlError::runtime(
16097                                "tap: unsupported control flow in block",
16098                                line,
16099                            ));
16100                        }
16101                    }
16102                }
16103                PipelineOp::Take(n) => {
16104                    let n = n.max(0) as usize;
16105                    if v.len() > n {
16106                        v.truncate(n);
16107                    }
16108                }
16109                PipelineOp::PMap { sub, progress } => {
16110                    v = self.pipeline_parallel_map(v, &sub, progress);
16111                }
16112                PipelineOp::PGrep { sub, progress } => {
16113                    let subs = self.subs.clone();
16114                    let (scope_capture, atomic_arrays, atomic_hashes) =
16115                        self.scope.capture_with_atomics();
16116                    let pmap_progress = PmapProgress::new(progress, v.len());
16117                    v = v
16118                        .into_par_iter()
16119                        .filter_map(|item| {
16120                            let mut local_interp = Interpreter::new();
16121                            local_interp.subs = subs.clone();
16122                            local_interp.scope.restore_capture(&scope_capture);
16123                            local_interp
16124                                .scope
16125                                .restore_atomics(&atomic_arrays, &atomic_hashes);
16126                            local_interp.enable_parallel_guard();
16127                            local_interp.scope.set_topic(item.clone());
16128                            local_interp.scope_push_hook();
16129                            let keep = match local_interp.exec_block_no_scope(&sub.body) {
16130                                Ok(val) => val.is_true(),
16131                                Err(_) => false,
16132                            };
16133                            local_interp.scope_pop_hook();
16134                            pmap_progress.tick();
16135                            if keep {
16136                                Some(item)
16137                            } else {
16138                                None
16139                            }
16140                        })
16141                        .collect();
16142                    pmap_progress.finish();
16143                }
16144                PipelineOp::PFor { sub, progress } => {
16145                    let subs = self.subs.clone();
16146                    let (scope_capture, atomic_arrays, atomic_hashes) =
16147                        self.scope.capture_with_atomics();
16148                    let pmap_progress = PmapProgress::new(progress, v.len());
16149                    let first_err: Arc<Mutex<Option<PerlError>>> = Arc::new(Mutex::new(None));
16150                    v.clone().into_par_iter().for_each(|item| {
16151                        if first_err.lock().is_some() {
16152                            return;
16153                        }
16154                        let mut local_interp = Interpreter::new();
16155                        local_interp.subs = subs.clone();
16156                        local_interp.scope.restore_capture(&scope_capture);
16157                        local_interp
16158                            .scope
16159                            .restore_atomics(&atomic_arrays, &atomic_hashes);
16160                        local_interp.enable_parallel_guard();
16161                        local_interp.scope.set_topic(item);
16162                        local_interp.scope_push_hook();
16163                        match local_interp.exec_block_no_scope(&sub.body) {
16164                            Ok(_) => {}
16165                            Err(e) => {
16166                                let stryke = match e {
16167                                    FlowOrError::Error(stryke) => stryke,
16168                                    FlowOrError::Flow(_) => PerlError::runtime(
16169                                        "return/last/next/redo not supported inside pipeline pfor block",
16170                                        line,
16171                                    ),
16172                                };
16173                                let mut g = first_err.lock();
16174                                if g.is_none() {
16175                                    *g = Some(stryke);
16176                                }
16177                            }
16178                        }
16179                        local_interp.scope_pop_hook();
16180                        pmap_progress.tick();
16181                    });
16182                    pmap_progress.finish();
16183                    let pfor_err = first_err.lock().take();
16184                    if let Some(e) = pfor_err {
16185                        return Err(e);
16186                    }
16187                }
16188                PipelineOp::PMapChunked {
16189                    chunk,
16190                    sub,
16191                    progress,
16192                } => {
16193                    let chunk_n = chunk.max(1) as usize;
16194                    let subs = self.subs.clone();
16195                    let (scope_capture, atomic_arrays, atomic_hashes) =
16196                        self.scope.capture_with_atomics();
16197                    let indexed_chunks: Vec<(usize, Vec<PerlValue>)> = v
16198                        .chunks(chunk_n)
16199                        .enumerate()
16200                        .map(|(i, c)| (i, c.to_vec()))
16201                        .collect();
16202                    let n_chunks = indexed_chunks.len();
16203                    let pmap_progress = PmapProgress::new(progress, n_chunks);
16204                    let mut chunk_results: Vec<(usize, Vec<PerlValue>)> = indexed_chunks
16205                        .into_par_iter()
16206                        .map(|(chunk_idx, chunk)| {
16207                            let mut local_interp = Interpreter::new();
16208                            local_interp.subs = subs.clone();
16209                            local_interp.scope.restore_capture(&scope_capture);
16210                            local_interp
16211                                .scope
16212                                .restore_atomics(&atomic_arrays, &atomic_hashes);
16213                            local_interp.enable_parallel_guard();
16214                            let mut out = Vec::with_capacity(chunk.len());
16215                            for item in chunk {
16216                                local_interp.scope.set_topic(item);
16217                                local_interp.scope_push_hook();
16218                                match local_interp.exec_block_no_scope(&sub.body) {
16219                                    Ok(val) => {
16220                                        local_interp.scope_pop_hook();
16221                                        out.push(val);
16222                                    }
16223                                    Err(_) => {
16224                                        local_interp.scope_pop_hook();
16225                                        out.push(PerlValue::UNDEF);
16226                                    }
16227                                }
16228                            }
16229                            pmap_progress.tick();
16230                            (chunk_idx, out)
16231                        })
16232                        .collect();
16233                    pmap_progress.finish();
16234                    chunk_results.sort_by_key(|(i, _)| *i);
16235                    v = chunk_results.into_iter().flat_map(|(_, x)| x).collect();
16236                }
16237                PipelineOp::PSort { cmp, progress } => {
16238                    let pmap_progress = PmapProgress::new(progress, 2);
16239                    pmap_progress.tick();
16240                    match cmp {
16241                        Some(cmp_block) => {
16242                            if let Some(mode) = detect_sort_block_fast(&cmp_block.body) {
16243                                v.par_sort_by(|a, b| sort_magic_cmp(a, b, mode));
16244                            } else {
16245                                let subs = self.subs.clone();
16246                                let scope_capture = self.scope.capture();
16247                                v.par_sort_by(|a, b| {
16248                                    let mut local_interp = Interpreter::new();
16249                                    local_interp.subs = subs.clone();
16250                                    local_interp.scope.restore_capture(&scope_capture);
16251                                    local_interp.enable_parallel_guard();
16252                                    let _ = local_interp.scope.set_scalar("a", a.clone());
16253                                    let _ = local_interp.scope.set_scalar("b", b.clone());
16254                                    let _ = local_interp.scope.set_scalar("_0", a.clone());
16255                                    let _ = local_interp.scope.set_scalar("_1", b.clone());
16256                                    local_interp.scope_push_hook();
16257                                    let ord =
16258                                        match local_interp.exec_block_no_scope(&cmp_block.body) {
16259                                            Ok(v) => {
16260                                                let n = v.to_int();
16261                                                if n < 0 {
16262                                                    std::cmp::Ordering::Less
16263                                                } else if n > 0 {
16264                                                    std::cmp::Ordering::Greater
16265                                                } else {
16266                                                    std::cmp::Ordering::Equal
16267                                                }
16268                                            }
16269                                            Err(_) => std::cmp::Ordering::Equal,
16270                                        };
16271                                    local_interp.scope_pop_hook();
16272                                    ord
16273                                });
16274                            }
16275                        }
16276                        None => {
16277                            v.par_sort_by(|a, b| a.to_string().cmp(&b.to_string()));
16278                        }
16279                    }
16280                    pmap_progress.tick();
16281                    pmap_progress.finish();
16282                }
16283                PipelineOp::PCache { sub, progress } => {
16284                    let subs = self.subs.clone();
16285                    let scope_capture = self.scope.capture();
16286                    let cache = &*crate::pcache::GLOBAL_PCACHE;
16287                    let pmap_progress = PmapProgress::new(progress, v.len());
16288                    v = v
16289                        .into_par_iter()
16290                        .map(|item| {
16291                            let k = crate::pcache::cache_key(&item);
16292                            if let Some(cached) = cache.get(&k) {
16293                                pmap_progress.tick();
16294                                return cached.clone();
16295                            }
16296                            let mut local_interp = Interpreter::new();
16297                            local_interp.subs = subs.clone();
16298                            local_interp.scope.restore_capture(&scope_capture);
16299                            local_interp.enable_parallel_guard();
16300                            local_interp.scope.set_topic(item.clone());
16301                            local_interp.scope_push_hook();
16302                            let val = match local_interp.exec_block_no_scope(&sub.body) {
16303                                Ok(v) => v,
16304                                Err(_) => PerlValue::UNDEF,
16305                            };
16306                            local_interp.scope_pop_hook();
16307                            cache.insert(k, val.clone());
16308                            pmap_progress.tick();
16309                            val
16310                        })
16311                        .collect();
16312                    pmap_progress.finish();
16313                }
16314                PipelineOp::PReduce { sub, progress } => {
16315                    if v.is_empty() {
16316                        return Ok(PerlValue::UNDEF);
16317                    }
16318                    if v.len() == 1 {
16319                        return Ok(v.into_iter().next().unwrap());
16320                    }
16321                    let block = sub.body.clone();
16322                    let subs = self.subs.clone();
16323                    let scope_capture = self.scope.capture();
16324                    let pmap_progress = PmapProgress::new(progress, v.len());
16325                    let result = v
16326                        .into_par_iter()
16327                        .map(|x| {
16328                            pmap_progress.tick();
16329                            x
16330                        })
16331                        .reduce_with(|a, b| {
16332                            let mut local_interp = Interpreter::new();
16333                            local_interp.subs = subs.clone();
16334                            local_interp.scope.restore_capture(&scope_capture);
16335                            local_interp.enable_parallel_guard();
16336                            let _ = local_interp.scope.set_scalar("a", a.clone());
16337                            let _ = local_interp.scope.set_scalar("b", b.clone());
16338                            let _ = local_interp.scope.set_scalar("_0", a);
16339                            let _ = local_interp.scope.set_scalar("_1", b);
16340                            match local_interp.exec_block(&block) {
16341                                Ok(val) => val,
16342                                Err(_) => PerlValue::UNDEF,
16343                            }
16344                        });
16345                    pmap_progress.finish();
16346                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16347                }
16348                PipelineOp::PReduceInit {
16349                    init,
16350                    sub,
16351                    progress,
16352                } => {
16353                    if v.is_empty() {
16354                        return Ok(init);
16355                    }
16356                    let block = sub.body.clone();
16357                    let subs = self.subs.clone();
16358                    let scope_capture = self.scope.capture();
16359                    let cap: &[(String, PerlValue)] = scope_capture.as_slice();
16360                    if v.len() == 1 {
16361                        return Ok(fold_preduce_init_step(
16362                            &subs,
16363                            cap,
16364                            &block,
16365                            preduce_init_fold_identity(&init),
16366                            v.into_iter().next().unwrap(),
16367                        ));
16368                    }
16369                    let pmap_progress = PmapProgress::new(progress, v.len());
16370                    let result = v
16371                        .into_par_iter()
16372                        .fold(
16373                            || preduce_init_fold_identity(&init),
16374                            |acc, item| {
16375                                pmap_progress.tick();
16376                                fold_preduce_init_step(&subs, cap, &block, acc, item)
16377                            },
16378                        )
16379                        .reduce(
16380                            || preduce_init_fold_identity(&init),
16381                            |a, b| merge_preduce_init_partials(a, b, &block, &subs, cap),
16382                        );
16383                    pmap_progress.finish();
16384                    return Ok(result);
16385                }
16386                PipelineOp::PMapReduce {
16387                    map,
16388                    reduce,
16389                    progress,
16390                } => {
16391                    if v.is_empty() {
16392                        return Ok(PerlValue::UNDEF);
16393                    }
16394                    let map_block = map.body.clone();
16395                    let reduce_block = reduce.body.clone();
16396                    let subs = self.subs.clone();
16397                    let scope_capture = self.scope.capture();
16398                    if v.len() == 1 {
16399                        let mut local_interp = Interpreter::new();
16400                        local_interp.subs = subs.clone();
16401                        local_interp.scope.restore_capture(&scope_capture);
16402                        local_interp.scope.set_topic(v[0].clone());
16403                        return match local_interp.exec_block_no_scope(&map_block) {
16404                            Ok(val) => Ok(val),
16405                            Err(_) => Ok(PerlValue::UNDEF),
16406                        };
16407                    }
16408                    let pmap_progress = PmapProgress::new(progress, v.len());
16409                    let result = v
16410                        .into_par_iter()
16411                        .map(|item| {
16412                            let mut local_interp = Interpreter::new();
16413                            local_interp.subs = subs.clone();
16414                            local_interp.scope.restore_capture(&scope_capture);
16415                            local_interp.scope.set_topic(item);
16416                            let val = match local_interp.exec_block_no_scope(&map_block) {
16417                                Ok(val) => val,
16418                                Err(_) => PerlValue::UNDEF,
16419                            };
16420                            pmap_progress.tick();
16421                            val
16422                        })
16423                        .reduce_with(|a, b| {
16424                            let mut local_interp = Interpreter::new();
16425                            local_interp.subs = subs.clone();
16426                            local_interp.scope.restore_capture(&scope_capture);
16427                            let _ = local_interp.scope.set_scalar("a", a.clone());
16428                            let _ = local_interp.scope.set_scalar("b", b.clone());
16429                            let _ = local_interp.scope.set_scalar("_0", a);
16430                            let _ = local_interp.scope.set_scalar("_1", b);
16431                            match local_interp.exec_block_no_scope(&reduce_block) {
16432                                Ok(val) => val,
16433                                Err(_) => PerlValue::UNDEF,
16434                            }
16435                        });
16436                    pmap_progress.finish();
16437                    return Ok(result.unwrap_or(PerlValue::UNDEF));
16438                }
16439            }
16440        }
16441        Ok(PerlValue::array(v))
16442    }
16443
16444    /// Streaming collect: wire pipeline ops through bounded channels so items flow
16445    /// between stages concurrently.  Order is **not** preserved.
16446    fn pipeline_collect_streaming(
16447        &mut self,
16448        source: Vec<PerlValue>,
16449        ops: &[PipelineOp],
16450        workers_per_stage: usize,
16451        buffer: usize,
16452        line: usize,
16453    ) -> PerlResult<PerlValue> {
16454        use crossbeam::channel::{bounded, Receiver, Sender};
16455
16456        // Validate: reject ops that require all items (can't stream).
16457        for op in ops {
16458            match op {
16459                PipelineOp::PSort { .. }
16460                | PipelineOp::PReduce { .. }
16461                | PipelineOp::PReduceInit { .. }
16462                | PipelineOp::PMapReduce { .. }
16463                | PipelineOp::PMapChunked { .. } => {
16464                    return Err(PerlError::runtime(
16465                        format!(
16466                            "par_pipeline_stream: {:?} requires all items and cannot stream; use par_pipeline instead",
16467                            std::mem::discriminant(op)
16468                        ),
16469                        line,
16470                    ));
16471                }
16472                _ => {}
16473            }
16474        }
16475
16476        // Filter out non-streamable ops and collect streamable ones.
16477        // Supported: Filter, Map, Take, PMap, PGrep, PFor, PCache.
16478        let streamable_ops: Vec<&PipelineOp> = ops.iter().collect();
16479        if streamable_ops.is_empty() {
16480            return Ok(PerlValue::array(source));
16481        }
16482
16483        let n_stages = streamable_ops.len();
16484        let wn = if workers_per_stage > 0 {
16485            workers_per_stage
16486        } else {
16487            self.parallel_thread_count()
16488        };
16489        let subs = self.subs.clone();
16490        let (capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
16491
16492        // Build channels: one between each pair of stages, plus one for output.
16493        // channel[0]: source → stage 0
16494        // channel[i]: stage i-1 → stage i
16495        // channel[n_stages]: stage n_stages-1 → collector
16496        let mut channels: Vec<(Sender<PerlValue>, Receiver<PerlValue>)> =
16497            (0..=n_stages).map(|_| bounded(buffer)).collect();
16498
16499        let err: Arc<Mutex<Option<String>>> = Arc::new(Mutex::new(None));
16500        let take_done: Arc<std::sync::atomic::AtomicBool> =
16501            Arc::new(std::sync::atomic::AtomicBool::new(false));
16502
16503        // Collect senders/receivers for each stage.
16504        // Stage i reads from channels[i].1 and writes to channels[i+1].0.
16505        let source_tx = channels[0].0.clone();
16506        let result_rx = channels[n_stages].1.clone();
16507        let results: Arc<Mutex<Vec<PerlValue>>> = Arc::new(Mutex::new(Vec::new()));
16508
16509        std::thread::scope(|scope| {
16510            // Collector thread: drain results concurrently to avoid deadlock
16511            // when bounded channels fill up.
16512            let result_rx_c = result_rx.clone();
16513            let results_c = Arc::clone(&results);
16514            scope.spawn(move || {
16515                while let Ok(item) = result_rx_c.recv() {
16516                    results_c.lock().push(item);
16517                }
16518            });
16519
16520            // Source feeder thread.
16521            let err_s = Arc::clone(&err);
16522            let take_done_s = Arc::clone(&take_done);
16523            scope.spawn(move || {
16524                for item in source {
16525                    if err_s.lock().is_some()
16526                        || take_done_s.load(std::sync::atomic::Ordering::Relaxed)
16527                    {
16528                        break;
16529                    }
16530                    if source_tx.send(item).is_err() {
16531                        break;
16532                    }
16533                }
16534            });
16535
16536            // Spawn workers for each stage.
16537            for (stage_idx, op) in streamable_ops.iter().enumerate() {
16538                let rx = channels[stage_idx].1.clone();
16539                let tx = channels[stage_idx + 1].0.clone();
16540
16541                for _ in 0..wn {
16542                    let rx = rx.clone();
16543                    let tx = tx.clone();
16544                    let subs = subs.clone();
16545                    let capture = capture.clone();
16546                    let atomic_arrays = atomic_arrays.clone();
16547                    let atomic_hashes = atomic_hashes.clone();
16548                    let err_w = Arc::clone(&err);
16549                    let take_done_w = Arc::clone(&take_done);
16550
16551                    match *op {
16552                        PipelineOp::Filter(ref sub) | PipelineOp::PGrep { ref sub, .. } => {
16553                            let sub = Arc::clone(sub);
16554                            scope.spawn(move || {
16555                                while let Ok(item) = rx.recv() {
16556                                    if err_w.lock().is_some() {
16557                                        break;
16558                                    }
16559                                    let mut interp = Interpreter::new();
16560                                    interp.subs = subs.clone();
16561                                    interp.scope.restore_capture(&capture);
16562                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16563                                    interp.enable_parallel_guard();
16564                                    interp.scope.set_topic(item.clone());
16565                                    interp.scope_push_hook();
16566                                    let keep = match interp.exec_block_no_scope(&sub.body) {
16567                                        Ok(val) => val.is_true(),
16568                                        Err(_) => false,
16569                                    };
16570                                    interp.scope_pop_hook();
16571                                    if keep && tx.send(item).is_err() {
16572                                        break;
16573                                    }
16574                                }
16575                            });
16576                        }
16577                        PipelineOp::Map(ref sub) | PipelineOp::PMap { ref sub, .. } => {
16578                            let sub = Arc::clone(sub);
16579                            scope.spawn(move || {
16580                                while let Ok(item) = rx.recv() {
16581                                    if err_w.lock().is_some() {
16582                                        break;
16583                                    }
16584                                    let mut interp = Interpreter::new();
16585                                    interp.subs = subs.clone();
16586                                    interp.scope.restore_capture(&capture);
16587                                    interp.scope.restore_atomics(&atomic_arrays, &atomic_hashes);
16588                                    interp.enable_parallel_guard();
16589                                    interp.scope.set_topic(item);
16590                                    interp.scope_push_hook();
16591                                    let mapped = match interp.exec_block_no_scope(&sub.body) {
16592                                        Ok(val) => val,
16593                                        Err(_) => PerlValue::UNDEF,
16594                                    };
16595                                    interp.scope_pop_hook();
16596                                    if tx.send(mapped).is_err() {
16597                                        break;
16598                                    }
16599                                }
16600                            });
16601                        }
16602                        PipelineOp::Take(n) => {
16603                            let limit = (*n).max(0) as usize;
16604                            let count = Arc::new(std::sync::atomic::AtomicUsize::new(0));
16605                            let count_w = Arc::clone(&count);
16606                            scope.spawn(move || {
16607                                while let Ok(item) = rx.recv() {
16608                                    let prev =
16609                                        count_w.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
16610                                    if prev >= limit {
16611                                        take_done_w
16612                                            .store(true, std::sync::atomic::Ordering::Relaxed);
16613                                        break;
16614                                    }
16615                                    if tx.send(item).is_err() {
16616                                        break;
16617                                    }
16618                                }
16619                            });
16620                            // Take only needs 1 worker; skip remaining worker spawns.
16621                            break;
16622                        }
16623                        PipelineOp::PFor { ref sub, .. } => {
16624                            let sub = Arc::clone(sub);
16625                            scope.spawn(move || {
16626                                while let Ok(item) = rx.recv() {
16627                                    if err_w.lock().is_some() {
16628                                        break;
16629                                    }
16630                                    let mut interp = Interpreter::new();
16631                                    interp.subs = subs.clone();
16632                                    interp.scope.restore_capture(&capture);
16633                                    interp
16634                                        .scope
16635                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16636                                    interp.enable_parallel_guard();
16637                                    interp.scope.set_topic(item.clone());
16638                                    interp.scope_push_hook();
16639                                    match interp.exec_block_no_scope(&sub.body) {
16640                                        Ok(_) => {}
16641                                        Err(e) => {
16642                                            let msg = match e {
16643                                                FlowOrError::Error(stryke) => stryke.to_string(),
16644                                                FlowOrError::Flow(_) => {
16645                                                    "unexpected control flow in par_pipeline_stream pfor".into()
16646                                                }
16647                                            };
16648                                            let mut g = err_w.lock();
16649                                            if g.is_none() {
16650                                                *g = Some(msg);
16651                                            }
16652                                            interp.scope_pop_hook();
16653                                            break;
16654                                        }
16655                                    }
16656                                    interp.scope_pop_hook();
16657                                    if tx.send(item).is_err() {
16658                                        break;
16659                                    }
16660                                }
16661                            });
16662                        }
16663                        PipelineOp::Tap(ref sub) => {
16664                            let sub = Arc::clone(sub);
16665                            scope.spawn(move || {
16666                                while let Ok(item) = rx.recv() {
16667                                    if err_w.lock().is_some() {
16668                                        break;
16669                                    }
16670                                    let mut interp = Interpreter::new();
16671                                    interp.subs = subs.clone();
16672                                    interp.scope.restore_capture(&capture);
16673                                    interp
16674                                        .scope
16675                                        .restore_atomics(&atomic_arrays, &atomic_hashes);
16676                                    interp.enable_parallel_guard();
16677                                    match interp.call_sub(
16678                                        &sub,
16679                                        vec![item.clone()],
16680                                        WantarrayCtx::Void,
16681                                        line,
16682                                    )
16683                                    {
16684                                        Ok(_) => {}
16685                                        Err(e) => {
16686                                            let msg = match e {
16687                                                FlowOrError::Error(stryke) => stryke.to_string(),
16688                                                FlowOrError::Flow(_) => {
16689                                                    "unexpected control flow in par_pipeline_stream tap"
16690                                                        .into()
16691                                                }
16692                                            };
16693                                            let mut g = err_w.lock();
16694                                            if g.is_none() {
16695                                                *g = Some(msg);
16696                                            }
16697                                            break;
16698                                        }
16699                                    }
16700                                    if tx.send(item).is_err() {
16701                                        break;
16702                                    }
16703                                }
16704                            });
16705                        }
16706                        PipelineOp::PCache { ref sub, .. } => {
16707                            let sub = Arc::clone(sub);
16708                            scope.spawn(move || {
16709                                while let Ok(item) = rx.recv() {
16710                                    if err_w.lock().is_some() {
16711                                        break;
16712                                    }
16713                                    let k = crate::pcache::cache_key(&item);
16714                                    let val = if let Some(cached) =
16715                                        crate::pcache::GLOBAL_PCACHE.get(&k)
16716                                    {
16717                                        cached.clone()
16718                                    } else {
16719                                        let mut interp = Interpreter::new();
16720                                        interp.subs = subs.clone();
16721                                        interp.scope.restore_capture(&capture);
16722                                        interp
16723                                            .scope
16724                                            .restore_atomics(&atomic_arrays, &atomic_hashes);
16725                                        interp.enable_parallel_guard();
16726                                        interp.scope.set_topic(item);
16727                                        interp.scope_push_hook();
16728                                        let v = match interp.exec_block_no_scope(&sub.body) {
16729                                            Ok(v) => v,
16730                                            Err(_) => PerlValue::UNDEF,
16731                                        };
16732                                        interp.scope_pop_hook();
16733                                        crate::pcache::GLOBAL_PCACHE.insert(k, v.clone());
16734                                        v
16735                                    };
16736                                    if tx.send(val).is_err() {
16737                                        break;
16738                                    }
16739                                }
16740                            });
16741                        }
16742                        // Non-streaming ops already rejected above.
16743                        _ => unreachable!(),
16744                    }
16745                }
16746            }
16747
16748            // Drop our copies of intermediate senders/receivers so channels disconnect
16749            // when workers finish.  Also drop result_rx so the collector thread exits
16750            // once all stage workers are done.
16751            channels.clear();
16752            drop(result_rx);
16753        });
16754
16755        if let Some(msg) = err.lock().take() {
16756            return Err(PerlError::runtime(msg, line));
16757        }
16758
16759        let results = std::mem::take(&mut *results.lock());
16760        Ok(PerlValue::array(results))
16761    }
16762
16763    fn heap_compare(&mut self, cmp: &Arc<PerlSub>, a: &PerlValue, b: &PerlValue) -> Ordering {
16764        self.scope_push_hook();
16765        if let Some(ref env) = cmp.closure_env {
16766            self.scope.restore_capture(env);
16767        }
16768        let _ = self.scope.set_scalar("a", a.clone());
16769        let _ = self.scope.set_scalar("b", b.clone());
16770        let _ = self.scope.set_scalar("_0", a.clone());
16771        let _ = self.scope.set_scalar("_1", b.clone());
16772        let ord = match self.exec_block_no_scope(&cmp.body) {
16773            Ok(v) => {
16774                let n = v.to_int();
16775                if n < 0 {
16776                    Ordering::Less
16777                } else if n > 0 {
16778                    Ordering::Greater
16779                } else {
16780                    Ordering::Equal
16781                }
16782            }
16783            Err(_) => Ordering::Equal,
16784        };
16785        self.scope_pop_hook();
16786        ord
16787    }
16788
16789    fn heap_sift_up(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
16790        while i > 0 {
16791            let p = (i - 1) / 2;
16792            if self.heap_compare(cmp, &items[i], &items[p]) != Ordering::Less {
16793                break;
16794            }
16795            items.swap(i, p);
16796            i = p;
16797        }
16798    }
16799
16800    fn heap_sift_down(&mut self, items: &mut [PerlValue], cmp: &Arc<PerlSub>, mut i: usize) {
16801        let n = items.len();
16802        loop {
16803            let mut sm = i;
16804            let l = 2 * i + 1;
16805            let r = 2 * i + 2;
16806            if l < n && self.heap_compare(cmp, &items[l], &items[sm]) == Ordering::Less {
16807                sm = l;
16808            }
16809            if r < n && self.heap_compare(cmp, &items[r], &items[sm]) == Ordering::Less {
16810                sm = r;
16811            }
16812            if sm == i {
16813                break;
16814            }
16815            items.swap(i, sm);
16816            i = sm;
16817        }
16818    }
16819
16820    fn hash_for_signature_destruct(
16821        &mut self,
16822        v: &PerlValue,
16823        line: usize,
16824    ) -> PerlResult<IndexMap<String, PerlValue>> {
16825        let Some(m) = self.match_subject_as_hash(v) else {
16826            return Err(PerlError::runtime(
16827                format!(
16828                    "sub signature hash destruct: expected HASH or HASH reference, got {}",
16829                    v.ref_type()
16830                ),
16831                line,
16832            ));
16833        };
16834        Ok(m)
16835    }
16836
16837    /// Bind stryke `sub name ($a, { k => $v })` parameters from `@_` before the body runs.
16838    pub(crate) fn apply_sub_signature(
16839        &mut self,
16840        sub: &PerlSub,
16841        argv: &[PerlValue],
16842        line: usize,
16843    ) -> PerlResult<()> {
16844        if sub.params.is_empty() {
16845            return Ok(());
16846        }
16847        let mut i = 0usize;
16848        for p in &sub.params {
16849            match p {
16850                SubSigParam::Scalar(name, ty) => {
16851                    let val = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
16852                    i += 1;
16853                    if let Some(t) = ty {
16854                        if let Err(e) = t.check_value(&val) {
16855                            return Err(PerlError::runtime(
16856                                format!("sub parameter ${}: {}", name, e),
16857                                line,
16858                            ));
16859                        }
16860                    }
16861                    let n = self.english_scalar_name(name);
16862                    self.scope.declare_scalar(n, val);
16863                }
16864                SubSigParam::Array(name) => {
16865                    let rest: Vec<PerlValue> = argv[i..].to_vec();
16866                    i = argv.len();
16867                    let aname = self.stash_array_name_for_package(name);
16868                    self.scope.declare_array(&aname, rest);
16869                }
16870                SubSigParam::Hash(name) => {
16871                    let rest: Vec<PerlValue> = argv[i..].to_vec();
16872                    i = argv.len();
16873                    let mut map = IndexMap::new();
16874                    let mut j = 0;
16875                    while j + 1 < rest.len() {
16876                        map.insert(rest[j].to_string(), rest[j + 1].clone());
16877                        j += 2;
16878                    }
16879                    self.scope.declare_hash(name, map);
16880                }
16881                SubSigParam::ArrayDestruct(elems) => {
16882                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
16883                    i += 1;
16884                    let Some(arr) = self.match_subject_as_array(&arg) else {
16885                        return Err(PerlError::runtime(
16886                            format!(
16887                                "sub signature array destruct: expected ARRAY or ARRAY reference, got {}",
16888                                arg.ref_type()
16889                            ),
16890                            line,
16891                        ));
16892                    };
16893                    let binds = self
16894                        .match_array_pattern_elems(&arr, elems, line)
16895                        .map_err(|e| match e {
16896                            FlowOrError::Error(stryke) => stryke,
16897                            FlowOrError::Flow(_) => PerlError::runtime(
16898                                "unexpected flow in sub signature array destruct",
16899                                line,
16900                            ),
16901                        })?;
16902                    let Some(binds) = binds else {
16903                        return Err(PerlError::runtime(
16904                            "sub signature array destruct: length or element mismatch",
16905                            line,
16906                        ));
16907                    };
16908                    for b in binds {
16909                        match b {
16910                            PatternBinding::Scalar(name, v) => {
16911                                let n = self.english_scalar_name(&name);
16912                                self.scope.declare_scalar(n, v);
16913                            }
16914                            PatternBinding::Array(name, elems) => {
16915                                self.scope.declare_array(&name, elems);
16916                            }
16917                        }
16918                    }
16919                }
16920                SubSigParam::HashDestruct(pairs) => {
16921                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
16922                    i += 1;
16923                    let map = self.hash_for_signature_destruct(&arg, line)?;
16924                    for (key, varname) in pairs {
16925                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
16926                        let n = self.english_scalar_name(varname);
16927                        self.scope.declare_scalar(n, v);
16928                    }
16929                }
16930            }
16931        }
16932        Ok(())
16933    }
16934
16935    /// Dispatch higher-order function wrappers (`comp`, `partial`, `constantly`,
16936    /// `complement`, `fnil`, `juxt`, `memoize`, `curry`, `once`).
16937    /// These are `PerlSub`s with empty bodies and magic keys in `closure_env`.
16938    pub(crate) fn try_hof_dispatch(
16939        &mut self,
16940        sub: &PerlSub,
16941        args: &[PerlValue],
16942        want: WantarrayCtx,
16943        line: usize,
16944    ) -> Option<ExecResult> {
16945        let env = sub.closure_env.as_ref()?;
16946        fn env_get<'a>(env: &'a [(String, PerlValue)], key: &str) -> Option<&'a PerlValue> {
16947            env.iter().find(|(k, _)| k == key).map(|(_, v)| v)
16948        }
16949
16950        match sub.name.as_str() {
16951            // ── compose: right-to-left function application ──
16952            "__comp__" => {
16953                let fns = env_get(env, "__comp_fns__")?.to_list();
16954                let mut val = args.first().cloned().unwrap_or(PerlValue::UNDEF);
16955                for f in fns.iter().rev() {
16956                    match self.dispatch_indirect_call(f.clone(), vec![val], want, line) {
16957                        Ok(v) => val = v,
16958                        Err(e) => return Some(Err(e)),
16959                    }
16960                }
16961                Some(Ok(val))
16962            }
16963            // ── constantly: always return the captured value ──
16964            "__constantly__" => Some(Ok(env_get(env, "__const_val__")?.clone())),
16965            // ── juxt: call each fn with same args, collect results ──
16966            "__juxt__" => {
16967                let fns = env_get(env, "__juxt_fns__")?.to_list();
16968                let mut results = Vec::with_capacity(fns.len());
16969                for f in &fns {
16970                    match self.dispatch_indirect_call(f.clone(), args.to_vec(), want, line) {
16971                        Ok(v) => results.push(v),
16972                        Err(e) => return Some(Err(e)),
16973                    }
16974                }
16975                Some(Ok(PerlValue::array(results)))
16976            }
16977            // ── partial: prepend bound args ──
16978            "__partial__" => {
16979                let fn_val = env_get(env, "__partial_fn__")?.clone();
16980                let bound = env_get(env, "__partial_args__")?.to_list();
16981                let mut all_args = bound;
16982                all_args.extend_from_slice(args);
16983                Some(self.dispatch_indirect_call(fn_val, all_args, want, line))
16984            }
16985            // ── complement: negate the result ──
16986            "__complement__" => {
16987                let fn_val = env_get(env, "__complement_fn__")?.clone();
16988                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
16989                    Ok(v) => Some(Ok(PerlValue::integer(if v.is_true() { 0 } else { 1 }))),
16990                    Err(e) => Some(Err(e)),
16991                }
16992            }
16993            // ── fnil: replace undef args with defaults ──
16994            "__fnil__" => {
16995                let fn_val = env_get(env, "__fnil_fn__")?.clone();
16996                let defaults = env_get(env, "__fnil_defaults__")?.to_list();
16997                let mut patched = args.to_vec();
16998                for (i, d) in defaults.iter().enumerate() {
16999                    if i < patched.len() {
17000                        if patched[i].is_undef() {
17001                            patched[i] = d.clone();
17002                        }
17003                    } else {
17004                        patched.push(d.clone());
17005                    }
17006                }
17007                Some(self.dispatch_indirect_call(fn_val, patched, want, line))
17008            }
17009            // ── memoize: cache by stringified args ──
17010            "__memoize__" => {
17011                let fn_val = env_get(env, "__memoize_fn__")?.clone();
17012                let cache_ref = env_get(env, "__memoize_cache__")?.clone();
17013                let key = args
17014                    .iter()
17015                    .map(|a| a.to_string())
17016                    .collect::<Vec<_>>()
17017                    .join("\x00");
17018                if let Some(href) = cache_ref.as_hash_ref() {
17019                    if let Some(cached) = href.read().get(&key) {
17020                        return Some(Ok(cached.clone()));
17021                    }
17022                }
17023                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17024                    Ok(v) => {
17025                        if let Some(href) = cache_ref.as_hash_ref() {
17026                            href.write().insert(key, v.clone());
17027                        }
17028                        Some(Ok(v))
17029                    }
17030                    Err(e) => Some(Err(e)),
17031                }
17032            }
17033            // ── curry: accumulate args until arity reached ──
17034            "__curry__" => {
17035                let fn_val = env_get(env, "__curry_fn__")?.clone();
17036                let arity = env_get(env, "__curry_arity__")?.to_int() as usize;
17037                let bound = env_get(env, "__curry_bound__")?.to_list();
17038                let mut all = bound;
17039                all.extend_from_slice(args);
17040                if all.len() >= arity {
17041                    Some(self.dispatch_indirect_call(fn_val, all, want, line))
17042                } else {
17043                    let curry_sub = PerlSub {
17044                        name: "__curry__".to_string(),
17045                        params: vec![],
17046                        body: vec![],
17047                        closure_env: Some(vec![
17048                            ("__curry_fn__".to_string(), fn_val),
17049                            (
17050                                "__curry_arity__".to_string(),
17051                                PerlValue::integer(arity as i64),
17052                            ),
17053                            ("__curry_bound__".to_string(), PerlValue::array(all)),
17054                        ]),
17055                        prototype: None,
17056                        fib_like: None,
17057                    };
17058                    Some(Ok(PerlValue::code_ref(Arc::new(curry_sub))))
17059                }
17060            }
17061            // ── once: call once, cache forever ──
17062            "__once__" => {
17063                let cache_ref = env_get(env, "__once_cache__")?.clone();
17064                if let Some(href) = cache_ref.as_hash_ref() {
17065                    let r = href.read();
17066                    if r.contains_key("done") {
17067                        return Some(Ok(r.get("val").cloned().unwrap_or(PerlValue::UNDEF)));
17068                    }
17069                }
17070                let fn_val = env_get(env, "__once_fn__")?.clone();
17071                match self.dispatch_indirect_call(fn_val, args.to_vec(), want, line) {
17072                    Ok(v) => {
17073                        if let Some(href) = cache_ref.as_hash_ref() {
17074                            let mut w = href.write();
17075                            w.insert("done".to_string(), PerlValue::integer(1));
17076                            w.insert("val".to_string(), v.clone());
17077                        }
17078                        Some(Ok(v))
17079                    }
17080                    Err(e) => Some(Err(e)),
17081                }
17082            }
17083            _ => None,
17084        }
17085    }
17086
17087    pub(crate) fn call_sub(
17088        &mut self,
17089        sub: &PerlSub,
17090        args: Vec<PerlValue>,
17091        want: WantarrayCtx,
17092        _line: usize,
17093    ) -> ExecResult {
17094        // Push current sub for __SUB__ access
17095        self.current_sub_stack.push(Arc::new(sub.clone()));
17096
17097        // Single frame for both @_ and the block's local variables —
17098        // avoids the double push_frame/pop_frame overhead per call.
17099        self.scope_push_hook();
17100        self.scope.declare_array("_", args.clone());
17101        if let Some(ref env) = sub.closure_env {
17102            self.scope.restore_capture(env);
17103        }
17104        // Set $_0, $_1, $_2, ... for all args, and $_ to first arg
17105        // so `>{ $_ + 1 }` works instead of requiring `>{ $_[0] + 1 }`
17106        // Must be AFTER restore_capture so we don't get shadowed by captured $_
17107        self.scope.set_closure_args(&args);
17108        // Move `@_` out so `native_dispatch` / `fib_like` take `&[PerlValue]` without `get_array` cloning.
17109        let argv = self.scope.take_sub_underscore().unwrap_or_default();
17110        self.apply_sub_signature(sub, &argv, _line)?;
17111        let saved = self.wantarray_kind;
17112        self.wantarray_kind = want;
17113        if let Some(r) = crate::list_util::native_dispatch(self, sub, &argv, want) {
17114            self.wantarray_kind = saved;
17115            self.scope_pop_hook();
17116            self.current_sub_stack.pop();
17117            return match r {
17118                Ok(v) => Ok(v),
17119                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17120                Err(e) => Err(e),
17121            };
17122        }
17123        if let Some(r) = self.try_hof_dispatch(sub, &argv, want, _line) {
17124            self.wantarray_kind = saved;
17125            self.scope_pop_hook();
17126            self.current_sub_stack.pop();
17127            return match r {
17128                Ok(v) => Ok(v),
17129                Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17130                Err(e) => Err(e),
17131            };
17132        }
17133        if let Some(pat) = sub.fib_like.as_ref() {
17134            if argv.len() == 1 {
17135                if let Some(n0) = argv.first().and_then(|v| v.as_integer()) {
17136                    let t0 = self.profiler.is_some().then(std::time::Instant::now);
17137                    if let Some(p) = &mut self.profiler {
17138                        p.enter_sub(&sub.name);
17139                    }
17140                    let n = crate::fib_like_tail::eval_fib_like_recursive_add(n0, pat);
17141                    if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
17142                        p.exit_sub(t0.elapsed());
17143                    }
17144                    self.wantarray_kind = saved;
17145                    self.scope_pop_hook();
17146                    self.current_sub_stack.pop();
17147                    return Ok(PerlValue::integer(n));
17148                }
17149            }
17150        }
17151        self.scope.declare_array("_", argv.clone());
17152        // Note: set_closure_args was already called at line 15077; don't call it again
17153        // as that would incorrectly shift the outer topic stack a second time.
17154        let t0 = self.profiler.is_some().then(std::time::Instant::now);
17155        if let Some(p) = &mut self.profiler {
17156            p.enter_sub(&sub.name);
17157        }
17158        // Always evaluate the function body's last expression in List context so
17159        // `@array` returns the array contents, not the count. The caller adapts the
17160        // return value to their own wantarray context after receiving it.
17161        let result = self.exec_block_no_scope_with_tail(&sub.body, WantarrayCtx::List);
17162        if let (Some(p), Some(t0)) = (&mut self.profiler, t0) {
17163            p.exit_sub(t0.elapsed());
17164        }
17165        // For goto &sub, capture @_ before popping the frame
17166        let goto_args = if matches!(result, Err(FlowOrError::Flow(Flow::GotoSub(_)))) {
17167            Some(self.scope.get_array("_"))
17168        } else {
17169            None
17170        };
17171        self.wantarray_kind = saved;
17172        self.scope_pop_hook();
17173        self.current_sub_stack.pop();
17174        match result {
17175            Ok(v) => Ok(v),
17176            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17177            Err(FlowOrError::Flow(Flow::GotoSub(target_name))) => {
17178                // goto &sub — tail call: look up target and call with same @_
17179                let goto_args = goto_args.unwrap_or_default();
17180                let fqn = if target_name.contains("::") {
17181                    target_name.clone()
17182                } else {
17183                    format!("{}::{}", self.current_package(), target_name)
17184                };
17185                if let Some(target_sub) = self
17186                    .subs
17187                    .get(&fqn)
17188                    .cloned()
17189                    .or_else(|| self.subs.get(&target_name).cloned())
17190                {
17191                    self.call_sub(&target_sub, goto_args, want, _line)
17192                } else {
17193                    Err(
17194                        PerlError::runtime(format!("Undefined subroutine &{}", target_name), _line)
17195                            .into(),
17196                    )
17197                }
17198            }
17199            Err(FlowOrError::Flow(Flow::Yield(_))) => {
17200                Err(PerlError::runtime("yield is only valid inside gen { }", 0).into())
17201            }
17202            Err(e) => Err(e),
17203        }
17204    }
17205
17206    /// Call a user-defined struct method: `$p->distance()` where `fn distance { }` is in struct.
17207    fn call_struct_method(
17208        &mut self,
17209        body: &Block,
17210        params: &[SubSigParam],
17211        args: Vec<PerlValue>,
17212        line: usize,
17213    ) -> ExecResult {
17214        self.scope_push_hook();
17215        self.scope.declare_array("_", args.clone());
17216        // Bind $self to first arg (the receiver)
17217        if let Some(self_val) = args.first() {
17218            self.scope.declare_scalar("self", self_val.clone());
17219        }
17220        // Set $_0, $_1, etc. for all args
17221        self.scope.set_closure_args(&args);
17222        // Apply signature if provided - skip the first arg ($self) for user params
17223        let user_args: Vec<PerlValue> = args.iter().skip(1).cloned().collect();
17224        self.apply_params_to_argv(params, &user_args, line)?;
17225        let result = self.exec_block_no_scope(body);
17226        self.scope_pop_hook();
17227        match result {
17228            Ok(v) => Ok(v),
17229            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17230            Err(e) => Err(e),
17231        }
17232    }
17233
17234    /// Call a user-defined class method: `$dog->bark()` where `fn bark { }` is in class.
17235    pub(crate) fn call_class_method(
17236        &mut self,
17237        body: &Block,
17238        params: &[SubSigParam],
17239        args: Vec<PerlValue>,
17240        line: usize,
17241    ) -> ExecResult {
17242        self.call_class_method_inner(body, params, args, line, false)
17243    }
17244
17245    /// Call a static class method: `Math::add(...)`.
17246    pub(crate) fn call_static_class_method(
17247        &mut self,
17248        body: &Block,
17249        params: &[SubSigParam],
17250        args: Vec<PerlValue>,
17251        line: usize,
17252    ) -> ExecResult {
17253        self.call_class_method_inner(body, params, args, line, true)
17254    }
17255
17256    fn call_class_method_inner(
17257        &mut self,
17258        body: &Block,
17259        params: &[SubSigParam],
17260        args: Vec<PerlValue>,
17261        line: usize,
17262        is_static: bool,
17263    ) -> ExecResult {
17264        self.scope_push_hook();
17265        self.scope.declare_array("_", args.clone());
17266        if !is_static {
17267            // Bind $self to first arg (the receiver) for instance methods
17268            if let Some(self_val) = args.first() {
17269                self.scope.declare_scalar("self", self_val.clone());
17270            }
17271        }
17272        // Set $_0, $_1, etc. for all args
17273        self.scope.set_closure_args(&args);
17274        // Apply signature: skip first arg ($self) only for instance methods
17275        let user_args: Vec<PerlValue> = if is_static {
17276            args.clone()
17277        } else {
17278            args.iter().skip(1).cloned().collect()
17279        };
17280        self.apply_params_to_argv(params, &user_args, line)?;
17281        let result = self.exec_block_no_scope(body);
17282        self.scope_pop_hook();
17283        match result {
17284            Ok(v) => Ok(v),
17285            Err(FlowOrError::Flow(Flow::Return(v))) => Ok(v),
17286            Err(e) => Err(e),
17287        }
17288    }
17289
17290    /// Apply SubSigParam bindings without the full PerlSub machinery.
17291    fn apply_params_to_argv(
17292        &mut self,
17293        params: &[SubSigParam],
17294        argv: &[PerlValue],
17295        line: usize,
17296    ) -> PerlResult<()> {
17297        let mut i = 0;
17298        for param in params {
17299            match param {
17300                SubSigParam::Scalar(name, ty_opt) => {
17301                    let v = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17302                    i += 1;
17303                    if let Some(ty) = ty_opt {
17304                        ty.check_value(&v).map_err(|msg| {
17305                            PerlError::type_error(
17306                                format!("method parameter ${}: {}", name, msg),
17307                                line,
17308                            )
17309                        })?;
17310                    }
17311                    let n = self.english_scalar_name(name);
17312                    self.scope.declare_scalar(n, v);
17313                }
17314                SubSigParam::Array(name) => {
17315                    let rest: Vec<PerlValue> = argv[i..].to_vec();
17316                    i = argv.len();
17317                    let aname = self.stash_array_name_for_package(name);
17318                    self.scope.declare_array(&aname, rest);
17319                }
17320                SubSigParam::Hash(name) => {
17321                    let rest: Vec<PerlValue> = argv[i..].to_vec();
17322                    i = argv.len();
17323                    let mut map = IndexMap::new();
17324                    let mut j = 0;
17325                    while j + 1 < rest.len() {
17326                        map.insert(rest[j].to_string(), rest[j + 1].clone());
17327                        j += 2;
17328                    }
17329                    self.scope.declare_hash(name, map);
17330                }
17331                SubSigParam::ArrayDestruct(elems) => {
17332                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17333                    i += 1;
17334                    let Some(arr) = self.match_subject_as_array(&arg) else {
17335                        return Err(PerlError::runtime(
17336                            format!("method parameter: expected ARRAY, got {}", arg.ref_type()),
17337                            line,
17338                        ));
17339                    };
17340                    let binds = self
17341                        .match_array_pattern_elems(&arr, elems, line)
17342                        .map_err(|e| match e {
17343                            FlowOrError::Error(stryke) => stryke,
17344                            FlowOrError::Flow(_) => {
17345                                PerlError::runtime("unexpected flow in method array destruct", line)
17346                            }
17347                        })?;
17348                    let Some(binds) = binds else {
17349                        return Err(PerlError::runtime(
17350                            format!(
17351                                "method parameter: array destructure failed at position {}",
17352                                i
17353                            ),
17354                            line,
17355                        ));
17356                    };
17357                    for b in binds {
17358                        match b {
17359                            PatternBinding::Scalar(name, v) => {
17360                                let n = self.english_scalar_name(&name);
17361                                self.scope.declare_scalar(n, v);
17362                            }
17363                            PatternBinding::Array(name, elems) => {
17364                                self.scope.declare_array(&name, elems);
17365                            }
17366                        }
17367                    }
17368                }
17369                SubSigParam::HashDestruct(pairs) => {
17370                    let arg = argv.get(i).cloned().unwrap_or(PerlValue::UNDEF);
17371                    i += 1;
17372                    let map = self.hash_for_signature_destruct(&arg, line)?;
17373                    for (key, varname) in pairs {
17374                        let v = map.get(key).cloned().unwrap_or(PerlValue::UNDEF);
17375                        let n = self.english_scalar_name(varname);
17376                        self.scope.declare_scalar(n, v);
17377                    }
17378                }
17379            }
17380        }
17381        Ok(())
17382    }
17383
17384    fn builtin_new(&mut self, class: &str, args: Vec<PerlValue>, line: usize) -> ExecResult {
17385        if class == "Set" {
17386            return Ok(crate::value::set_from_elements(args.into_iter().skip(1)));
17387        }
17388        if let Some(def) = self.struct_defs.get(class).cloned() {
17389            let mut provided = Vec::new();
17390            let mut i = 1;
17391            while i + 1 < args.len() {
17392                let k = args[i].to_string();
17393                let v = args[i + 1].clone();
17394                provided.push((k, v));
17395                i += 2;
17396            }
17397            let mut defaults = Vec::with_capacity(def.fields.len());
17398            for field in &def.fields {
17399                if let Some(ref expr) = field.default {
17400                    let val = self.eval_expr(expr)?;
17401                    defaults.push(Some(val));
17402                } else {
17403                    defaults.push(None);
17404                }
17405            }
17406            return Ok(crate::native_data::struct_new_with_defaults(
17407                &def, &provided, &defaults, line,
17408            )?);
17409        }
17410        // Default OO constructor: Class->new(%args) → bless {%args}, class
17411        let mut map = IndexMap::new();
17412        let mut i = 1; // skip $self (first arg is class name)
17413        while i + 1 < args.len() {
17414            let k = args[i].to_string();
17415            let v = args[i + 1].clone();
17416            map.insert(k, v);
17417            i += 2;
17418        }
17419        Ok(PerlValue::blessed(Arc::new(
17420            crate::value::BlessedRef::new_blessed(class.to_string(), PerlValue::hash(map)),
17421        )))
17422    }
17423
17424    fn exec_print(
17425        &mut self,
17426        handle: Option<&str>,
17427        args: &[Expr],
17428        newline: bool,
17429        line: usize,
17430    ) -> ExecResult {
17431        if newline && (self.feature_bits & FEAT_SAY) == 0 {
17432            return Err(PerlError::runtime(
17433                "say() is disabled (enable with use feature 'say' or use feature ':5.10')",
17434                line,
17435            )
17436            .into());
17437        }
17438        let mut output = String::new();
17439        if args.is_empty() {
17440            // Perl: print with no LIST prints $_ (same for say).
17441            let topic = self.scope.get_scalar("_").clone();
17442            let s = self.stringify_value(topic, line)?;
17443            output.push_str(&s);
17444        } else {
17445            // Perl: each comma-separated EXPR is evaluated in list context; `$ofs` is inserted
17446            // between those top-level expressions only (not between elements of an expanded `@arr`).
17447            for (i, a) in args.iter().enumerate() {
17448                if i > 0 {
17449                    output.push_str(&self.ofs);
17450                }
17451                let val = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17452                for item in val.to_list() {
17453                    let s = self.stringify_value(item, line)?;
17454                    output.push_str(&s);
17455                }
17456            }
17457        }
17458        if newline {
17459            output.push('\n');
17460        }
17461        output.push_str(&self.ors);
17462
17463        let handle_name =
17464            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17465        self.write_formatted_print(handle_name.as_str(), &output, line)?;
17466        Ok(PerlValue::integer(1))
17467    }
17468
17469    fn exec_printf(&mut self, handle: Option<&str>, args: &[Expr], line: usize) -> ExecResult {
17470        let (fmt, rest): (String, &[Expr]) = if args.is_empty() {
17471            // Perl: printf with no args uses $_ as the format string.
17472            let s = self.stringify_value(self.scope.get_scalar("_").clone(), line)?;
17473            (s, &[])
17474        } else {
17475            (self.eval_expr(&args[0])?.to_string(), &args[1..])
17476        };
17477        // printf arg list after the format is Perl list context — `1..5`, `@arr`, `reverse`,
17478        // `grep`, etc. flatten into the format argument sequence. Scalar context collapses
17479        // ranges to flip-flop values, so go through list-context eval and splat.
17480        let mut arg_vals = Vec::new();
17481        for a in rest {
17482            let v = self.eval_expr_ctx(a, WantarrayCtx::List)?;
17483            if let Some(items) = v.as_array_vec() {
17484                arg_vals.extend(items);
17485            } else {
17486                arg_vals.push(v);
17487            }
17488        }
17489        let output = self.perl_sprintf_stringify(&fmt, &arg_vals, line)?;
17490        let handle_name =
17491            self.resolve_io_handle_name(handle.unwrap_or(self.default_print_handle.as_str()));
17492        match handle_name.as_str() {
17493            "STDOUT" => {
17494                if !self.suppress_stdout {
17495                    print!("{}", output);
17496                    if self.output_autoflush {
17497                        let _ = io::stdout().flush();
17498                    }
17499                }
17500            }
17501            "STDERR" => {
17502                eprint!("{}", output);
17503                let _ = io::stderr().flush();
17504            }
17505            name => {
17506                if let Some(writer) = self.output_handles.get_mut(name) {
17507                    let _ = writer.write_all(output.as_bytes());
17508                    if self.output_autoflush {
17509                        let _ = writer.flush();
17510                    }
17511                }
17512            }
17513        }
17514        Ok(PerlValue::integer(1))
17515    }
17516
17517    /// `substr` with optional replacement — mutates `string` when `replacement` is `Some` (also used by VM).
17518    pub(crate) fn eval_substr_expr(
17519        &mut self,
17520        string: &Expr,
17521        offset: &Expr,
17522        length: Option<&Expr>,
17523        replacement: Option<&Expr>,
17524        _line: usize,
17525    ) -> Result<PerlValue, FlowOrError> {
17526        let s = self.eval_expr(string)?.to_string();
17527        let off = self.eval_expr(offset)?.to_int();
17528        let start = if off < 0 {
17529            (s.len() as i64 + off).max(0) as usize
17530        } else {
17531            off as usize
17532        };
17533        let len = if let Some(l) = length {
17534            let len_val = self.eval_expr(l)?.to_int();
17535            if len_val < 0 {
17536                // Negative length: count from end of string
17537                let remaining = s.len().saturating_sub(start) as i64;
17538                (remaining + len_val).max(0) as usize
17539            } else {
17540                len_val as usize
17541            }
17542        } else {
17543            s.len().saturating_sub(start)
17544        };
17545        let end = start.saturating_add(len).min(s.len());
17546        let result = s.get(start..end).unwrap_or("").to_string();
17547        if let Some(rep) = replacement {
17548            let rep_s = self.eval_expr(rep)?.to_string();
17549            let mut new_s = String::new();
17550            new_s.push_str(&s[..start]);
17551            new_s.push_str(&rep_s);
17552            new_s.push_str(&s[end..]);
17553            self.assign_value(string, PerlValue::string(new_s))?;
17554        }
17555        Ok(PerlValue::string(result))
17556    }
17557
17558    pub(crate) fn eval_push_expr(
17559        &mut self,
17560        array: &Expr,
17561        values: &[Expr],
17562        line: usize,
17563    ) -> Result<PerlValue, FlowOrError> {
17564        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17565            for v in values {
17566                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17567                self.push_array_deref_value(aref.clone(), val, line)?;
17568            }
17569            let len = self.array_deref_len(aref, line)?;
17570            return Ok(PerlValue::integer(len));
17571        }
17572        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17573        if self.scope.is_array_frozen(&arr_name) {
17574            return Err(PerlError::runtime(
17575                format!("Modification of a frozen value: @{}", arr_name),
17576                line,
17577            )
17578            .into());
17579        }
17580        for v in values {
17581            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17582            if let Some(items) = val.as_array_vec() {
17583                for item in items {
17584                    self.scope
17585                        .push_to_array(&arr_name, item)
17586                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17587                }
17588            } else {
17589                self.scope
17590                    .push_to_array(&arr_name, val)
17591                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17592            }
17593        }
17594        let len = self.scope.array_len(&arr_name);
17595        Ok(PerlValue::integer(len as i64))
17596    }
17597
17598    pub(crate) fn eval_pop_expr(
17599        &mut self,
17600        array: &Expr,
17601        line: usize,
17602    ) -> Result<PerlValue, FlowOrError> {
17603        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17604            return self.pop_array_deref(aref, line);
17605        }
17606        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17607        self.scope
17608            .pop_from_array(&arr_name)
17609            .map_err(|e| FlowOrError::Error(e.at_line(line)))
17610    }
17611
17612    pub(crate) fn eval_shift_expr(
17613        &mut self,
17614        array: &Expr,
17615        line: usize,
17616    ) -> Result<PerlValue, FlowOrError> {
17617        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17618            return self.shift_array_deref(aref, line);
17619        }
17620        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17621        self.scope
17622            .shift_from_array(&arr_name)
17623            .map_err(|e| FlowOrError::Error(e.at_line(line)))
17624    }
17625
17626    pub(crate) fn eval_unshift_expr(
17627        &mut self,
17628        array: &Expr,
17629        values: &[Expr],
17630        line: usize,
17631    ) -> Result<PerlValue, FlowOrError> {
17632        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17633            let mut vals = Vec::new();
17634            for v in values {
17635                let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17636                if let Some(items) = val.as_array_vec() {
17637                    vals.extend(items);
17638                } else {
17639                    vals.push(val);
17640                }
17641            }
17642            let len = self.unshift_array_deref_multi(aref, vals, line)?;
17643            return Ok(PerlValue::integer(len));
17644        }
17645        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17646        let mut vals = Vec::new();
17647        for v in values {
17648            let val = self.eval_expr_ctx(v, WantarrayCtx::List)?;
17649            if let Some(items) = val.as_array_vec() {
17650                vals.extend(items);
17651            } else {
17652                vals.push(val);
17653            }
17654        }
17655        let arr = self
17656            .scope
17657            .get_array_mut(&arr_name)
17658            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17659        for (i, v) in vals.into_iter().enumerate() {
17660            arr.insert(i, v);
17661        }
17662        let len = arr.len();
17663        Ok(PerlValue::integer(len as i64))
17664    }
17665
17666    /// One `push` element onto an array ref or package array name (symbolic `@{"Pkg::A"}`).
17667    pub(crate) fn push_array_deref_value(
17668        &mut self,
17669        arr_ref: PerlValue,
17670        val: PerlValue,
17671        line: usize,
17672    ) -> Result<(), FlowOrError> {
17673        if let Some(r) = arr_ref.as_array_ref() {
17674            let mut w = r.write();
17675            if let Some(items) = val.as_array_vec() {
17676                w.extend(items.iter().cloned());
17677            } else {
17678                w.push(val);
17679            }
17680            return Ok(());
17681        }
17682        if let Some(name) = arr_ref.as_array_binding_name() {
17683            if let Some(items) = val.as_array_vec() {
17684                for item in items {
17685                    self.scope
17686                        .push_to_array(&name, item)
17687                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17688                }
17689            } else {
17690                self.scope
17691                    .push_to_array(&name, val)
17692                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17693            }
17694            return Ok(());
17695        }
17696        if let Some(s) = arr_ref.as_str() {
17697            if self.strict_refs {
17698                return Err(PerlError::runtime(
17699                    format!(
17700                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17701                        s
17702                    ),
17703                    line,
17704                )
17705                .into());
17706            }
17707            let name = s.to_string();
17708            if let Some(items) = val.as_array_vec() {
17709                for item in items {
17710                    self.scope
17711                        .push_to_array(&name, item)
17712                        .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17713                }
17714            } else {
17715                self.scope
17716                    .push_to_array(&name, val)
17717                    .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17718            }
17719            return Ok(());
17720        }
17721        Err(PerlError::runtime("push argument is not an ARRAY reference", line).into())
17722    }
17723
17724    pub(crate) fn array_deref_len(
17725        &self,
17726        arr_ref: PerlValue,
17727        line: usize,
17728    ) -> Result<i64, FlowOrError> {
17729        if let Some(r) = arr_ref.as_array_ref() {
17730            return Ok(r.read().len() as i64);
17731        }
17732        if let Some(name) = arr_ref.as_array_binding_name() {
17733            return Ok(self.scope.array_len(&name) as i64);
17734        }
17735        if let Some(s) = arr_ref.as_str() {
17736            if self.strict_refs {
17737                return Err(PerlError::runtime(
17738                    format!(
17739                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17740                        s
17741                    ),
17742                    line,
17743                )
17744                .into());
17745            }
17746            return Ok(self.scope.array_len(&s) as i64);
17747        }
17748        Err(PerlError::runtime("argument is not an ARRAY reference", line).into())
17749    }
17750
17751    pub(crate) fn pop_array_deref(
17752        &mut self,
17753        arr_ref: PerlValue,
17754        line: usize,
17755    ) -> Result<PerlValue, FlowOrError> {
17756        if let Some(r) = arr_ref.as_array_ref() {
17757            let mut w = r.write();
17758            return Ok(w.pop().unwrap_or(PerlValue::UNDEF));
17759        }
17760        if let Some(name) = arr_ref.as_array_binding_name() {
17761            return self
17762                .scope
17763                .pop_from_array(&name)
17764                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17765        }
17766        if let Some(s) = arr_ref.as_str() {
17767            if self.strict_refs {
17768                return Err(PerlError::runtime(
17769                    format!(
17770                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17771                        s
17772                    ),
17773                    line,
17774                )
17775                .into());
17776            }
17777            return self
17778                .scope
17779                .pop_from_array(&s)
17780                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17781        }
17782        Err(PerlError::runtime("pop argument is not an ARRAY reference", line).into())
17783    }
17784
17785    pub(crate) fn shift_array_deref(
17786        &mut self,
17787        arr_ref: PerlValue,
17788        line: usize,
17789    ) -> Result<PerlValue, FlowOrError> {
17790        if let Some(r) = arr_ref.as_array_ref() {
17791            let mut w = r.write();
17792            return Ok(if w.is_empty() {
17793                PerlValue::UNDEF
17794            } else {
17795                w.remove(0)
17796            });
17797        }
17798        if let Some(name) = arr_ref.as_array_binding_name() {
17799            return self
17800                .scope
17801                .shift_from_array(&name)
17802                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17803        }
17804        if let Some(s) = arr_ref.as_str() {
17805            if self.strict_refs {
17806                return Err(PerlError::runtime(
17807                    format!(
17808                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17809                        s
17810                    ),
17811                    line,
17812                )
17813                .into());
17814            }
17815            return self
17816                .scope
17817                .shift_from_array(&s)
17818                .map_err(|e| FlowOrError::Error(e.at_line(line)));
17819        }
17820        Err(PerlError::runtime("shift argument is not an ARRAY reference", line).into())
17821    }
17822
17823    pub(crate) fn unshift_array_deref_multi(
17824        &mut self,
17825        arr_ref: PerlValue,
17826        vals: Vec<PerlValue>,
17827        line: usize,
17828    ) -> Result<i64, FlowOrError> {
17829        let mut flat: Vec<PerlValue> = Vec::new();
17830        for v in vals {
17831            if let Some(items) = v.as_array_vec() {
17832                flat.extend(items);
17833            } else {
17834                flat.push(v);
17835            }
17836        }
17837        if let Some(r) = arr_ref.as_array_ref() {
17838            let mut w = r.write();
17839            for (i, v) in flat.into_iter().enumerate() {
17840                w.insert(i, v);
17841            }
17842            return Ok(w.len() as i64);
17843        }
17844        if let Some(name) = arr_ref.as_array_binding_name() {
17845            let arr = self
17846                .scope
17847                .get_array_mut(&name)
17848                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17849            for (i, v) in flat.into_iter().enumerate() {
17850                arr.insert(i, v);
17851            }
17852            return Ok(arr.len() as i64);
17853        }
17854        if let Some(s) = arr_ref.as_str() {
17855            if self.strict_refs {
17856                return Err(PerlError::runtime(
17857                    format!(
17858                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17859                        s
17860                    ),
17861                    line,
17862                )
17863                .into());
17864            }
17865            let name = s.to_string();
17866            let arr = self
17867                .scope
17868                .get_array_mut(&name)
17869                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17870            for (i, v) in flat.into_iter().enumerate() {
17871                arr.insert(i, v);
17872            }
17873            return Ok(arr.len() as i64);
17874        }
17875        Err(PerlError::runtime("unshift argument is not an ARRAY reference", line).into())
17876    }
17877
17878    /// `splice @$aref, OFFSET, LENGTH, LIST` — uses [`Self::wantarray_kind`] (VM [`Op::WantarrayPush`]
17879    /// / compiler wraps `splice` like other context-sensitive builtins).
17880    pub(crate) fn splice_array_deref(
17881        &mut self,
17882        aref: PerlValue,
17883        offset_val: PerlValue,
17884        length_val: PerlValue,
17885        rep_vals: Vec<PerlValue>,
17886        line: usize,
17887    ) -> Result<PerlValue, FlowOrError> {
17888        let ctx = self.wantarray_kind;
17889        if let Some(r) = aref.as_array_ref() {
17890            let arr_len = r.read().len();
17891            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17892            let mut w = r.write();
17893            let removed: Vec<PerlValue> = w.drain(off..end).collect();
17894            for (i, v) in rep_vals.into_iter().enumerate() {
17895                w.insert(off + i, v);
17896            }
17897            return Ok(match ctx {
17898                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
17899                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
17900            });
17901        }
17902        if let Some(name) = aref.as_array_binding_name() {
17903            let arr_len = self.scope.array_len(&name);
17904            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17905            let arr = self
17906                .scope
17907                .get_array_mut(&name)
17908                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17909            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
17910            for (i, v) in rep_vals.into_iter().enumerate() {
17911                arr.insert(off + i, v);
17912            }
17913            return Ok(match ctx {
17914                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
17915                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
17916            });
17917        }
17918        if let Some(s) = aref.as_str() {
17919            if self.strict_refs {
17920                return Err(PerlError::runtime(
17921                    format!(
17922                        "Can't use string (\"{}\") as an ARRAY ref while \"strict refs\" in use",
17923                        s
17924                    ),
17925                    line,
17926                )
17927                .into());
17928            }
17929            let arr_len = self.scope.array_len(&s);
17930            let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17931            let arr = self
17932                .scope
17933                .get_array_mut(&s)
17934                .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17935            let removed: Vec<PerlValue> = arr.drain(off..end).collect();
17936            for (i, v) in rep_vals.into_iter().enumerate() {
17937                arr.insert(off + i, v);
17938            }
17939            return Ok(match ctx {
17940                WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
17941                WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
17942            });
17943        }
17944        Err(PerlError::runtime("splice argument is not an ARRAY reference", line).into())
17945    }
17946
17947    pub(crate) fn eval_splice_expr(
17948        &mut self,
17949        array: &Expr,
17950        offset: Option<&Expr>,
17951        length: Option<&Expr>,
17952        replacement: &[Expr],
17953        ctx: WantarrayCtx,
17954        line: usize,
17955    ) -> Result<PerlValue, FlowOrError> {
17956        if let Some(aref) = self.try_eval_array_deref_container(array)? {
17957            let offset_val = if let Some(o) = offset {
17958                self.eval_expr(o)?
17959            } else {
17960                PerlValue::integer(0)
17961            };
17962            let length_val = if let Some(l) = length {
17963                self.eval_expr(l)?
17964            } else {
17965                PerlValue::UNDEF
17966            };
17967            let mut rep_vals = Vec::new();
17968            for r in replacement {
17969                rep_vals.push(self.eval_expr(r)?);
17970            }
17971            let saved = self.wantarray_kind;
17972            self.wantarray_kind = ctx;
17973            let out = self.splice_array_deref(aref, offset_val, length_val, rep_vals, line);
17974            self.wantarray_kind = saved;
17975            return out;
17976        }
17977        let arr_name = self.extract_array_name(Self::peel_array_builtin_operand(array))?;
17978        let arr_len = self.scope.array_len(&arr_name);
17979        let offset_val = if let Some(o) = offset {
17980            self.eval_expr(o)?
17981        } else {
17982            PerlValue::integer(0)
17983        };
17984        let length_val = if let Some(l) = length {
17985            self.eval_expr(l)?
17986        } else {
17987            PerlValue::UNDEF
17988        };
17989        let (off, end) = splice_compute_range(arr_len, &offset_val, &length_val);
17990        let mut rep_vals = Vec::new();
17991        for r in replacement {
17992            rep_vals.push(self.eval_expr(r)?);
17993        }
17994        let arr = self
17995            .scope
17996            .get_array_mut(&arr_name)
17997            .map_err(|e| FlowOrError::Error(e.at_line(line)))?;
17998        let removed: Vec<PerlValue> = arr.drain(off..end).collect();
17999        for (i, v) in rep_vals.into_iter().enumerate() {
18000            arr.insert(off + i, v);
18001        }
18002        Ok(match ctx {
18003            WantarrayCtx::Scalar => removed.last().cloned().unwrap_or(PerlValue::UNDEF),
18004            WantarrayCtx::List | WantarrayCtx::Void => PerlValue::array(removed),
18005        })
18006    }
18007
18008    /// Result of `keys EXPR` after `EXPR` has been evaluated (VM opcode path or tests).
18009    pub(crate) fn keys_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
18010        if let Some(h) = val.as_hash_map() {
18011            Ok(PerlValue::array(
18012                h.keys().map(|k| PerlValue::string(k.clone())).collect(),
18013            ))
18014        } else if let Some(r) = val.as_hash_ref() {
18015            Ok(PerlValue::array(
18016                r.read()
18017                    .keys()
18018                    .map(|k| PerlValue::string(k.clone()))
18019                    .collect(),
18020            ))
18021        } else {
18022            Err(PerlError::runtime("keys requires hash", line).into())
18023        }
18024    }
18025
18026    pub(crate) fn eval_keys_expr(
18027        &mut self,
18028        expr: &Expr,
18029        line: usize,
18030    ) -> Result<PerlValue, FlowOrError> {
18031        // Operand must be evaluated in list context so `%h` stays a hash (scalar context would
18032        // apply `scalar %h`, not a hash value — breaks `keys` / `values` / `each` fallbacks).
18033        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
18034        Self::keys_from_value(val, line)
18035    }
18036
18037    /// Result of `values EXPR` after `EXPR` has been evaluated.
18038    pub(crate) fn values_from_value(val: PerlValue, line: usize) -> Result<PerlValue, FlowOrError> {
18039        if let Some(h) = val.as_hash_map() {
18040            Ok(PerlValue::array(h.values().cloned().collect()))
18041        } else if let Some(r) = val.as_hash_ref() {
18042            Ok(PerlValue::array(r.read().values().cloned().collect()))
18043        } else {
18044            Err(PerlError::runtime("values requires hash", line).into())
18045        }
18046    }
18047
18048    pub(crate) fn eval_values_expr(
18049        &mut self,
18050        expr: &Expr,
18051        line: usize,
18052    ) -> Result<PerlValue, FlowOrError> {
18053        let val = self.eval_expr_ctx(expr, WantarrayCtx::List)?;
18054        Self::values_from_value(val, line)
18055    }
18056
18057    pub(crate) fn eval_delete_operand(
18058        &mut self,
18059        expr: &Expr,
18060        line: usize,
18061    ) -> Result<PerlValue, FlowOrError> {
18062        match &expr.kind {
18063            ExprKind::HashElement { hash, key } => {
18064                let k = self.eval_expr(key)?.to_string();
18065                self.touch_env_hash(hash);
18066                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
18067                    let class = obj
18068                        .as_blessed_ref()
18069                        .map(|b| b.class.clone())
18070                        .unwrap_or_default();
18071                    let full = format!("{}::DELETE", class);
18072                    if let Some(sub) = self.subs.get(&full).cloned() {
18073                        return self.call_sub(
18074                            &sub,
18075                            vec![obj, PerlValue::string(k)],
18076                            WantarrayCtx::Scalar,
18077                            line,
18078                        );
18079                    }
18080                }
18081                self.scope
18082                    .delete_hash_element(hash, &k)
18083                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
18084            }
18085            ExprKind::ArrayElement { array, index } => {
18086                self.check_strict_array_var(array, line)?;
18087                let idx = self.eval_expr(index)?.to_int();
18088                let aname = self.stash_array_name_for_package(array);
18089                self.scope
18090                    .delete_array_element(&aname, idx)
18091                    .map_err(|e| FlowOrError::Error(e.at_line(line)))
18092            }
18093            ExprKind::ArrowDeref {
18094                expr: inner,
18095                index,
18096                kind: DerefKind::Hash,
18097            } => {
18098                let k = self.eval_expr(index)?.to_string();
18099                let container = self.eval_expr(inner)?;
18100                self.delete_arrow_hash_element(container, &k, line)
18101                    .map_err(Into::into)
18102            }
18103            ExprKind::ArrowDeref {
18104                expr: inner,
18105                index,
18106                kind: DerefKind::Array,
18107            } => {
18108                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
18109                    return Err(PerlError::runtime(
18110                        "delete on array element needs scalar subscript",
18111                        line,
18112                    )
18113                    .into());
18114                }
18115                let container = self.eval_expr(inner)?;
18116                let idx = self.eval_expr(index)?.to_int();
18117                self.delete_arrow_array_element(container, idx, line)
18118                    .map_err(Into::into)
18119            }
18120            _ => Err(PerlError::runtime("delete requires hash or array element", line).into()),
18121        }
18122    }
18123
18124    pub(crate) fn eval_exists_operand(
18125        &mut self,
18126        expr: &Expr,
18127        line: usize,
18128    ) -> Result<PerlValue, FlowOrError> {
18129        match &expr.kind {
18130            ExprKind::HashElement { hash, key } => {
18131                let k = self.eval_expr(key)?.to_string();
18132                self.touch_env_hash(hash);
18133                if let Some(obj) = self.tied_hashes.get(hash).cloned() {
18134                    let class = obj
18135                        .as_blessed_ref()
18136                        .map(|b| b.class.clone())
18137                        .unwrap_or_default();
18138                    let full = format!("{}::EXISTS", class);
18139                    if let Some(sub) = self.subs.get(&full).cloned() {
18140                        return self.call_sub(
18141                            &sub,
18142                            vec![obj, PerlValue::string(k)],
18143                            WantarrayCtx::Scalar,
18144                            line,
18145                        );
18146                    }
18147                }
18148                Ok(PerlValue::integer(
18149                    if self.scope.exists_hash_element(hash, &k) {
18150                        1
18151                    } else {
18152                        0
18153                    },
18154                ))
18155            }
18156            ExprKind::ArrayElement { array, index } => {
18157                self.check_strict_array_var(array, line)?;
18158                let idx = self.eval_expr(index)?.to_int();
18159                let aname = self.stash_array_name_for_package(array);
18160                Ok(PerlValue::integer(
18161                    if self.scope.exists_array_element(&aname, idx) {
18162                        1
18163                    } else {
18164                        0
18165                    },
18166                ))
18167            }
18168            ExprKind::ArrowDeref {
18169                expr: inner,
18170                index,
18171                kind: DerefKind::Hash,
18172            } => {
18173                let k = self.eval_expr(index)?.to_string();
18174                let container = self.eval_expr(inner)?;
18175                let yes = self.exists_arrow_hash_element(container, &k, line)?;
18176                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
18177            }
18178            ExprKind::ArrowDeref {
18179                expr: inner,
18180                index,
18181                kind: DerefKind::Array,
18182            } => {
18183                if !crate::compiler::arrow_deref_arrow_subscript_is_plain_scalar_index(index) {
18184                    return Err(PerlError::runtime(
18185                        "exists on array element needs scalar subscript",
18186                        line,
18187                    )
18188                    .into());
18189                }
18190                let container = self.eval_expr(inner)?;
18191                let idx = self.eval_expr(index)?.to_int();
18192                let yes = self.exists_arrow_array_element(container, idx, line)?;
18193                Ok(PerlValue::integer(if yes { 1 } else { 0 }))
18194            }
18195            _ => Err(PerlError::runtime("exists requires hash or array element", line).into()),
18196        }
18197    }
18198
18199    /// `pmap_on $cluster { ... } @list` — distributed map over an SSH worker pool.
18200    ///
18201    /// Uses the persistent dispatcher in [`crate::cluster`]: one ssh process per slot,
18202    /// HELLO + SESSION_INIT once per slot lifetime, JOB frames flowing over a shared work
18203    /// queue, fault tolerance via re-enqueue + retry budget. The basic v1 fan-out (one
18204    /// ssh per item) was replaced because it spent ~50–200 ms per item on ssh handshakes;
18205    /// the new path amortizes the handshake across the whole map.
18206    pub(crate) fn eval_pmap_remote(
18207        &mut self,
18208        cluster_pv: PerlValue,
18209        list_pv: PerlValue,
18210        show_progress: bool,
18211        block: &Block,
18212        flat_outputs: bool,
18213        line: usize,
18214    ) -> Result<PerlValue, FlowOrError> {
18215        let Some(cluster) = cluster_pv.as_remote_cluster() else {
18216            return Err(PerlError::runtime("pmap_on: expected cluster(...) value", line).into());
18217        };
18218        let items = list_pv.to_list();
18219        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18220        if !atomic_arrays.is_empty() || !atomic_hashes.is_empty() {
18221            return Err(PerlError::runtime(
18222                "pmap_on: mysync/atomic capture is not supported for remote workers",
18223                line,
18224            )
18225            .into());
18226        }
18227        let cap_json = crate::remote_wire::capture_entries_to_json(&scope_capture)
18228            .map_err(|e| PerlError::runtime(e, line))?;
18229        let subs_prelude = crate::remote_wire::build_subs_prelude(&self.subs);
18230        let block_src = crate::fmt::format_block(block);
18231        let item_jsons =
18232            crate::cluster::perl_items_to_json(&items).map_err(|e| PerlError::runtime(e, line))?;
18233
18234        // Progress bar (best effort) — ticks once per result. The dispatcher itself is
18235        // synchronous from the caller's POV, so we drive the bar before/after the call.
18236        let pmap_progress = PmapProgress::new(show_progress, items.len());
18237        let result_values =
18238            crate::cluster::run_cluster(&cluster, subs_prelude, block_src, cap_json, item_jsons)
18239                .map_err(|e| PerlError::runtime(format!("pmap_on remote: {e}"), line))?;
18240        for _ in 0..result_values.len() {
18241            pmap_progress.tick();
18242        }
18243        pmap_progress.finish();
18244
18245        if flat_outputs {
18246            let flattened: Vec<PerlValue> = result_values
18247                .into_iter()
18248                .flat_map(|v| v.map_flatten_outputs(true))
18249                .collect();
18250            Ok(PerlValue::array(flattened))
18251        } else {
18252            Ok(PerlValue::array(result_values))
18253        }
18254    }
18255
18256    /// `par_lines PATH, sub { } [, progress => EXPR]` — mmap + parallel line iteration (also used by VM).
18257    pub(crate) fn eval_par_lines_expr(
18258        &mut self,
18259        path: &Expr,
18260        callback: &Expr,
18261        progress: Option<&Expr>,
18262        line: usize,
18263    ) -> Result<PerlValue, FlowOrError> {
18264        let show_progress = progress
18265            .map(|p| self.eval_expr(p))
18266            .transpose()?
18267            .map(|v| v.is_true())
18268            .unwrap_or(false);
18269        let path_s = self.eval_expr(path)?.to_string();
18270        let cb_val = self.eval_expr(callback)?;
18271        let sub = if let Some(s) = cb_val.as_code_ref() {
18272            s
18273        } else {
18274            return Err(PerlError::runtime(
18275                "par_lines: second argument must be a code reference",
18276                line,
18277            )
18278            .into());
18279        };
18280        let subs = self.subs.clone();
18281        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18282        let file = std::fs::File::open(std::path::Path::new(&path_s)).map_err(|e| {
18283            FlowOrError::Error(PerlError::runtime(format!("par_lines: {}", e), line))
18284        })?;
18285        let mmap = unsafe {
18286            memmap2::Mmap::map(&file).map_err(|e| {
18287                FlowOrError::Error(PerlError::runtime(format!("par_lines: mmap: {}", e), line))
18288            })?
18289        };
18290        let data: &[u8] = &mmap;
18291        if data.is_empty() {
18292            return Ok(PerlValue::UNDEF);
18293        }
18294        let line_total = crate::par_lines::line_count_bytes(data);
18295        let pmap_progress = PmapProgress::new(show_progress, line_total);
18296        if self.num_threads == 0 {
18297            self.num_threads = rayon::current_num_threads();
18298        }
18299        let num_chunks = self.num_threads.saturating_mul(8).max(1);
18300        let chunks = crate::par_lines::line_aligned_chunks(data, num_chunks);
18301        chunks.into_par_iter().try_for_each(|(start, end)| {
18302            let slice = &data[start..end];
18303            let mut s = 0usize;
18304            while s < slice.len() {
18305                let e = slice[s..]
18306                    .iter()
18307                    .position(|&b| b == b'\n')
18308                    .map(|p| s + p)
18309                    .unwrap_or(slice.len());
18310                let line_bytes = &slice[s..e];
18311                let line_str = crate::par_lines::line_to_perl_string(line_bytes);
18312                let mut local_interp = Interpreter::new();
18313                local_interp.subs = subs.clone();
18314                local_interp.scope.restore_capture(&scope_capture);
18315                local_interp
18316                    .scope
18317                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18318                local_interp.enable_parallel_guard();
18319                local_interp.scope.set_topic(PerlValue::string(line_str));
18320                match local_interp.call_sub(&sub, vec![], WantarrayCtx::Void, line) {
18321                    Ok(_) => {}
18322                    Err(e) => return Err(e),
18323                }
18324                pmap_progress.tick();
18325                if e >= slice.len() {
18326                    break;
18327                }
18328                s = e + 1;
18329            }
18330            Ok(())
18331        })?;
18332        pmap_progress.finish();
18333        Ok(PerlValue::UNDEF)
18334    }
18335
18336    /// `par_walk PATH, sub { } [, progress => EXPR]` — parallel recursive directory walk (also used by VM).
18337    pub(crate) fn eval_par_walk_expr(
18338        &mut self,
18339        path: &Expr,
18340        callback: &Expr,
18341        progress: Option<&Expr>,
18342        line: usize,
18343    ) -> Result<PerlValue, FlowOrError> {
18344        let show_progress = progress
18345            .map(|p| self.eval_expr(p))
18346            .transpose()?
18347            .map(|v| v.is_true())
18348            .unwrap_or(false);
18349        let path_val = self.eval_expr(path)?;
18350        let roots: Vec<PathBuf> = if let Some(arr) = path_val.as_array_vec() {
18351            arr.into_iter()
18352                .map(|v| PathBuf::from(v.to_string()))
18353                .collect()
18354        } else {
18355            vec![PathBuf::from(path_val.to_string())]
18356        };
18357        let cb_val = self.eval_expr(callback)?;
18358        let sub = if let Some(s) = cb_val.as_code_ref() {
18359            s
18360        } else {
18361            return Err(PerlError::runtime(
18362                "par_walk: second argument must be a code reference",
18363                line,
18364            )
18365            .into());
18366        };
18367        let subs = self.subs.clone();
18368        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18369
18370        if show_progress {
18371            let paths = crate::par_walk::collect_paths(&roots);
18372            let pmap_progress = PmapProgress::new(true, paths.len());
18373            paths.into_par_iter().try_for_each(|p| {
18374                let s = p.to_string_lossy().into_owned();
18375                let mut local_interp = Interpreter::new();
18376                local_interp.subs = subs.clone();
18377                local_interp.scope.restore_capture(&scope_capture);
18378                local_interp
18379                    .scope
18380                    .restore_atomics(&atomic_arrays, &atomic_hashes);
18381                local_interp.enable_parallel_guard();
18382                local_interp.scope.set_topic(PerlValue::string(s));
18383                match local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line) {
18384                    Ok(_) => {}
18385                    Err(e) => return Err(e),
18386                }
18387                pmap_progress.tick();
18388                Ok(())
18389            })?;
18390            pmap_progress.finish();
18391        } else {
18392            for r in &roots {
18393                par_walk_recursive(
18394                    r.as_path(),
18395                    &sub,
18396                    &subs,
18397                    &scope_capture,
18398                    &atomic_arrays,
18399                    &atomic_hashes,
18400                    line,
18401                )?;
18402            }
18403        }
18404        Ok(PerlValue::UNDEF)
18405    }
18406
18407    /// `par_sed(PATTERN, REPLACEMENT, FILES...)` — parallel in-place regex substitution per file (`g` semantics).
18408    pub(crate) fn builtin_par_sed(
18409        &mut self,
18410        args: &[PerlValue],
18411        line: usize,
18412        has_progress: bool,
18413    ) -> PerlResult<PerlValue> {
18414        let show_progress = if has_progress {
18415            args.last().map(|v| v.is_true()).unwrap_or(false)
18416        } else {
18417            false
18418        };
18419        let slice = if has_progress {
18420            &args[..args.len().saturating_sub(1)]
18421        } else {
18422            args
18423        };
18424        if slice.len() < 3 {
18425            return Err(PerlError::runtime(
18426                "par_sed: need pattern, replacement, and at least one file path",
18427                line,
18428            ));
18429        }
18430        let pat_val = &slice[0];
18431        let repl = slice[1].to_string();
18432        let files: Vec<String> = slice[2..].iter().map(|v| v.to_string()).collect();
18433
18434        let re = if let Some(rx) = pat_val.as_regex() {
18435            rx
18436        } else {
18437            let pattern = pat_val.to_string();
18438            match self.compile_regex(&pattern, "g", line) {
18439                Ok(r) => r,
18440                Err(FlowOrError::Error(e)) => return Err(e),
18441                Err(FlowOrError::Flow(f)) => {
18442                    return Err(PerlError::runtime(format!("par_sed: {:?}", f), line))
18443                }
18444            }
18445        };
18446
18447        let pmap = PmapProgress::new(show_progress, files.len());
18448        let touched = AtomicUsize::new(0);
18449        files.par_iter().try_for_each(|path| {
18450            let content = read_file_text_perl_compat(path)
18451                .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18452            let new_s = re.replace_all(&content, &repl);
18453            if new_s != content {
18454                std::fs::write(path, new_s.as_bytes())
18455                    .map_err(|e| PerlError::runtime(format!("par_sed {}: {}", path, e), line))?;
18456                touched.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
18457            }
18458            pmap.tick();
18459            Ok(())
18460        })?;
18461        pmap.finish();
18462        Ok(PerlValue::integer(
18463            touched.load(std::sync::atomic::Ordering::Relaxed) as i64,
18464        ))
18465    }
18466
18467    /// `pwatch GLOB, sub { }` — filesystem notify loop (also used by VM).
18468    pub(crate) fn eval_pwatch_expr(
18469        &mut self,
18470        path: &Expr,
18471        callback: &Expr,
18472        line: usize,
18473    ) -> Result<PerlValue, FlowOrError> {
18474        let pattern_s = self.eval_expr(path)?.to_string();
18475        let cb_val = self.eval_expr(callback)?;
18476        let sub = if let Some(s) = cb_val.as_code_ref() {
18477            s
18478        } else {
18479            return Err(PerlError::runtime(
18480                "pwatch: second argument must be a code reference",
18481                line,
18482            )
18483            .into());
18484        };
18485        let subs = self.subs.clone();
18486        let (scope_capture, atomic_arrays, atomic_hashes) = self.scope.capture_with_atomics();
18487        crate::pwatch::run_pwatch(
18488            &pattern_s,
18489            sub,
18490            subs,
18491            scope_capture,
18492            atomic_arrays,
18493            atomic_hashes,
18494            line,
18495        )
18496        .map_err(FlowOrError::Error)
18497    }
18498
18499    /// Interpolate `$var` in s/// replacement strings, preserving numeric backrefs ($1, $2, etc.).
18500    fn interpolate_replacement_string(&self, replacement: &str) -> String {
18501        let mut out = String::with_capacity(replacement.len());
18502        let chars: Vec<char> = replacement.chars().collect();
18503        let mut i = 0;
18504        while i < chars.len() {
18505            if chars[i] == '\\' && i + 1 < chars.len() {
18506                out.push(chars[i]);
18507                out.push(chars[i + 1]);
18508                i += 2;
18509                continue;
18510            }
18511            if chars[i] == '$' && i + 1 < chars.len() {
18512                let start = i;
18513                i += 1;
18514                if chars[i].is_ascii_digit() {
18515                    out.push('$');
18516                    while i < chars.len() && chars[i].is_ascii_digit() {
18517                        out.push(chars[i]);
18518                        i += 1;
18519                    }
18520                    continue;
18521                }
18522                if chars[i] == '&' || chars[i] == '`' || chars[i] == '\'' {
18523                    out.push('$');
18524                    out.push(chars[i]);
18525                    i += 1;
18526                    continue;
18527                }
18528                if !chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{' {
18529                    out.push('$');
18530                    continue;
18531                }
18532                let mut name = String::new();
18533                if chars[i] == '{' {
18534                    i += 1;
18535                    while i < chars.len() && chars[i] != '}' {
18536                        name.push(chars[i]);
18537                        i += 1;
18538                    }
18539                    if i < chars.len() {
18540                        i += 1;
18541                    }
18542                } else {
18543                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18544                        name.push(chars[i]);
18545                        i += 1;
18546                    }
18547                }
18548                if !name.is_empty() && !name.chars().all(|c| c.is_ascii_digit()) {
18549                    let val = self.scope.get_scalar(&name);
18550                    out.push_str(&val.to_string());
18551                } else if !name.is_empty() {
18552                    out.push_str(&replacement[start..i]);
18553                } else {
18554                    out.push('$');
18555                }
18556                continue;
18557            }
18558            out.push(chars[i]);
18559            i += 1;
18560        }
18561        out
18562    }
18563
18564    /// Interpolate `$var` / `@var` in regex patterns (Perl double-quote-like interpolation).
18565    fn interpolate_regex_pattern(&self, pattern: &str) -> String {
18566        let mut out = String::with_capacity(pattern.len());
18567        let chars: Vec<char> = pattern.chars().collect();
18568        let mut i = 0;
18569        while i < chars.len() {
18570            if chars[i] == '\\' && i + 1 < chars.len() {
18571                // Preserve escape sequences (including \$ which is literal $)
18572                out.push(chars[i]);
18573                out.push(chars[i + 1]);
18574                i += 2;
18575                continue;
18576            }
18577            if chars[i] == '$' && i + 1 < chars.len() {
18578                i += 1;
18579                // `$` at end of pattern is an anchor, not a variable
18580                if i >= chars.len()
18581                    || (!chars[i].is_alphanumeric() && chars[i] != '_' && chars[i] != '{')
18582                {
18583                    out.push('$');
18584                    continue;
18585                }
18586                let mut name = String::new();
18587                if chars[i] == '{' {
18588                    i += 1;
18589                    while i < chars.len() && chars[i] != '}' {
18590                        name.push(chars[i]);
18591                        i += 1;
18592                    }
18593                    if i < chars.len() {
18594                        i += 1;
18595                    } // skip }
18596                } else {
18597                    while i < chars.len() && (chars[i].is_alphanumeric() || chars[i] == '_') {
18598                        name.push(chars[i]);
18599                        i += 1;
18600                    }
18601                }
18602                if !name.is_empty() {
18603                    let val = self.scope.get_scalar(&name);
18604                    out.push_str(&val.to_string());
18605                } else {
18606                    out.push('$');
18607                }
18608                continue;
18609            }
18610            out.push(chars[i]);
18611            i += 1;
18612        }
18613        out
18614    }
18615
18616    pub(crate) fn compile_regex(
18617        &mut self,
18618        pattern: &str,
18619        flags: &str,
18620        line: usize,
18621    ) -> Result<Arc<PerlCompiledRegex>, FlowOrError> {
18622        // Interpolate variables in the pattern: `$var`, `${var}`, `@var`
18623        let pattern = if pattern.contains('$') || pattern.contains('@') {
18624            std::borrow::Cow::Owned(self.interpolate_regex_pattern(pattern))
18625        } else {
18626            std::borrow::Cow::Borrowed(pattern)
18627        };
18628        let pattern = pattern.as_ref();
18629        // Fast path: same regex as last call (common in loops).
18630        // Arc clone is cheap (ref-count increment) AND preserves the lazy DFA cache.
18631        let multiline = self.multiline_match;
18632        if let Some((ref lp, ref lf, ref lm, ref lr)) = self.regex_last {
18633            if lp == pattern && lf == flags && *lm == multiline {
18634                return Ok(lr.clone());
18635            }
18636        }
18637        // Slow path: HashMap lookup
18638        let key = format!("{}\x00{}\x00{}", multiline as u8, flags, pattern);
18639        if let Some(cached) = self.regex_cache.get(&key) {
18640            self.regex_last = Some((
18641                pattern.to_string(),
18642                flags.to_string(),
18643                multiline,
18644                cached.clone(),
18645            ));
18646            return Ok(cached.clone());
18647        }
18648        let expanded = expand_perl_regex_quotemeta(pattern);
18649        let expanded = expand_perl_regex_octal_escapes(&expanded);
18650        let expanded = rewrite_perl_regex_dollar_end_anchor(&expanded, flags.contains('m'));
18651        let mut re_str = String::new();
18652        if flags.contains('i') {
18653            re_str.push_str("(?i)");
18654        }
18655        if flags.contains('s') {
18656            re_str.push_str("(?s)");
18657        }
18658        if flags.contains('m') {
18659            re_str.push_str("(?m)");
18660        }
18661        if flags.contains('x') {
18662            re_str.push_str("(?x)");
18663        }
18664        // Deprecated `$*` multiline: dot matches newline (same intent as `(?s)`).
18665        if multiline {
18666            re_str.push_str("(?s)");
18667        }
18668        re_str.push_str(&expanded);
18669        let re = PerlCompiledRegex::compile(&re_str).map_err(|e| {
18670            FlowOrError::Error(PerlError::runtime(
18671                format!("Invalid regex /{}/: {}", pattern, e),
18672                line,
18673            ))
18674        })?;
18675        let arc = re;
18676        self.regex_last = Some((
18677            pattern.to_string(),
18678            flags.to_string(),
18679            multiline,
18680            arc.clone(),
18681        ));
18682        self.regex_cache.insert(key, arc.clone());
18683        Ok(arc)
18684    }
18685
18686    /// `(bracket, line)` for Perl's `die` / `warn` suffix `, <bracket> line N.` (`bracket` is `<>`, `<STDIN>`, `<FH>`, …).
18687    pub(crate) fn die_warn_io_annotation(&self) -> Option<(String, i64)> {
18688        if self.last_readline_handle.is_empty() {
18689            return (self.line_number > 0).then_some(("<>".to_string(), self.line_number));
18690        }
18691        let n = *self
18692            .handle_line_numbers
18693            .get(&self.last_readline_handle)
18694            .unwrap_or(&0);
18695        if n <= 0 {
18696            return None;
18697        }
18698        if !self.argv_current_file.is_empty() && self.last_readline_handle == self.argv_current_file
18699        {
18700            return Some(("<>".to_string(), n));
18701        }
18702        if self.last_readline_handle == "STDIN" {
18703            return Some((self.last_stdin_die_bracket.clone(), n));
18704        }
18705        Some((format!("<{}>", self.last_readline_handle), n))
18706    }
18707
18708    /// Trailing ` at FILE line N` plus optional `, <> line $.` for `die` / `warn` (matches Perl 5).
18709    pub(crate) fn die_warn_at_suffix(&self, source_line: usize) -> String {
18710        let mut s = format!(" at {} line {}", self.file, source_line);
18711        if let Some((bracket, n)) = self.die_warn_io_annotation() {
18712            s.push_str(&format!(", {} line {}.", bracket, n));
18713        } else {
18714            s.push('.');
18715        }
18716        s
18717    }
18718
18719    /// Process a line in -n/-p mode.
18720    ///
18721    /// `is_last_input_line` is true when this line is the last from the current stdin or `@ARGV`
18722    /// file so `eof` with no arguments matches Perl behavior on that line.
18723    pub fn process_line(
18724        &mut self,
18725        line_str: &str,
18726        program: &Program,
18727        is_last_input_line: bool,
18728    ) -> PerlResult<Option<String>> {
18729        self.line_mode_eof_pending = is_last_input_line;
18730        let result: PerlResult<Option<String>> = (|| {
18731            self.line_number += 1;
18732            self.scope
18733                .set_topic(PerlValue::string(line_str.to_string()));
18734
18735            if self.auto_split {
18736                let sep = self.field_separator.as_deref().unwrap_or(" ");
18737                let re = regex::Regex::new(sep).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
18738                let fields: Vec<PerlValue> = re
18739                    .split(line_str)
18740                    .map(|s| PerlValue::string(s.to_string()))
18741                    .collect();
18742                self.scope.set_array("F", fields)?;
18743            }
18744
18745            for stmt in &program.statements {
18746                match &stmt.kind {
18747                    StmtKind::SubDecl { .. }
18748                    | StmtKind::Begin(_)
18749                    | StmtKind::UnitCheck(_)
18750                    | StmtKind::Check(_)
18751                    | StmtKind::Init(_)
18752                    | StmtKind::End(_) => continue,
18753                    _ => match self.exec_statement(stmt) {
18754                        Ok(_) => {}
18755                        Err(FlowOrError::Error(e)) => return Err(e),
18756                        Err(FlowOrError::Flow(_)) => {}
18757                    },
18758                }
18759            }
18760
18761            // `-p` implicit print matches `print $_` (appends `$\` / [`Self::ors`] — set by `-l`).
18762            let mut out = self.scope.get_scalar("_").to_string();
18763            out.push_str(&self.ors);
18764            Ok(Some(out))
18765        })();
18766        self.line_mode_eof_pending = false;
18767        result
18768    }
18769}
18770
18771fn par_walk_invoke_entry(
18772    path: &Path,
18773    sub: &Arc<PerlSub>,
18774    subs: &HashMap<String, Arc<PerlSub>>,
18775    scope_capture: &[(String, PerlValue)],
18776    atomic_arrays: &[(String, crate::scope::AtomicArray)],
18777    atomic_hashes: &[(String, crate::scope::AtomicHash)],
18778    line: usize,
18779) -> Result<(), FlowOrError> {
18780    let s = path.to_string_lossy().into_owned();
18781    let mut local_interp = Interpreter::new();
18782    local_interp.subs = subs.clone();
18783    local_interp.scope.restore_capture(scope_capture);
18784    local_interp
18785        .scope
18786        .restore_atomics(atomic_arrays, atomic_hashes);
18787    local_interp.enable_parallel_guard();
18788    local_interp.scope.set_topic(PerlValue::string(s));
18789    local_interp.call_sub(sub.as_ref(), vec![], WantarrayCtx::Void, line)?;
18790    Ok(())
18791}
18792
18793fn par_walk_recursive(
18794    path: &Path,
18795    sub: &Arc<PerlSub>,
18796    subs: &HashMap<String, Arc<PerlSub>>,
18797    scope_capture: &[(String, PerlValue)],
18798    atomic_arrays: &[(String, crate::scope::AtomicArray)],
18799    atomic_hashes: &[(String, crate::scope::AtomicHash)],
18800    line: usize,
18801) -> Result<(), FlowOrError> {
18802    if path.is_file() || (path.is_symlink() && !path.is_dir()) {
18803        return par_walk_invoke_entry(
18804            path,
18805            sub,
18806            subs,
18807            scope_capture,
18808            atomic_arrays,
18809            atomic_hashes,
18810            line,
18811        );
18812    }
18813    if !path.is_dir() {
18814        return Ok(());
18815    }
18816    par_walk_invoke_entry(
18817        path,
18818        sub,
18819        subs,
18820        scope_capture,
18821        atomic_arrays,
18822        atomic_hashes,
18823        line,
18824    )?;
18825    let read = match std::fs::read_dir(path) {
18826        Ok(r) => r,
18827        Err(_) => return Ok(()),
18828    };
18829    let entries: Vec<_> = read.filter_map(|e| e.ok()).collect();
18830    entries.par_iter().try_for_each(|e| {
18831        par_walk_recursive(
18832            &e.path(),
18833            sub,
18834            subs,
18835            scope_capture,
18836            atomic_arrays,
18837            atomic_hashes,
18838            line,
18839        )
18840    })?;
18841    Ok(())
18842}
18843
18844/// `sprintf` with pluggable `%s` formatting (stringify for overload-aware `Interpreter`).
18845pub(crate) fn perl_sprintf_format_with<F>(
18846    fmt: &str,
18847    args: &[PerlValue],
18848    mut string_for_s: F,
18849) -> Result<String, FlowOrError>
18850where
18851    F: FnMut(&PerlValue) -> Result<String, FlowOrError>,
18852{
18853    let mut result = String::new();
18854    let mut arg_idx = 0;
18855    let chars: Vec<char> = fmt.chars().collect();
18856    let mut i = 0;
18857
18858    while i < chars.len() {
18859        if chars[i] == '%' {
18860            i += 1;
18861            if i >= chars.len() {
18862                break;
18863            }
18864            if chars[i] == '%' {
18865                result.push('%');
18866                i += 1;
18867                continue;
18868            }
18869
18870            // Parse format specifier
18871            let mut flags = String::new();
18872            while i < chars.len() && "-+ #0".contains(chars[i]) {
18873                flags.push(chars[i]);
18874                i += 1;
18875            }
18876            let mut width = String::new();
18877            while i < chars.len() && chars[i].is_ascii_digit() {
18878                width.push(chars[i]);
18879                i += 1;
18880            }
18881            let mut precision = String::new();
18882            if i < chars.len() && chars[i] == '.' {
18883                i += 1;
18884                while i < chars.len() && chars[i].is_ascii_digit() {
18885                    precision.push(chars[i]);
18886                    i += 1;
18887                }
18888            }
18889            if i >= chars.len() {
18890                break;
18891            }
18892            let spec = chars[i];
18893            i += 1;
18894
18895            let arg = args.get(arg_idx).cloned().unwrap_or(PerlValue::UNDEF);
18896            arg_idx += 1;
18897
18898            let w: usize = width.parse().unwrap_or(0);
18899            let p: usize = precision.parse().unwrap_or(6);
18900
18901            let zero_pad = flags.contains('0') && !flags.contains('-');
18902            let left_align = flags.contains('-');
18903            let formatted = match spec {
18904                'd' | 'i' => {
18905                    if zero_pad {
18906                        format!("{:0width$}", arg.to_int(), width = w)
18907                    } else if left_align {
18908                        format!("{:<width$}", arg.to_int(), width = w)
18909                    } else {
18910                        format!("{:width$}", arg.to_int(), width = w)
18911                    }
18912                }
18913                'u' => {
18914                    if zero_pad {
18915                        format!("{:0width$}", arg.to_int() as u64, width = w)
18916                    } else {
18917                        format!("{:width$}", arg.to_int() as u64, width = w)
18918                    }
18919                }
18920                'f' => format!("{:width$.prec$}", arg.to_number(), width = w, prec = p),
18921                'e' => format!("{:width$.prec$e}", arg.to_number(), width = w, prec = p),
18922                'g' => {
18923                    let n = arg.to_number();
18924                    if n.abs() >= 1e-4 && n.abs() < 1e15 {
18925                        format!("{:width$.prec$}", n, width = w, prec = p)
18926                    } else {
18927                        format!("{:width$.prec$e}", n, width = w, prec = p)
18928                    }
18929                }
18930                's' => {
18931                    let s = string_for_s(&arg)?;
18932                    if !precision.is_empty() {
18933                        let truncated: String = s.chars().take(p).collect();
18934                        if flags.contains('-') {
18935                            format!("{:<width$}", truncated, width = w)
18936                        } else {
18937                            format!("{:>width$}", truncated, width = w)
18938                        }
18939                    } else if flags.contains('-') {
18940                        format!("{:<width$}", s, width = w)
18941                    } else {
18942                        format!("{:>width$}", s, width = w)
18943                    }
18944                }
18945                'x' => {
18946                    let v = arg.to_int();
18947                    if zero_pad && w > 0 {
18948                        format!("{:0width$x}", v, width = w)
18949                    } else if left_align {
18950                        format!("{:<width$x}", v, width = w)
18951                    } else if w > 0 {
18952                        format!("{:width$x}", v, width = w)
18953                    } else {
18954                        format!("{:x}", v)
18955                    }
18956                }
18957                'X' => {
18958                    let v = arg.to_int();
18959                    if zero_pad && w > 0 {
18960                        format!("{:0width$X}", v, width = w)
18961                    } else if left_align {
18962                        format!("{:<width$X}", v, width = w)
18963                    } else if w > 0 {
18964                        format!("{:width$X}", v, width = w)
18965                    } else {
18966                        format!("{:X}", v)
18967                    }
18968                }
18969                'o' => {
18970                    let v = arg.to_int();
18971                    if zero_pad && w > 0 {
18972                        format!("{:0width$o}", v, width = w)
18973                    } else if left_align {
18974                        format!("{:<width$o}", v, width = w)
18975                    } else if w > 0 {
18976                        format!("{:width$o}", v, width = w)
18977                    } else {
18978                        format!("{:o}", v)
18979                    }
18980                }
18981                'b' => {
18982                    let v = arg.to_int();
18983                    if zero_pad && w > 0 {
18984                        format!("{:0width$b}", v, width = w)
18985                    } else if left_align {
18986                        format!("{:<width$b}", v, width = w)
18987                    } else if w > 0 {
18988                        format!("{:width$b}", v, width = w)
18989                    } else {
18990                        format!("{:b}", v)
18991                    }
18992                }
18993                'c' => char::from_u32(arg.to_int() as u32)
18994                    .map(|c| c.to_string())
18995                    .unwrap_or_default(),
18996                _ => arg.to_string(),
18997            };
18998
18999            result.push_str(&formatted);
19000        } else {
19001            result.push(chars[i]);
19002            i += 1;
19003        }
19004    }
19005    Ok(result)
19006}
19007
19008#[cfg(test)]
19009mod regex_expand_tests {
19010    use super::Interpreter;
19011
19012    #[test]
19013    fn compile_regex_quotemeta_qe_matches_literal() {
19014        let mut i = Interpreter::new();
19015        let re = i.compile_regex(r"\Qa.c\E", "", 1).expect("regex");
19016        assert!(re.is_match("a.c"));
19017        assert!(!re.is_match("abc"));
19018    }
19019
19020    /// `]` may be the first character in a Perl class when a later `]` closes it; `$` inside must
19021    /// stay literal (not rewritten to `(?:\n?\z)`).
19022    #[test]
19023    fn compile_regex_char_class_leading_close_bracket_is_literal() {
19024        let mut i = Interpreter::new();
19025        let re = i.compile_regex(r"[]\[^$.*/]", "", 1).expect("regex");
19026        assert!(re.is_match("$"));
19027        assert!(re.is_match("]"));
19028        assert!(!re.is_match("x"));
19029    }
19030}
19031
19032#[cfg(test)]
19033mod special_scalar_name_tests {
19034    use super::Interpreter;
19035
19036    #[test]
19037    fn special_scalar_name_for_get_matches_magic_globals() {
19038        assert!(Interpreter::is_special_scalar_name_for_get("0"));
19039        assert!(Interpreter::is_special_scalar_name_for_get("!"));
19040        assert!(Interpreter::is_special_scalar_name_for_get("^W"));
19041        assert!(Interpreter::is_special_scalar_name_for_get("^O"));
19042        assert!(Interpreter::is_special_scalar_name_for_get("^MATCH"));
19043        assert!(Interpreter::is_special_scalar_name_for_get("<"));
19044        assert!(Interpreter::is_special_scalar_name_for_get("?"));
19045        assert!(Interpreter::is_special_scalar_name_for_get("|"));
19046        assert!(Interpreter::is_special_scalar_name_for_get("^UNICODE"));
19047        assert!(Interpreter::is_special_scalar_name_for_get("\""));
19048        assert!(!Interpreter::is_special_scalar_name_for_get("foo"));
19049        assert!(!Interpreter::is_special_scalar_name_for_get("plainvar"));
19050    }
19051
19052    #[test]
19053    fn special_scalar_name_for_set_matches_set_special_var_arms() {
19054        assert!(Interpreter::is_special_scalar_name_for_set("0"));
19055        assert!(Interpreter::is_special_scalar_name_for_set("^D"));
19056        assert!(Interpreter::is_special_scalar_name_for_set("^H"));
19057        assert!(Interpreter::is_special_scalar_name_for_set("^WARNING_BITS"));
19058        assert!(Interpreter::is_special_scalar_name_for_set("ARGV"));
19059        assert!(Interpreter::is_special_scalar_name_for_set("|"));
19060        assert!(Interpreter::is_special_scalar_name_for_set("?"));
19061        assert!(Interpreter::is_special_scalar_name_for_set("^UNICODE"));
19062        assert!(Interpreter::is_special_scalar_name_for_set("."));
19063        assert!(!Interpreter::is_special_scalar_name_for_set("foo"));
19064        assert!(!Interpreter::is_special_scalar_name_for_set("__PACKAGE__"));
19065    }
19066
19067    #[test]
19068    fn caret_and_id_specials_roundtrip_get() {
19069        let i = Interpreter::new();
19070        assert_eq!(i.get_special_var("^O").to_string(), super::perl_osname());
19071        assert_eq!(
19072            i.get_special_var("^V").to_string(),
19073            format!("v{}", env!("CARGO_PKG_VERSION"))
19074        );
19075        assert_eq!(i.get_special_var("^GLOBAL_PHASE").to_string(), "RUN");
19076        assert!(i.get_special_var("^T").to_int() >= 0);
19077        #[cfg(unix)]
19078        {
19079            assert!(i.get_special_var("<").to_int() >= 0);
19080        }
19081    }
19082
19083    #[test]
19084    fn scalar_flip_flop_three_dot_same_dollar_dot_second_eval_stays_active() {
19085        let mut i = Interpreter::new();
19086        i.last_readline_handle.clear();
19087        i.line_number = 3;
19088        i.prepare_flip_flop_vm_slots(1);
19089        assert_eq!(
19090            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
19091            1
19092        );
19093        assert!(i.flip_flop_active[0]);
19094        assert_eq!(i.flip_flop_exclusive_left_line[0], Some(3));
19095        // Second evaluation on the same `$.` must not clear the range (Perl `...` defers the right test).
19096        assert_eq!(
19097            i.scalar_flip_flop_eval(3, 3, 0, true).expect("ok").to_int(),
19098            1
19099        );
19100        assert!(i.flip_flop_active[0]);
19101    }
19102
19103    #[test]
19104    fn scalar_flip_flop_three_dot_deactivates_when_past_left_line_and_dot_matches_right() {
19105        let mut i = Interpreter::new();
19106        i.last_readline_handle.clear();
19107        i.line_number = 2;
19108        i.prepare_flip_flop_vm_slots(1);
19109        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
19110        assert!(i.flip_flop_active[0]);
19111        i.line_number = 3;
19112        i.scalar_flip_flop_eval(2, 3, 0, true).expect("ok");
19113        assert!(!i.flip_flop_active[0]);
19114        assert_eq!(i.flip_flop_exclusive_left_line[0], None);
19115    }
19116}