Skip to main content

stryke/
scope.rs

1use std::collections::{HashMap, HashSet};
2use std::sync::Arc;
3
4use indexmap::IndexMap;
5use parking_lot::{Mutex, RwLock};
6
7use crate::ast::PerlTypeName;
8use crate::error::PerlError;
9use crate::value::PerlValue;
10
11/// Thread-safe shared array for `mysync @a`.
12#[derive(Debug, Clone)]
13pub struct AtomicArray(pub Arc<Mutex<Vec<PerlValue>>>);
14
15/// Thread-safe shared hash for `mysync %h`.
16#[derive(Debug, Clone)]
17pub struct AtomicHash(pub Arc<Mutex<IndexMap<String, PerlValue>>>);
18
19type ScopeCaptureWithAtomics = (
20    Vec<(String, PerlValue)>,
21    Vec<(String, AtomicArray)>,
22    Vec<(String, AtomicHash)>,
23);
24
25/// Arrays installed by [`crate::interpreter::Interpreter::new`] on the outer frame. They must not be
26/// copied into [`Scope::capture`] / [`Scope::restore_capture`] for closures, or the restored copy
27/// would shadow the live handles (stale `@INC`, `%ENV`, topic `@_`, etc.).
28#[inline]
29fn capture_skip_bootstrap_array(name: &str) -> bool {
30    matches!(
31        name,
32        "INC" | "ARGV" | "_" | "-" | "+" | "^CAPTURE" | "^CAPTURE_ALL"
33    )
34}
35
36/// Hashes installed at interpreter bootstrap (same rationale as [`capture_skip_bootstrap_array`]).
37#[inline]
38fn capture_skip_bootstrap_hash(name: &str) -> bool {
39    matches!(name, "INC" | "ENV" | "SIG" | "^HOOK")
40}
41
42/// Saved bindings for `local $x` / `local @a` / `local %h` — restored on [`Scope::pop_frame`].
43#[derive(Clone, Debug)]
44enum LocalRestore {
45    Scalar(String, PerlValue),
46    Array(String, Vec<PerlValue>),
47    Hash(String, IndexMap<String, PerlValue>),
48    /// `local $h{k}` — third is `None` if the key was absent before `local` (restore deletes the key).
49    HashElement(String, String, Option<PerlValue>),
50    /// `local $a[i]` — restore previous slot value (see [`Scope::local_set_array_element`]).
51    ArrayElement(String, i64, PerlValue),
52}
53
54/// A single lexical scope frame.
55/// Uses Vec instead of HashMap — for typical Perl code with < 10 variables per
56/// scope, linear scan is faster than hashing due to cache locality and zero
57/// hash overhead.
58#[derive(Debug, Clone)]
59struct Frame {
60    scalars: Vec<(String, PerlValue)>,
61    arrays: Vec<(String, Vec<PerlValue>)>,
62    /// Subroutine (or bootstrap) `@_` — stored separately so call paths can move the arg
63    /// [`Vec`] into the frame without an extra copy via [`Frame::arrays`].
64    sub_underscore: Option<Vec<PerlValue>>,
65    hashes: Vec<(String, IndexMap<String, PerlValue>)>,
66    /// Slot-indexed scalars for O(1) access from compiled subroutines.
67    /// Compiler assigns `my $x` declarations a u8 slot index; the VM accesses
68    /// `scalar_slots[idx]` directly without name lookup or frame walking.
69    scalar_slots: Vec<PerlValue>,
70    /// Bare scalar name for each slot (same index as `scalar_slots`) — for [`Scope::capture`]
71    /// / closures when the binding exists only in `scalar_slots`.
72    scalar_slot_names: Vec<Option<String>>,
73    /// Dynamic `local` saves — applied in reverse when this frame is popped.
74    local_restores: Vec<LocalRestore>,
75    /// Lexical names from `frozen my $x` / `@a` / `%h` (bare name, same as storage key).
76    frozen_scalars: HashSet<String>,
77    frozen_arrays: HashSet<String>,
78    frozen_hashes: HashSet<String>,
79    /// `typed my $x : Int` — runtime type checks on assignment.
80    typed_scalars: HashMap<String, PerlTypeName>,
81    /// Thread-safe arrays from `mysync @a`
82    atomic_arrays: Vec<(String, AtomicArray)>,
83    /// Thread-safe hashes from `mysync %h`
84    atomic_hashes: Vec<(String, AtomicHash)>,
85    /// `defer { BLOCK }` closures to run when this frame is popped (LIFO order).
86    defers: Vec<PerlValue>,
87}
88
89impl Frame {
90    /// Drop all lexical bindings so blessed objects run `DESTROY` when frames are recycled
91    /// ([`Scope::pop_frame`]) or reused ([`Scope::push_frame`]).
92    #[inline]
93    fn clear_all_bindings(&mut self) {
94        self.scalars.clear();
95        self.arrays.clear();
96        self.sub_underscore = None;
97        self.hashes.clear();
98        self.scalar_slots.clear();
99        self.scalar_slot_names.clear();
100        self.local_restores.clear();
101        self.frozen_scalars.clear();
102        self.frozen_arrays.clear();
103        self.frozen_hashes.clear();
104        self.typed_scalars.clear();
105        self.atomic_arrays.clear();
106        self.defers.clear();
107        self.atomic_hashes.clear();
108    }
109
110    /// True if this slot index is a real binding (not vec padding before a higher-index declare).
111    /// Anonymous temps use [`Option::Some`] with an empty string so slot ops do not fall through
112    /// to an outer frame's same slot index.
113    #[inline]
114    fn owns_scalar_slot_index(&self, idx: usize) -> bool {
115        self.scalar_slot_names.get(idx).is_some_and(|n| n.is_some())
116    }
117
118    #[inline]
119    fn new() -> Self {
120        Self {
121            scalars: Vec::new(),
122            arrays: Vec::new(),
123            sub_underscore: None,
124            hashes: Vec::new(),
125            scalar_slots: Vec::new(),
126            scalar_slot_names: Vec::new(),
127            frozen_scalars: HashSet::new(),
128            frozen_arrays: HashSet::new(),
129            frozen_hashes: HashSet::new(),
130            typed_scalars: HashMap::new(),
131            atomic_arrays: Vec::new(),
132            atomic_hashes: Vec::new(),
133            local_restores: Vec::new(),
134            defers: Vec::new(),
135        }
136    }
137
138    #[inline]
139    fn get_scalar(&self, name: &str) -> Option<&PerlValue> {
140        if let Some(v) = self.get_scalar_from_slot(name) {
141            return Some(v);
142        }
143        self.scalars.iter().find(|(k, _)| k == name).map(|(_, v)| v)
144    }
145
146    /// O(N) scan over slot names — only used by `get_scalar` fallback (name-based lookup);
147    /// hot compiled paths use `get_scalar_slot(idx)` directly.
148    #[inline]
149    fn get_scalar_from_slot(&self, name: &str) -> Option<&PerlValue> {
150        for (i, sn) in self.scalar_slot_names.iter().enumerate() {
151            if let Some(ref n) = sn {
152                if n == name {
153                    return self.scalar_slots.get(i);
154                }
155            }
156        }
157        None
158    }
159
160    #[inline]
161    fn has_scalar(&self, name: &str) -> bool {
162        if self
163            .scalar_slot_names
164            .iter()
165            .any(|sn| sn.as_deref() == Some(name))
166        {
167            return true;
168        }
169        self.scalars.iter().any(|(k, _)| k == name)
170    }
171
172    #[inline]
173    fn set_scalar(&mut self, name: &str, val: PerlValue) {
174        for (i, sn) in self.scalar_slot_names.iter().enumerate() {
175            if let Some(ref n) = sn {
176                if n == name {
177                    if i < self.scalar_slots.len() {
178                        self.scalar_slots[i] = val;
179                    }
180                    return;
181                }
182            }
183        }
184        if let Some(entry) = self.scalars.iter_mut().find(|(k, _)| k == name) {
185            entry.1 = val;
186        } else {
187            self.scalars.push((name.to_string(), val));
188        }
189    }
190
191    #[inline]
192    fn get_array(&self, name: &str) -> Option<&Vec<PerlValue>> {
193        if name == "_" {
194            if let Some(ref v) = self.sub_underscore {
195                return Some(v);
196            }
197        }
198        self.arrays.iter().find(|(k, _)| k == name).map(|(_, v)| v)
199    }
200
201    #[inline]
202    fn has_array(&self, name: &str) -> bool {
203        if name == "_" && self.sub_underscore.is_some() {
204            return true;
205        }
206        self.arrays.iter().any(|(k, _)| k == name)
207    }
208
209    #[inline]
210    fn get_array_mut(&mut self, name: &str) -> Option<&mut Vec<PerlValue>> {
211        if name == "_" {
212            return self.sub_underscore.as_mut();
213        }
214        self.arrays
215            .iter_mut()
216            .find(|(k, _)| k == name)
217            .map(|(_, v)| v)
218    }
219
220    #[inline]
221    fn set_array(&mut self, name: &str, val: Vec<PerlValue>) {
222        if name == "_" {
223            if let Some(pos) = self.arrays.iter().position(|(k, _)| k == name) {
224                self.arrays.swap_remove(pos);
225            }
226            self.sub_underscore = Some(val);
227            return;
228        }
229        if let Some(entry) = self.arrays.iter_mut().find(|(k, _)| k == name) {
230            entry.1 = val;
231        } else {
232            self.arrays.push((name.to_string(), val));
233        }
234    }
235
236    #[inline]
237    fn get_hash(&self, name: &str) -> Option<&IndexMap<String, PerlValue>> {
238        self.hashes.iter().find(|(k, _)| k == name).map(|(_, v)| v)
239    }
240
241    #[inline]
242    fn has_hash(&self, name: &str) -> bool {
243        self.hashes.iter().any(|(k, _)| k == name)
244    }
245
246    #[inline]
247    fn get_hash_mut(&mut self, name: &str) -> Option<&mut IndexMap<String, PerlValue>> {
248        self.hashes
249            .iter_mut()
250            .find(|(k, _)| k == name)
251            .map(|(_, v)| v)
252    }
253
254    #[inline]
255    fn set_hash(&mut self, name: &str, val: IndexMap<String, PerlValue>) {
256        if let Some(entry) = self.hashes.iter_mut().find(|(k, _)| k == name) {
257            entry.1 = val;
258        } else {
259            self.hashes.push((name.to_string(), val));
260        }
261    }
262}
263
264/// Manages lexical scoping with a stack of frames.
265/// Innermost frame is last in the vector.
266#[derive(Debug, Clone)]
267pub struct Scope {
268    frames: Vec<Frame>,
269    /// Recycled frames to avoid allocation on every push_frame/pop_frame cycle.
270    frame_pool: Vec<Frame>,
271    /// When true (rayon worker / parallel block), reject writes to outer captured lexicals unless
272    /// the binding is `mysync` (atomic) or a loop topic (`$_`, `$a`, `$b`). Package names with `::`
273    /// are exempt. Requires at least two frames (captured + block locals); use [`Self::push_frame`]
274    /// before running a block body on a worker.
275    parallel_guard: bool,
276}
277
278impl Default for Scope {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284impl Scope {
285    pub fn new() -> Self {
286        let mut s = Self {
287            frames: Vec::with_capacity(32),
288            frame_pool: Vec::with_capacity(32),
289            parallel_guard: false,
290        };
291        s.frames.push(Frame::new());
292        s
293    }
294
295    /// Enable [`Self::parallel_guard`] for parallel worker interpreters (pmap, fan, …).
296    #[inline]
297    pub fn set_parallel_guard(&mut self, enabled: bool) {
298        self.parallel_guard = enabled;
299    }
300
301    #[inline]
302    pub fn parallel_guard(&self) -> bool {
303        self.parallel_guard
304    }
305
306    #[inline]
307    fn parallel_skip_special_name(name: &str) -> bool {
308        name.contains("::")
309    }
310
311    /// Loop/sort topic scalars that parallel ops assign before each iteration.
312    #[inline]
313    fn parallel_allowed_topic_scalar(name: &str) -> bool {
314        matches!(name, "_" | "a" | "b")
315    }
316
317    /// Regex / runtime scratch arrays live on an outer frame; parallel match still mutates them.
318    #[inline]
319    fn parallel_allowed_internal_array(name: &str) -> bool {
320        matches!(name, "-" | "+" | "^CAPTURE" | "^CAPTURE_ALL")
321    }
322
323    /// `%ENV`, `%INC`, and regex named-capture hashes `"+"` / `"-"` — same outer-frame issue as internal arrays.
324    #[inline]
325    fn parallel_allowed_internal_hash(name: &str) -> bool {
326        matches!(name, "+" | "-" | "ENV" | "INC")
327    }
328
329    fn check_parallel_scalar_write(&self, name: &str) -> Result<(), PerlError> {
330        if !self.parallel_guard || Self::parallel_skip_special_name(name) {
331            return Ok(());
332        }
333        if Self::parallel_allowed_topic_scalar(name) {
334            return Ok(());
335        }
336        if crate::special_vars::is_regex_match_scalar_name(name) {
337            return Ok(());
338        }
339        let inner = self.frames.len().saturating_sub(1);
340        for (i, frame) in self.frames.iter().enumerate().rev() {
341            if frame.has_scalar(name) {
342                if let Some(v) = frame.get_scalar(name) {
343                    if v.as_atomic_arc().is_some() {
344                        return Ok(());
345                    }
346                }
347                if i != inner {
348                    return Err(PerlError::runtime(
349                        format!(
350                            "cannot assign to captured non-mysync variable `${}` in a parallel block",
351                            name
352                        ),
353                        0,
354                    ));
355                }
356                return Ok(());
357            }
358        }
359        Err(PerlError::runtime(
360            format!(
361                "cannot assign to undeclared variable `${}` in a parallel block",
362                name
363            ),
364            0,
365        ))
366    }
367
368    #[inline]
369    pub fn depth(&self) -> usize {
370        self.frames.len()
371    }
372
373    /// Pop frames until we're at `target_depth`. Used by VM ReturnValue
374    /// to cleanly unwind through if/while/for blocks on return.
375    #[inline]
376    pub fn pop_to_depth(&mut self, target_depth: usize) {
377        while self.frames.len() > target_depth && self.frames.len() > 1 {
378            self.pop_frame();
379        }
380    }
381
382    #[inline]
383    pub fn push_frame(&mut self) {
384        if let Some(mut frame) = self.frame_pool.pop() {
385            frame.clear_all_bindings();
386            self.frames.push(frame);
387        } else {
388            self.frames.push(Frame::new());
389        }
390    }
391
392    // ── Frame-local scalar slots (O(1) access for compiled subs) ──
393
394    /// Read scalar from slot — innermost binding for `slot` wins (same index can exist on nested
395    /// frames; padding entries without [`Frame::owns_scalar_slot_index`] do not shadow outers).
396    #[inline]
397    pub fn get_scalar_slot(&self, slot: u8) -> PerlValue {
398        let idx = slot as usize;
399        for frame in self.frames.iter().rev() {
400            if idx < frame.scalar_slots.len() && frame.owns_scalar_slot_index(idx) {
401                return frame.scalar_slots[idx].clone();
402            }
403        }
404        PerlValue::UNDEF
405    }
406
407    /// Write scalar to slot — innermost binding for `slot` wins (see [`Self::get_scalar_slot`]).
408    #[inline]
409    pub fn set_scalar_slot(&mut self, slot: u8, val: PerlValue) {
410        let idx = slot as usize;
411        let len = self.frames.len();
412        for i in (0..len).rev() {
413            if idx < self.frames[i].scalar_slots.len() && self.frames[i].owns_scalar_slot_index(idx)
414            {
415                self.frames[i].scalar_slots[idx] = val;
416                return;
417            }
418        }
419        let top = self.frames.last_mut().unwrap();
420        top.scalar_slots.resize(idx + 1, PerlValue::UNDEF);
421        if idx >= top.scalar_slot_names.len() {
422            top.scalar_slot_names.resize(idx + 1, None);
423        }
424        top.scalar_slot_names[idx] = Some(String::new());
425        top.scalar_slots[idx] = val;
426    }
427
428    /// Like [`set_scalar_slot`] but respects the parallel guard — returns `Err` when assigning
429    /// to a slot that belongs to an outer frame inside a parallel block.  `slot_name` is resolved
430    /// from the bytecode's name table by the caller when available.
431    #[inline]
432    pub fn set_scalar_slot_checked(
433        &mut self,
434        slot: u8,
435        val: PerlValue,
436        slot_name: Option<&str>,
437    ) -> Result<(), PerlError> {
438        if self.parallel_guard {
439            let idx = slot as usize;
440            let len = self.frames.len();
441            let top_has = idx < self.frames[len - 1].scalar_slots.len()
442                && self.frames[len - 1].owns_scalar_slot_index(idx);
443            if !top_has {
444                let name_owned: String = {
445                    let mut found = String::new();
446                    for i in (0..len).rev() {
447                        if let Some(Some(n)) = self.frames[i].scalar_slot_names.get(idx) {
448                            found = n.clone();
449                            break;
450                        }
451                    }
452                    if found.is_empty() {
453                        if let Some(sn) = slot_name {
454                            found = sn.to_string();
455                        }
456                    }
457                    found
458                };
459                let name = name_owned.as_str();
460                if !name.is_empty() && !Self::parallel_allowed_topic_scalar(name) {
461                    let inner = len.saturating_sub(1);
462                    for (fi, frame) in self.frames.iter().enumerate().rev() {
463                        if frame.has_scalar(name)
464                            || (idx < frame.scalar_slots.len() && frame.owns_scalar_slot_index(idx))
465                        {
466                            if fi != inner {
467                                return Err(PerlError::runtime(
468                                    format!(
469                                        "cannot assign to captured outer lexical `${}` inside a parallel block (use `mysync`)",
470                                        name
471                                    ),
472                                    0,
473                                ));
474                            }
475                            break;
476                        }
477                    }
478                }
479            }
480        }
481        self.set_scalar_slot(slot, val);
482        Ok(())
483    }
484
485    /// Declare + initialize scalar in the current frame's slot array.
486    /// `name` (bare identifier, e.g. `x` for `$x`) is stored for [`Scope::capture`] when the
487    /// binding is slot-only (no duplicate `frame.scalars` row).
488    #[inline]
489    pub fn declare_scalar_slot(&mut self, slot: u8, val: PerlValue, name: Option<&str>) {
490        let idx = slot as usize;
491        let frame = self.frames.last_mut().unwrap();
492        if idx >= frame.scalar_slots.len() {
493            frame.scalar_slots.resize(idx + 1, PerlValue::UNDEF);
494        }
495        frame.scalar_slots[idx] = val;
496        if idx >= frame.scalar_slot_names.len() {
497            frame.scalar_slot_names.resize(idx + 1, None);
498        }
499        match name {
500            Some(n) => frame.scalar_slot_names[idx] = Some(n.to_string()),
501            // Anonymous slot: mark occupied so padding holes don't shadow parent frame slots.
502            None => frame.scalar_slot_names[idx] = Some(String::new()),
503        }
504    }
505
506    /// Slot-indexed `.=` — avoids frame walking and string comparison on every iteration.
507    ///
508    /// Returns a [`PerlValue::shallow_clone`] (Arc::clone) of the stored value
509    /// rather than a full [`Clone`], which would deep-copy the entire `String`
510    /// payload and turn a `$s .= "x"` loop into O(N²) memcpy.
511    /// Repeated `$slot .= rhs` fused-loop fast path: locates the slot's frame once,
512    /// tries `try_concat_repeat_inplace` (unique heap-String → single `reserve`+`push_str`
513    /// burst), and returns `true` on success. Returns `false` when the slot is not a
514    /// uniquely-held `String` so the caller can fall back to the per-iteration slow
515    /// path. Called from `Op::ConcatConstSlotLoop`.
516    #[inline]
517    pub fn scalar_slot_concat_repeat_inplace(&mut self, slot: u8, rhs: &str, n: usize) -> bool {
518        let idx = slot as usize;
519        let len = self.frames.len();
520        let fi = {
521            let mut found = len - 1;
522            if idx >= self.frames[found].scalar_slots.len()
523                || !self.frames[found].owns_scalar_slot_index(idx)
524            {
525                for i in (0..len - 1).rev() {
526                    if idx < self.frames[i].scalar_slots.len()
527                        && self.frames[i].owns_scalar_slot_index(idx)
528                    {
529                        found = i;
530                        break;
531                    }
532                }
533            }
534            found
535        };
536        let frame = &mut self.frames[fi];
537        if idx >= frame.scalar_slots.len() {
538            frame.scalar_slots.resize(idx + 1, PerlValue::UNDEF);
539        }
540        frame.scalar_slots[idx].try_concat_repeat_inplace(rhs, n)
541    }
542
543    /// Slow fallback for the fused string-append loop: clones the RHS into a new
544    /// `PerlValue::string` once and runs the existing `scalar_slot_concat_inplace`
545    /// path `n` times. Used by `Op::ConcatConstSlotLoop` when the slot is aliased
546    /// and the in-place fast path rejected the mutation.
547    #[inline]
548    pub fn scalar_slot_concat_repeat_slow(&mut self, slot: u8, rhs: &str, n: usize) {
549        let pv = PerlValue::string(rhs.to_owned());
550        for _ in 0..n {
551            let _ = self.scalar_slot_concat_inplace(slot, &pv);
552        }
553    }
554
555    #[inline]
556    pub fn scalar_slot_concat_inplace(&mut self, slot: u8, rhs: &PerlValue) -> PerlValue {
557        let idx = slot as usize;
558        let len = self.frames.len();
559        let fi = {
560            let mut found = len - 1;
561            if idx >= self.frames[found].scalar_slots.len()
562                || !self.frames[found].owns_scalar_slot_index(idx)
563            {
564                for i in (0..len - 1).rev() {
565                    if idx < self.frames[i].scalar_slots.len()
566                        && self.frames[i].owns_scalar_slot_index(idx)
567                    {
568                        found = i;
569                        break;
570                    }
571                }
572            }
573            found
574        };
575        let frame = &mut self.frames[fi];
576        if idx >= frame.scalar_slots.len() {
577            frame.scalar_slots.resize(idx + 1, PerlValue::UNDEF);
578        }
579        // Fast path: when the slot holds the only `Arc<HeapObject::String>` handle,
580        // extend the underlying `String` buffer in place — no Arc alloc, no full
581        // unwrap/rewrap. This turns a `$s .= "x"` loop into `String::push_str` only.
582        // The shallow_clone handle that goes back onto the VM stack briefly bumps
583        // the refcount to 2, so the NEXT iteration's fast path would fail — except
584        // the VM immediately `Pop`s that handle (or `ConcatAppendSlotVoid` never
585        // pushes it), restoring unique ownership before the next `.=`.
586        if frame.scalar_slots[idx].try_concat_append_inplace(rhs) {
587            return frame.scalar_slots[idx].shallow_clone();
588        }
589        let new_val = std::mem::replace(&mut frame.scalar_slots[idx], PerlValue::UNDEF)
590            .concat_append_owned(rhs);
591        let handle = new_val.shallow_clone();
592        frame.scalar_slots[idx] = new_val;
593        handle
594    }
595
596    #[inline]
597    pub(crate) fn can_pop_frame(&self) -> bool {
598        self.frames.len() > 1
599    }
600
601    #[inline]
602    pub fn pop_frame(&mut self) {
603        if self.frames.len() > 1 {
604            let mut frame = self.frames.pop().expect("pop_frame");
605            // Local restore must write outer bindings even when parallel_guard is on
606            // (user code cannot mutate captured vars; unwind is not user mutation).
607            let saved_guard = self.parallel_guard;
608            self.parallel_guard = false;
609            for entry in frame.local_restores.drain(..).rev() {
610                match entry {
611                    LocalRestore::Scalar(name, old) => {
612                        let _ = self.set_scalar(&name, old);
613                    }
614                    LocalRestore::Array(name, old) => {
615                        let _ = self.set_array(&name, old);
616                    }
617                    LocalRestore::Hash(name, old) => {
618                        let _ = self.set_hash(&name, old);
619                    }
620                    LocalRestore::HashElement(name, key, old) => match old {
621                        Some(v) => {
622                            let _ = self.set_hash_element(&name, &key, v);
623                        }
624                        None => {
625                            let _ = self.delete_hash_element(&name, &key);
626                        }
627                    },
628                    LocalRestore::ArrayElement(name, index, old) => {
629                        let _ = self.set_array_element(&name, index, old);
630                    }
631                }
632            }
633            self.parallel_guard = saved_guard;
634            frame.clear_all_bindings();
635            // Return frame to pool for reuse (avoids allocation on next push_frame).
636            if self.frame_pool.len() < 64 {
637                self.frame_pool.push(frame);
638            }
639        }
640    }
641
642    /// `local $name` — save current value, assign `val`; restore on `pop_frame`.
643    pub fn local_set_scalar(&mut self, name: &str, val: PerlValue) -> Result<(), PerlError> {
644        let old = self.get_scalar(name);
645        if let Some(frame) = self.frames.last_mut() {
646            frame
647                .local_restores
648                .push(LocalRestore::Scalar(name.to_string(), old));
649        }
650        self.set_scalar(name, val)
651    }
652
653    /// `local @name` — not valid for `mysync` arrays.
654    pub fn local_set_array(&mut self, name: &str, val: Vec<PerlValue>) -> Result<(), PerlError> {
655        if self.find_atomic_array(name).is_some() {
656            return Err(PerlError::runtime(
657                "local cannot be used on mysync arrays",
658                0,
659            ));
660        }
661        let old = self.get_array(name);
662        if let Some(frame) = self.frames.last_mut() {
663            frame
664                .local_restores
665                .push(LocalRestore::Array(name.to_string(), old));
666        }
667        self.set_array(name, val)?;
668        Ok(())
669    }
670
671    /// `local %name`
672    pub fn local_set_hash(
673        &mut self,
674        name: &str,
675        val: IndexMap<String, PerlValue>,
676    ) -> Result<(), PerlError> {
677        if self.find_atomic_hash(name).is_some() {
678            return Err(PerlError::runtime(
679                "local cannot be used on mysync hashes",
680                0,
681            ));
682        }
683        let old = self.get_hash(name);
684        if let Some(frame) = self.frames.last_mut() {
685            frame
686                .local_restores
687                .push(LocalRestore::Hash(name.to_string(), old));
688        }
689        self.set_hash(name, val)?;
690        Ok(())
691    }
692
693    /// `local $h{key} = val` — save key state; restore one slot on `pop_frame`.
694    pub fn local_set_hash_element(
695        &mut self,
696        name: &str,
697        key: &str,
698        val: PerlValue,
699    ) -> Result<(), PerlError> {
700        if self.find_atomic_hash(name).is_some() {
701            return Err(PerlError::runtime(
702                "local cannot be used on mysync hash elements",
703                0,
704            ));
705        }
706        let old = if self.exists_hash_element(name, key) {
707            Some(self.get_hash_element(name, key))
708        } else {
709            None
710        };
711        if let Some(frame) = self.frames.last_mut() {
712            frame.local_restores.push(LocalRestore::HashElement(
713                name.to_string(),
714                key.to_string(),
715                old,
716            ));
717        }
718        self.set_hash_element(name, key, val)?;
719        Ok(())
720    }
721
722    /// `local $a[i] = val` — save element (as returned by [`Self::get_array_element`]), assign;
723    /// restore on [`Self::pop_frame`].
724    pub fn local_set_array_element(
725        &mut self,
726        name: &str,
727        index: i64,
728        val: PerlValue,
729    ) -> Result<(), PerlError> {
730        if self.find_atomic_array(name).is_some() {
731            return Err(PerlError::runtime(
732                "local cannot be used on mysync array elements",
733                0,
734            ));
735        }
736        let old = self.get_array_element(name, index);
737        if let Some(frame) = self.frames.last_mut() {
738            frame
739                .local_restores
740                .push(LocalRestore::ArrayElement(name.to_string(), index, old));
741        }
742        self.set_array_element(name, index, val)?;
743        Ok(())
744    }
745
746    // ── Scalars ──
747
748    #[inline]
749    pub fn declare_scalar(&mut self, name: &str, val: PerlValue) {
750        let _ = self.declare_scalar_frozen(name, val, false, None);
751    }
752
753    /// Declare a lexical scalar; `frozen` means no further assignment to this binding.
754    /// `ty` is from `typed my $x : Int` — enforced on every assignment.
755    pub fn declare_scalar_frozen(
756        &mut self,
757        name: &str,
758        val: PerlValue,
759        frozen: bool,
760        ty: Option<PerlTypeName>,
761    ) -> Result<(), PerlError> {
762        if let Some(ref t) = ty {
763            t.check_value(&val)
764                .map_err(|msg| PerlError::type_error(format!("`${}`: {}", name, msg), 0))?;
765        }
766        if let Some(frame) = self.frames.last_mut() {
767            frame.set_scalar(name, val);
768            if frozen {
769                frame.frozen_scalars.insert(name.to_string());
770            }
771            if let Some(t) = ty {
772                frame.typed_scalars.insert(name.to_string(), t);
773            }
774        }
775        Ok(())
776    }
777
778    /// True if the innermost lexical scalar binding for `name` is `frozen`.
779    pub fn is_scalar_frozen(&self, name: &str) -> bool {
780        for frame in self.frames.iter().rev() {
781            if frame.has_scalar(name) {
782                return frame.frozen_scalars.contains(name);
783            }
784        }
785        false
786    }
787
788    /// True if the innermost lexical array binding for `name` is `frozen`.
789    pub fn is_array_frozen(&self, name: &str) -> bool {
790        for frame in self.frames.iter().rev() {
791            if frame.has_array(name) {
792                return frame.frozen_arrays.contains(name);
793            }
794        }
795        false
796    }
797
798    /// True if the innermost lexical hash binding for `name` is `frozen`.
799    pub fn is_hash_frozen(&self, name: &str) -> bool {
800        for frame in self.frames.iter().rev() {
801            if frame.has_hash(name) {
802                return frame.frozen_hashes.contains(name);
803            }
804        }
805        false
806    }
807
808    /// Returns Some(sigil) if the named variable is frozen, None if mutable.
809    pub fn check_frozen(&self, sigil: &str, name: &str) -> Option<&'static str> {
810        match sigil {
811            "$" => {
812                if self.is_scalar_frozen(name) {
813                    Some("scalar")
814                } else {
815                    None
816                }
817            }
818            "@" => {
819                if self.is_array_frozen(name) {
820                    Some("array")
821                } else {
822                    None
823                }
824            }
825            "%" => {
826                if self.is_hash_frozen(name) {
827                    Some("hash")
828                } else {
829                    None
830                }
831            }
832            _ => None,
833        }
834    }
835
836    #[inline]
837    pub fn get_scalar(&self, name: &str) -> PerlValue {
838        for frame in self.frames.iter().rev() {
839            if let Some(val) = frame.get_scalar(name) {
840                // Transparently unwrap Atomic — read through the lock
841                if let Some(arc) = val.as_atomic_arc() {
842                    return arc.lock().clone();
843                }
844                // Transparently unwrap ScalarRef (captured closure variable) — read through the lock
845                if let Some(arc) = val.as_scalar_ref() {
846                    return arc.read().clone();
847                }
848                return val.clone();
849            }
850        }
851        PerlValue::UNDEF
852    }
853
854    /// True if any frame has a lexical scalar binding for `name` (`my` / `our` / assignment).
855    #[inline]
856    pub fn scalar_binding_exists(&self, name: &str) -> bool {
857        for frame in self.frames.iter().rev() {
858            if frame.has_scalar(name) {
859                return true;
860            }
861        }
862        false
863    }
864
865    /// Collect all scalar variable names across all frames (for debugger).
866    pub fn all_scalar_names(&self) -> Vec<String> {
867        let mut names = Vec::new();
868        for frame in &self.frames {
869            for (name, _) in &frame.scalars {
870                if !names.contains(name) {
871                    names.push(name.clone());
872                }
873            }
874            for name in frame.scalar_slot_names.iter().flatten() {
875                if !names.contains(name) {
876                    names.push(name.clone());
877                }
878            }
879        }
880        names
881    }
882
883    /// True if any frame or atomic slot holds an array named `name`.
884    #[inline]
885    pub fn array_binding_exists(&self, name: &str) -> bool {
886        if self.find_atomic_array(name).is_some() {
887            return true;
888        }
889        for frame in self.frames.iter().rev() {
890            if frame.has_array(name) {
891                return true;
892            }
893        }
894        false
895    }
896
897    /// True if any frame or atomic slot holds a hash named `name`.
898    #[inline]
899    pub fn hash_binding_exists(&self, name: &str) -> bool {
900        if self.find_atomic_hash(name).is_some() {
901            return true;
902        }
903        for frame in self.frames.iter().rev() {
904            if frame.has_hash(name) {
905                return true;
906            }
907        }
908        false
909    }
910
911    /// Get the raw scalar value WITHOUT unwrapping Atomic.
912    /// Used by scope.capture() to preserve the Arc for sharing across threads.
913    #[inline]
914    pub fn get_scalar_raw(&self, name: &str) -> PerlValue {
915        for frame in self.frames.iter().rev() {
916            if let Some(val) = frame.get_scalar(name) {
917                return val.clone();
918            }
919        }
920        PerlValue::UNDEF
921    }
922
923    /// Atomically read-modify-write a scalar. Holds the Mutex lock for
924    /// the entire cycle so `mysync` variables are race-free under `fan`/`pfor`.
925    /// Returns the NEW value.
926    pub fn atomic_mutate(
927        &mut self,
928        name: &str,
929        f: impl FnOnce(&PerlValue) -> PerlValue,
930    ) -> PerlValue {
931        for frame in self.frames.iter().rev() {
932            if let Some(v) = frame.get_scalar(name) {
933                if let Some(arc) = v.as_atomic_arc() {
934                    let mut guard = arc.lock();
935                    let old = guard.clone();
936                    let new_val = f(&guard);
937                    *guard = new_val.clone();
938                    crate::parallel_trace::emit_scalar_mutation(name, &old, &new_val);
939                    return new_val;
940                }
941            }
942        }
943        // Non-atomic fallback
944        let old = self.get_scalar(name);
945        let new_val = f(&old);
946        let _ = self.set_scalar(name, new_val.clone());
947        new_val
948    }
949
950    /// Like atomic_mutate but returns the OLD value (for postfix `$x++`).
951    pub fn atomic_mutate_post(
952        &mut self,
953        name: &str,
954        f: impl FnOnce(&PerlValue) -> PerlValue,
955    ) -> PerlValue {
956        for frame in self.frames.iter().rev() {
957            if let Some(v) = frame.get_scalar(name) {
958                if let Some(arc) = v.as_atomic_arc() {
959                    let mut guard = arc.lock();
960                    let old = guard.clone();
961                    let new_val = f(&old);
962                    *guard = new_val.clone();
963                    crate::parallel_trace::emit_scalar_mutation(name, &old, &new_val);
964                    return old;
965                }
966            }
967        }
968        // Non-atomic fallback
969        let old = self.get_scalar(name);
970        let _ = self.set_scalar(name, f(&old));
971        old
972    }
973
974    /// Append `rhs` to a scalar string in-place (no clone of the existing string).
975    /// If the scalar is not yet a String, it is converted first.
976    ///
977    /// The binding and the returned [`PerlValue`] share the same heap [`Arc`] via
978    /// [`PerlValue::shallow_clone`] on the store — a full [`Clone`] would deep-copy the
979    /// entire `String` each time and make repeated `.=` O(N²) in the total length.
980    #[inline]
981    pub fn scalar_concat_inplace(
982        &mut self,
983        name: &str,
984        rhs: &PerlValue,
985    ) -> Result<PerlValue, PerlError> {
986        self.check_parallel_scalar_write(name)?;
987        for frame in self.frames.iter_mut().rev() {
988            if let Some(entry) = frame.scalars.iter_mut().find(|(k, _)| k == name) {
989                // `mysync $x` stores `HeapObject::Atomic` — must mutate under the mutex, not
990                // `into_string()` the wrapper (that would stringify the cell, not the payload).
991                if let Some(atomic_arc) = entry.1.as_atomic_arc() {
992                    let mut guard = atomic_arc.lock();
993                    let inner = std::mem::replace(&mut *guard, PerlValue::UNDEF);
994                    let new_val = inner.concat_append_owned(rhs);
995                    *guard = new_val.shallow_clone();
996                    return Ok(new_val);
997                }
998                // Fast path: same `Arc::get_mut` trick as the slot variant — mutate the
999                // underlying `String` directly when the scalar is the lone handle.
1000                if entry.1.try_concat_append_inplace(rhs) {
1001                    return Ok(entry.1.shallow_clone());
1002                }
1003                // Use `into_string` + `append_to` so heap strings take the `Arc::try_unwrap`
1004                // fast path instead of `Display` / heap formatting on every `.=`.
1005                let new_val =
1006                    std::mem::replace(&mut entry.1, PerlValue::UNDEF).concat_append_owned(rhs);
1007                entry.1 = new_val.shallow_clone();
1008                return Ok(new_val);
1009            }
1010        }
1011        // Variable not found — create as new string
1012        let val = PerlValue::UNDEF.concat_append_owned(rhs);
1013        self.frames[0].set_scalar(name, val.shallow_clone());
1014        Ok(val)
1015    }
1016
1017    #[inline]
1018    pub fn set_scalar(&mut self, name: &str, val: PerlValue) -> Result<(), PerlError> {
1019        self.check_parallel_scalar_write(name)?;
1020        for frame in self.frames.iter_mut().rev() {
1021            // If the existing value is Atomic, write through the lock
1022            if let Some(v) = frame.get_scalar(name) {
1023                if let Some(arc) = v.as_atomic_arc() {
1024                    let mut guard = arc.lock();
1025                    let old = guard.clone();
1026                    *guard = val.clone();
1027                    crate::parallel_trace::emit_scalar_mutation(name, &old, &val);
1028                    return Ok(());
1029                }
1030                // If the existing value is ScalarRef (captured closure variable), write through it
1031                if let Some(arc) = v.as_scalar_ref() {
1032                    *arc.write() = val;
1033                    return Ok(());
1034                }
1035            }
1036            if frame.has_scalar(name) {
1037                if let Some(ty) = frame.typed_scalars.get(name) {
1038                    ty.check_value(&val)
1039                        .map_err(|msg| PerlError::type_error(format!("`${}`: {}", name, msg), 0))?;
1040                }
1041                frame.set_scalar(name, val);
1042                return Ok(());
1043            }
1044        }
1045        self.frames[0].set_scalar(name, val);
1046        Ok(())
1047    }
1048
1049    /// Set the topic variable `$_` and its numeric alias `$_0` together.
1050    /// Use this for single-arg closures (map, grep, etc.) so both `$_` and `$_0` work.
1051    /// This declares them in the current scope (not global), suitable for sub calls.
1052    ///
1053    /// Also sets outer topic aliases: `$_<` = previous `$_`, `$_<<` = previous `$_<`, etc.
1054    /// This allows nested blocks (e.g. `fan` inside `>{}`) to access enclosing topic values.
1055    #[inline]
1056    pub fn set_topic(&mut self, val: PerlValue) {
1057        // Shift existing outer topics down one level before setting new topic.
1058        // We support up to 4 levels: $_<, $_<<, $_<<<, $_<<<<
1059        // First, read current values (in reverse order to avoid overwriting what we read).
1060        let old_3lt = self.get_scalar("_<<<");
1061        let old_2lt = self.get_scalar("_<<");
1062        let old_1lt = self.get_scalar("_<");
1063        let old_topic = self.get_scalar("_");
1064
1065        // Now set the new values
1066        self.declare_scalar("_", val.clone());
1067        self.declare_scalar("_0", val);
1068        // Set outer topics only if there was a previous topic
1069        if !old_topic.is_undef() {
1070            self.declare_scalar("_<", old_topic);
1071        }
1072        if !old_1lt.is_undef() {
1073            self.declare_scalar("_<<", old_1lt);
1074        }
1075        if !old_2lt.is_undef() {
1076            self.declare_scalar("_<<<", old_2lt);
1077        }
1078        if !old_3lt.is_undef() {
1079            self.declare_scalar("_<<<<", old_3lt);
1080        }
1081    }
1082
1083    /// Set numeric closure argument aliases `$_0`, `$_1`, `$_2`, ... for all args.
1084    /// Also sets `$_` to the first argument (if any), shifting outer topics like [`set_topic`].
1085    #[inline]
1086    pub fn set_closure_args(&mut self, args: &[PerlValue]) {
1087        if let Some(first) = args.first() {
1088            // Use set_topic to properly shift the topic stack
1089            self.set_topic(first.clone());
1090        }
1091        for (i, val) in args.iter().enumerate() {
1092            self.declare_scalar(&format!("_{}", i), val.clone());
1093        }
1094    }
1095
1096    /// Register a `defer { BLOCK }` closure to run when this scope exits.
1097    #[inline]
1098    pub fn push_defer(&mut self, coderef: PerlValue) {
1099        if let Some(frame) = self.frames.last_mut() {
1100            frame.defers.push(coderef);
1101        }
1102    }
1103
1104    /// Take all deferred blocks from the current frame (for execution on scope exit).
1105    /// Returns them in reverse order (LIFO - last defer runs first).
1106    #[inline]
1107    pub fn take_defers(&mut self) -> Vec<PerlValue> {
1108        if let Some(frame) = self.frames.last_mut() {
1109            let mut defers = std::mem::take(&mut frame.defers);
1110            defers.reverse();
1111            defers
1112        } else {
1113            Vec::new()
1114        }
1115    }
1116
1117    // ── Atomic array/hash declarations ──
1118
1119    pub fn declare_atomic_array(&mut self, name: &str, val: Vec<PerlValue>) {
1120        if let Some(frame) = self.frames.last_mut() {
1121            frame
1122                .atomic_arrays
1123                .push((name.to_string(), AtomicArray(Arc::new(Mutex::new(val)))));
1124        }
1125    }
1126
1127    pub fn declare_atomic_hash(&mut self, name: &str, val: IndexMap<String, PerlValue>) {
1128        if let Some(frame) = self.frames.last_mut() {
1129            frame
1130                .atomic_hashes
1131                .push((name.to_string(), AtomicHash(Arc::new(Mutex::new(val)))));
1132        }
1133    }
1134
1135    /// Find an atomic array by name (returns the Arc for sharing).
1136    fn find_atomic_array(&self, name: &str) -> Option<&AtomicArray> {
1137        for frame in self.frames.iter().rev() {
1138            if let Some(aa) = frame.atomic_arrays.iter().find(|(k, _)| k == name) {
1139                return Some(&aa.1);
1140            }
1141        }
1142        None
1143    }
1144
1145    /// Find an atomic hash by name.
1146    fn find_atomic_hash(&self, name: &str) -> Option<&AtomicHash> {
1147        for frame in self.frames.iter().rev() {
1148            if let Some(ah) = frame.atomic_hashes.iter().find(|(k, _)| k == name) {
1149                return Some(&ah.1);
1150            }
1151        }
1152        None
1153    }
1154
1155    // ── Arrays ──
1156
1157    /// Remove `@_` from the innermost frame without cloning (move out of the frame `sub_underscore` field).
1158    /// Call sites restore with [`Self::declare_array`] before running a body that uses `shift` / `@_`.
1159    #[inline]
1160    pub fn take_sub_underscore(&mut self) -> Option<Vec<PerlValue>> {
1161        self.frames.last_mut()?.sub_underscore.take()
1162    }
1163
1164    pub fn declare_array(&mut self, name: &str, val: Vec<PerlValue>) {
1165        self.declare_array_frozen(name, val, false);
1166    }
1167
1168    pub fn declare_array_frozen(&mut self, name: &str, val: Vec<PerlValue>, frozen: bool) {
1169        // Package stash names (`Foo::BAR`) live in the outermost frame so nested blocks/subs
1170        // cannot shadow `@C::ISA` with an empty array (breaks inheritance / SUPER).
1171        let idx = if name.contains("::") {
1172            0
1173        } else {
1174            self.frames.len().saturating_sub(1)
1175        };
1176        if let Some(frame) = self.frames.get_mut(idx) {
1177            frame.set_array(name, val);
1178            if frozen {
1179                frame.frozen_arrays.insert(name.to_string());
1180            } else {
1181                // Redeclaring as non-frozen should unfreeze if previously frozen
1182                frame.frozen_arrays.remove(name);
1183            }
1184        }
1185    }
1186
1187    pub fn get_array(&self, name: &str) -> Vec<PerlValue> {
1188        // Check atomic arrays first
1189        if let Some(aa) = self.find_atomic_array(name) {
1190            return aa.0.lock().clone();
1191        }
1192        if name.contains("::") {
1193            if let Some(f) = self.frames.first() {
1194                if let Some(val) = f.get_array(name) {
1195                    return val.clone();
1196                }
1197            }
1198            return Vec::new();
1199        }
1200        for frame in self.frames.iter().rev() {
1201            if let Some(val) = frame.get_array(name) {
1202                return val.clone();
1203            }
1204        }
1205        Vec::new()
1206    }
1207
1208    /// Borrow the innermost binding for `name` when it is a plain [`Vec`] (not `mysync`).
1209    /// Used to pass `@_` to [`crate::list_util::native_dispatch`] without cloning the vector.
1210    #[inline]
1211    pub fn get_array_borrow(&self, name: &str) -> Option<&[PerlValue]> {
1212        if self.find_atomic_array(name).is_some() {
1213            return None;
1214        }
1215        if name.contains("::") {
1216            return self
1217                .frames
1218                .first()
1219                .and_then(|f| f.get_array(name))
1220                .map(|v| v.as_slice());
1221        }
1222        for frame in self.frames.iter().rev() {
1223            if let Some(val) = frame.get_array(name) {
1224                return Some(val.as_slice());
1225            }
1226        }
1227        None
1228    }
1229
1230    fn resolve_array_frame_idx(&self, name: &str) -> Option<usize> {
1231        if name.contains("::") {
1232            return Some(0);
1233        }
1234        (0..self.frames.len())
1235            .rev()
1236            .find(|&i| self.frames[i].has_array(name))
1237    }
1238
1239    fn check_parallel_array_write(&self, name: &str) -> Result<(), PerlError> {
1240        if !self.parallel_guard
1241            || Self::parallel_skip_special_name(name)
1242            || Self::parallel_allowed_internal_array(name)
1243        {
1244            return Ok(());
1245        }
1246        let inner = self.frames.len().saturating_sub(1);
1247        match self.resolve_array_frame_idx(name) {
1248            None => Err(PerlError::runtime(
1249                format!(
1250                    "cannot modify undeclared array `@{}` in a parallel block",
1251                    name
1252                ),
1253                0,
1254            )),
1255            Some(idx) if idx != inner => Err(PerlError::runtime(
1256                format!(
1257                    "cannot modify captured non-mysync array `@{}` in a parallel block",
1258                    name
1259                ),
1260                0,
1261            )),
1262            Some(_) => Ok(()),
1263        }
1264    }
1265
1266    pub fn get_array_mut(&mut self, name: &str) -> Result<&mut Vec<PerlValue>, PerlError> {
1267        // Note: can't return &mut into a Mutex. Callers needing atomic array
1268        // mutation should use atomic_array_mutate instead. For non-atomic arrays:
1269        if self.find_atomic_array(name).is_some() {
1270            return Err(PerlError::runtime(
1271                "get_array_mut: use atomic path for mysync arrays",
1272                0,
1273            ));
1274        }
1275        self.check_parallel_array_write(name)?;
1276        let idx = self.resolve_array_frame_idx(name).unwrap_or_default();
1277        let frame = &mut self.frames[idx];
1278        if frame.get_array_mut(name).is_none() {
1279            frame.arrays.push((name.to_string(), Vec::new()));
1280        }
1281        Ok(frame.get_array_mut(name).unwrap())
1282    }
1283
1284    /// Push to array — works for both regular and atomic arrays.
1285    pub fn push_to_array(&mut self, name: &str, val: PerlValue) -> Result<(), PerlError> {
1286        if let Some(aa) = self.find_atomic_array(name) {
1287            aa.0.lock().push(val);
1288            return Ok(());
1289        }
1290        self.get_array_mut(name)?.push(val);
1291        Ok(())
1292    }
1293
1294    /// Bulk `push @name, start..end-1` for the fused counted-loop superinstruction:
1295    /// reserves the `Vec` once, then pushes `PerlValue::integer(i)` for `i in start..end`
1296    /// in a tight Rust loop. Atomic arrays take a single `lock().push()` burst.
1297    pub fn push_int_range_to_array(
1298        &mut self,
1299        name: &str,
1300        start: i64,
1301        end: i64,
1302    ) -> Result<(), PerlError> {
1303        if end <= start {
1304            return Ok(());
1305        }
1306        let count = (end - start) as usize;
1307        if let Some(aa) = self.find_atomic_array(name) {
1308            let mut g = aa.0.lock();
1309            g.reserve(count);
1310            for i in start..end {
1311                g.push(PerlValue::integer(i));
1312            }
1313            return Ok(());
1314        }
1315        let arr = self.get_array_mut(name)?;
1316        arr.reserve(count);
1317        for i in start..end {
1318            arr.push(PerlValue::integer(i));
1319        }
1320        Ok(())
1321    }
1322
1323    /// Pop from array — works for both regular and atomic arrays.
1324    pub fn pop_from_array(&mut self, name: &str) -> Result<PerlValue, PerlError> {
1325        if let Some(aa) = self.find_atomic_array(name) {
1326            return Ok(aa.0.lock().pop().unwrap_or(PerlValue::UNDEF));
1327        }
1328        Ok(self.get_array_mut(name)?.pop().unwrap_or(PerlValue::UNDEF))
1329    }
1330
1331    /// Shift from array — works for both regular and atomic arrays.
1332    pub fn shift_from_array(&mut self, name: &str) -> Result<PerlValue, PerlError> {
1333        if let Some(aa) = self.find_atomic_array(name) {
1334            let mut guard = aa.0.lock();
1335            return Ok(if guard.is_empty() {
1336                PerlValue::UNDEF
1337            } else {
1338                guard.remove(0)
1339            });
1340        }
1341        let arr = self.get_array_mut(name)?;
1342        Ok(if arr.is_empty() {
1343            PerlValue::UNDEF
1344        } else {
1345            arr.remove(0)
1346        })
1347    }
1348
1349    /// Get array length — works for both regular and atomic arrays.
1350    pub fn array_len(&self, name: &str) -> usize {
1351        if let Some(aa) = self.find_atomic_array(name) {
1352            return aa.0.lock().len();
1353        }
1354        if name.contains("::") {
1355            return self
1356                .frames
1357                .first()
1358                .and_then(|f| f.get_array(name))
1359                .map(|a| a.len())
1360                .unwrap_or(0);
1361        }
1362        for frame in self.frames.iter().rev() {
1363            if let Some(arr) = frame.get_array(name) {
1364                return arr.len();
1365            }
1366        }
1367        0
1368    }
1369
1370    pub fn set_array(&mut self, name: &str, val: Vec<PerlValue>) -> Result<(), PerlError> {
1371        if let Some(aa) = self.find_atomic_array(name) {
1372            *aa.0.lock() = val;
1373            return Ok(());
1374        }
1375        self.check_parallel_array_write(name)?;
1376        for frame in self.frames.iter_mut().rev() {
1377            if frame.has_array(name) {
1378                frame.set_array(name, val);
1379                return Ok(());
1380            }
1381        }
1382        self.frames[0].set_array(name, val);
1383        Ok(())
1384    }
1385
1386    /// Direct element access — works for both regular and atomic arrays.
1387    #[inline]
1388    pub fn get_array_element(&self, name: &str, index: i64) -> PerlValue {
1389        if let Some(aa) = self.find_atomic_array(name) {
1390            let arr = aa.0.lock();
1391            let idx = if index < 0 {
1392                (arr.len() as i64 + index) as usize
1393            } else {
1394                index as usize
1395            };
1396            return arr.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
1397        }
1398        for frame in self.frames.iter().rev() {
1399            if let Some(arr) = frame.get_array(name) {
1400                let idx = if index < 0 {
1401                    (arr.len() as i64 + index) as usize
1402                } else {
1403                    index as usize
1404                };
1405                return arr.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
1406            }
1407        }
1408        PerlValue::UNDEF
1409    }
1410
1411    pub fn set_array_element(
1412        &mut self,
1413        name: &str,
1414        index: i64,
1415        val: PerlValue,
1416    ) -> Result<(), PerlError> {
1417        if let Some(aa) = self.find_atomic_array(name) {
1418            let mut arr = aa.0.lock();
1419            let idx = if index < 0 {
1420                (arr.len() as i64 + index).max(0) as usize
1421            } else {
1422                index as usize
1423            };
1424            if idx >= arr.len() {
1425                arr.resize(idx + 1, PerlValue::UNDEF);
1426            }
1427            arr[idx] = val;
1428            return Ok(());
1429        }
1430        let arr = self.get_array_mut(name)?;
1431        let idx = if index < 0 {
1432            let len = arr.len() as i64;
1433            (len + index).max(0) as usize
1434        } else {
1435            index as usize
1436        };
1437        if idx >= arr.len() {
1438            arr.resize(idx + 1, PerlValue::UNDEF);
1439        }
1440        arr[idx] = val;
1441        Ok(())
1442    }
1443
1444    /// Perl `exists $a[$i]` — true when the slot index is within the current array length.
1445    pub fn exists_array_element(&self, name: &str, index: i64) -> bool {
1446        if let Some(aa) = self.find_atomic_array(name) {
1447            let arr = aa.0.lock();
1448            let idx = if index < 0 {
1449                (arr.len() as i64 + index) as usize
1450            } else {
1451                index as usize
1452            };
1453            return idx < arr.len();
1454        }
1455        for frame in self.frames.iter().rev() {
1456            if let Some(arr) = frame.get_array(name) {
1457                let idx = if index < 0 {
1458                    (arr.len() as i64 + index) as usize
1459                } else {
1460                    index as usize
1461                };
1462                return idx < arr.len();
1463            }
1464        }
1465        false
1466    }
1467
1468    /// Perl `delete $a[$i]` — sets the element to `undef`, returns the previous value.
1469    pub fn delete_array_element(&mut self, name: &str, index: i64) -> Result<PerlValue, PerlError> {
1470        if let Some(aa) = self.find_atomic_array(name) {
1471            let mut arr = aa.0.lock();
1472            let idx = if index < 0 {
1473                (arr.len() as i64 + index) as usize
1474            } else {
1475                index as usize
1476            };
1477            if idx >= arr.len() {
1478                return Ok(PerlValue::UNDEF);
1479            }
1480            let old = arr.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
1481            arr[idx] = PerlValue::UNDEF;
1482            return Ok(old);
1483        }
1484        let arr = self.get_array_mut(name)?;
1485        let idx = if index < 0 {
1486            (arr.len() as i64 + index) as usize
1487        } else {
1488            index as usize
1489        };
1490        if idx >= arr.len() {
1491            return Ok(PerlValue::UNDEF);
1492        }
1493        let old = arr.get(idx).cloned().unwrap_or(PerlValue::UNDEF);
1494        arr[idx] = PerlValue::UNDEF;
1495        Ok(old)
1496    }
1497
1498    // ── Hashes ──
1499
1500    #[inline]
1501    pub fn declare_hash(&mut self, name: &str, val: IndexMap<String, PerlValue>) {
1502        self.declare_hash_frozen(name, val, false);
1503    }
1504
1505    pub fn declare_hash_frozen(
1506        &mut self,
1507        name: &str,
1508        val: IndexMap<String, PerlValue>,
1509        frozen: bool,
1510    ) {
1511        if let Some(frame) = self.frames.last_mut() {
1512            frame.set_hash(name, val);
1513            if frozen {
1514                frame.frozen_hashes.insert(name.to_string());
1515            }
1516        }
1517    }
1518
1519    /// Declare a hash in the bottom (global) frame, not the current lexical frame.
1520    pub fn declare_hash_global(&mut self, name: &str, val: IndexMap<String, PerlValue>) {
1521        if let Some(frame) = self.frames.first_mut() {
1522            frame.set_hash(name, val);
1523        }
1524    }
1525
1526    /// Declare a frozen hash in the bottom (global) frame — prevents user reassignment.
1527    pub fn declare_hash_global_frozen(&mut self, name: &str, val: IndexMap<String, PerlValue>) {
1528        if let Some(frame) = self.frames.first_mut() {
1529            frame.set_hash(name, val);
1530            frame.frozen_hashes.insert(name.to_string());
1531        }
1532    }
1533
1534    /// Returns `true` if a lexical (non-bottom) frame declares `%name`.
1535    pub fn has_lexical_hash(&self, name: &str) -> bool {
1536        self.frames.iter().skip(1).any(|f| f.has_hash(name))
1537    }
1538
1539    /// Returns `true` if ANY frame (including global) declares `%name`.
1540    pub fn any_frame_has_hash(&self, name: &str) -> bool {
1541        self.frames.iter().any(|f| f.has_hash(name))
1542    }
1543
1544    pub fn get_hash(&self, name: &str) -> IndexMap<String, PerlValue> {
1545        if let Some(ah) = self.find_atomic_hash(name) {
1546            return ah.0.lock().clone();
1547        }
1548        for frame in self.frames.iter().rev() {
1549            if let Some(val) = frame.get_hash(name) {
1550                return val.clone();
1551            }
1552        }
1553        IndexMap::new()
1554    }
1555
1556    fn resolve_hash_frame_idx(&self, name: &str) -> Option<usize> {
1557        if name.contains("::") {
1558            return Some(0);
1559        }
1560        (0..self.frames.len())
1561            .rev()
1562            .find(|&i| self.frames[i].has_hash(name))
1563    }
1564
1565    fn check_parallel_hash_write(&self, name: &str) -> Result<(), PerlError> {
1566        if !self.parallel_guard
1567            || Self::parallel_skip_special_name(name)
1568            || Self::parallel_allowed_internal_hash(name)
1569        {
1570            return Ok(());
1571        }
1572        let inner = self.frames.len().saturating_sub(1);
1573        match self.resolve_hash_frame_idx(name) {
1574            None => Err(PerlError::runtime(
1575                format!(
1576                    "cannot modify undeclared hash `%{}` in a parallel block",
1577                    name
1578                ),
1579                0,
1580            )),
1581            Some(idx) if idx != inner => Err(PerlError::runtime(
1582                format!(
1583                    "cannot modify captured non-mysync hash `%{}` in a parallel block",
1584                    name
1585                ),
1586                0,
1587            )),
1588            Some(_) => Ok(()),
1589        }
1590    }
1591
1592    pub fn get_hash_mut(
1593        &mut self,
1594        name: &str,
1595    ) -> Result<&mut IndexMap<String, PerlValue>, PerlError> {
1596        if self.find_atomic_hash(name).is_some() {
1597            return Err(PerlError::runtime(
1598                "get_hash_mut: use atomic path for mysync hashes",
1599                0,
1600            ));
1601        }
1602        self.check_parallel_hash_write(name)?;
1603        let idx = self.resolve_hash_frame_idx(name).unwrap_or_default();
1604        let frame = &mut self.frames[idx];
1605        if frame.get_hash_mut(name).is_none() {
1606            frame.hashes.push((name.to_string(), IndexMap::new()));
1607        }
1608        Ok(frame.get_hash_mut(name).unwrap())
1609    }
1610
1611    pub fn set_hash(
1612        &mut self,
1613        name: &str,
1614        val: IndexMap<String, PerlValue>,
1615    ) -> Result<(), PerlError> {
1616        if let Some(ah) = self.find_atomic_hash(name) {
1617            *ah.0.lock() = val;
1618            return Ok(());
1619        }
1620        self.check_parallel_hash_write(name)?;
1621        for frame in self.frames.iter_mut().rev() {
1622            if frame.has_hash(name) {
1623                frame.set_hash(name, val);
1624                return Ok(());
1625            }
1626        }
1627        self.frames[0].set_hash(name, val);
1628        Ok(())
1629    }
1630
1631    #[inline]
1632    pub fn get_hash_element(&self, name: &str, key: &str) -> PerlValue {
1633        if let Some(ah) = self.find_atomic_hash(name) {
1634            return ah.0.lock().get(key).cloned().unwrap_or(PerlValue::UNDEF);
1635        }
1636        for frame in self.frames.iter().rev() {
1637            if let Some(hash) = frame.get_hash(name) {
1638                return hash.get(key).cloned().unwrap_or(PerlValue::UNDEF);
1639            }
1640        }
1641        PerlValue::UNDEF
1642    }
1643
1644    /// Atomically read-modify-write a hash element. For atomic hashes, holds
1645    /// the Mutex for the full cycle. Returns the new value.
1646    pub fn atomic_hash_mutate(
1647        &mut self,
1648        name: &str,
1649        key: &str,
1650        f: impl FnOnce(&PerlValue) -> PerlValue,
1651    ) -> Result<PerlValue, PerlError> {
1652        if let Some(ah) = self.find_atomic_hash(name) {
1653            let mut guard = ah.0.lock();
1654            let old = guard.get(key).cloned().unwrap_or(PerlValue::UNDEF);
1655            let new_val = f(&old);
1656            guard.insert(key.to_string(), new_val.clone());
1657            return Ok(new_val);
1658        }
1659        // Non-atomic fallback
1660        let old = self.get_hash_element(name, key);
1661        let new_val = f(&old);
1662        self.set_hash_element(name, key, new_val.clone())?;
1663        Ok(new_val)
1664    }
1665
1666    /// Atomically read-modify-write an array element. Returns the new value.
1667    pub fn atomic_array_mutate(
1668        &mut self,
1669        name: &str,
1670        index: i64,
1671        f: impl FnOnce(&PerlValue) -> PerlValue,
1672    ) -> Result<PerlValue, PerlError> {
1673        if let Some(aa) = self.find_atomic_array(name) {
1674            let mut guard = aa.0.lock();
1675            let idx = if index < 0 {
1676                (guard.len() as i64 + index).max(0) as usize
1677            } else {
1678                index as usize
1679            };
1680            if idx >= guard.len() {
1681                guard.resize(idx + 1, PerlValue::UNDEF);
1682            }
1683            let old = guard[idx].clone();
1684            let new_val = f(&old);
1685            guard[idx] = new_val.clone();
1686            return Ok(new_val);
1687        }
1688        // Non-atomic fallback
1689        let old = self.get_array_element(name, index);
1690        let new_val = f(&old);
1691        self.set_array_element(name, index, new_val.clone())?;
1692        Ok(new_val)
1693    }
1694
1695    pub fn set_hash_element(
1696        &mut self,
1697        name: &str,
1698        key: &str,
1699        val: PerlValue,
1700    ) -> Result<(), PerlError> {
1701        // `$SIG{INT} = \&h` — lazily install the matching signal hook. Until Perl code touches
1702        // `%SIG`, the POSIX default stays in place so Ctrl-C terminates immediately.
1703        if name == "SIG" {
1704            crate::perl_signal::install(key);
1705        }
1706        if let Some(ah) = self.find_atomic_hash(name) {
1707            ah.0.lock().insert(key.to_string(), val);
1708            return Ok(());
1709        }
1710        let hash = self.get_hash_mut(name)?;
1711        hash.insert(key.to_string(), val);
1712        Ok(())
1713    }
1714
1715    /// Bulk `for i in start..end { $h{i} = i * k }` for the fused hash-insert loop.
1716    /// Reserves capacity once and runs the whole range in a tight Rust loop.
1717    /// `itoa` is used to stringify each key without a transient `format!` allocation.
1718    pub fn set_hash_int_times_range(
1719        &mut self,
1720        name: &str,
1721        start: i64,
1722        end: i64,
1723        k: i64,
1724    ) -> Result<(), PerlError> {
1725        if end <= start {
1726            return Ok(());
1727        }
1728        let count = (end - start) as usize;
1729        if let Some(ah) = self.find_atomic_hash(name) {
1730            let mut g = ah.0.lock();
1731            g.reserve(count);
1732            let mut buf = itoa::Buffer::new();
1733            for i in start..end {
1734                let key = buf.format(i).to_owned();
1735                g.insert(key, PerlValue::integer(i.wrapping_mul(k)));
1736            }
1737            return Ok(());
1738        }
1739        let hash = self.get_hash_mut(name)?;
1740        hash.reserve(count);
1741        let mut buf = itoa::Buffer::new();
1742        for i in start..end {
1743            let key = buf.format(i).to_owned();
1744            hash.insert(key, PerlValue::integer(i.wrapping_mul(k)));
1745        }
1746        Ok(())
1747    }
1748
1749    pub fn delete_hash_element(&mut self, name: &str, key: &str) -> Result<PerlValue, PerlError> {
1750        if let Some(ah) = self.find_atomic_hash(name) {
1751            return Ok(ah.0.lock().shift_remove(key).unwrap_or(PerlValue::UNDEF));
1752        }
1753        let hash = self.get_hash_mut(name)?;
1754        Ok(hash.shift_remove(key).unwrap_or(PerlValue::UNDEF))
1755    }
1756
1757    #[inline]
1758    pub fn exists_hash_element(&self, name: &str, key: &str) -> bool {
1759        if let Some(ah) = self.find_atomic_hash(name) {
1760            return ah.0.lock().contains_key(key);
1761        }
1762        for frame in self.frames.iter().rev() {
1763            if let Some(hash) = frame.get_hash(name) {
1764                return hash.contains_key(key);
1765            }
1766        }
1767        false
1768    }
1769
1770    /// Walk all values of the named hash with a visitor. Used by the fused
1771    /// `for my $k (keys %h) { $sum += $h{$k} }` op so the hot loop runs without
1772    /// cloning the entire map into a keys array (vs the un-fused shape, which
1773    /// allocates one `PerlValue::string` per key).
1774    #[inline]
1775    pub fn for_each_hash_value(&self, name: &str, mut visit: impl FnMut(&PerlValue)) {
1776        if let Some(ah) = self.find_atomic_hash(name) {
1777            let g = ah.0.lock();
1778            for v in g.values() {
1779                visit(v);
1780            }
1781            return;
1782        }
1783        for frame in self.frames.iter().rev() {
1784            if let Some(hash) = frame.get_hash(name) {
1785                for v in hash.values() {
1786                    visit(v);
1787                }
1788                return;
1789            }
1790        }
1791    }
1792
1793    /// Sigil-prefixed names (`$x`, `@a`, `%h`) from all frames, for REPL tab-completion.
1794    pub fn repl_binding_names(&self) -> Vec<String> {
1795        let mut seen = HashSet::new();
1796        let mut out = Vec::new();
1797        for frame in &self.frames {
1798            for (name, _) in &frame.scalars {
1799                let s = format!("${}", name);
1800                if seen.insert(s.clone()) {
1801                    out.push(s);
1802                }
1803            }
1804            for (name, _) in &frame.arrays {
1805                let s = format!("@{}", name);
1806                if seen.insert(s.clone()) {
1807                    out.push(s);
1808                }
1809            }
1810            for (name, _) in &frame.hashes {
1811                let s = format!("%{}", name);
1812                if seen.insert(s.clone()) {
1813                    out.push(s);
1814                }
1815            }
1816            for (name, _) in &frame.atomic_arrays {
1817                let s = format!("@{}", name);
1818                if seen.insert(s.clone()) {
1819                    out.push(s);
1820                }
1821            }
1822            for (name, _) in &frame.atomic_hashes {
1823                let s = format!("%{}", name);
1824                if seen.insert(s.clone()) {
1825                    out.push(s);
1826                }
1827            }
1828        }
1829        out.sort();
1830        out
1831    }
1832
1833    pub fn capture(&mut self) -> Vec<(String, PerlValue)> {
1834        let mut captured = Vec::new();
1835        for frame in &mut self.frames {
1836            for (k, v) in &mut frame.scalars {
1837                // Wrap scalar in ScalarRef so the closure shares the same memory cell.
1838                // If it's already a ScalarRef, just clone it (shares the same Arc).
1839                // Only wrap simple scalars (integers, floats, strings, undef); complex values
1840                // like refs, blessed objects, atomics, etc. already share via Arc and wrapping
1841                // them in ScalarRef breaks type detection (as_ppool, as_blessed_ref, etc.).
1842                if v.as_scalar_ref().is_some() {
1843                    captured.push((format!("${}", k), v.clone()));
1844                } else if v.is_simple_scalar() {
1845                    let wrapped = PerlValue::scalar_ref(Arc::new(RwLock::new(v.clone())));
1846                    // Update the original scope variable to point to the same ScalarRef
1847                    // so that subsequent closures share the same reference.
1848                    *v = wrapped.clone();
1849                    captured.push((format!("${}", k), wrapped));
1850                } else {
1851                    captured.push((format!("${}", k), v.clone()));
1852                }
1853            }
1854            for (i, v) in frame.scalar_slots.iter().enumerate() {
1855                if let Some(Some(name)) = frame.scalar_slot_names.get(i) {
1856                    // Scalar slots are used by the VM; don't modify them in-place.
1857                    // Wrap in ScalarRef for the captured closure environment only.
1858                    let wrapped = if v.as_scalar_ref().is_some() {
1859                        v.clone()
1860                    } else {
1861                        PerlValue::scalar_ref(Arc::new(RwLock::new(v.clone())))
1862                    };
1863                    captured.push((format!("$slot:{}:{}", i, name), wrapped));
1864                }
1865            }
1866            for (k, v) in &frame.arrays {
1867                if capture_skip_bootstrap_array(k) {
1868                    continue;
1869                }
1870                if frame.frozen_arrays.contains(k) {
1871                    captured.push((format!("@frozen:{}", k), PerlValue::array(v.clone())));
1872                } else {
1873                    captured.push((format!("@{}", k), PerlValue::array(v.clone())));
1874                }
1875            }
1876            for (k, v) in &frame.hashes {
1877                if capture_skip_bootstrap_hash(k) {
1878                    continue;
1879                }
1880                if frame.frozen_hashes.contains(k) {
1881                    captured.push((format!("%frozen:{}", k), PerlValue::hash(v.clone())));
1882                } else {
1883                    captured.push((format!("%{}", k), PerlValue::hash(v.clone())));
1884                }
1885            }
1886            for (k, _aa) in &frame.atomic_arrays {
1887                captured.push((
1888                    format!("@sync_{}", k),
1889                    PerlValue::atomic(Arc::new(Mutex::new(PerlValue::string(String::new())))),
1890                ));
1891            }
1892            for (k, _ah) in &frame.atomic_hashes {
1893                captured.push((
1894                    format!("%sync_{}", k),
1895                    PerlValue::atomic(Arc::new(Mutex::new(PerlValue::string(String::new())))),
1896                ));
1897            }
1898        }
1899        captured
1900    }
1901
1902    /// Extended capture that returns atomic arrays/hashes separately.
1903    pub fn capture_with_atomics(&self) -> ScopeCaptureWithAtomics {
1904        let mut scalars = Vec::new();
1905        let mut arrays = Vec::new();
1906        let mut hashes = Vec::new();
1907        for frame in &self.frames {
1908            for (k, v) in &frame.scalars {
1909                scalars.push((format!("${}", k), v.clone()));
1910            }
1911            for (i, v) in frame.scalar_slots.iter().enumerate() {
1912                if let Some(Some(name)) = frame.scalar_slot_names.get(i) {
1913                    scalars.push((format!("$slot:{}:{}", i, name), v.clone()));
1914                }
1915            }
1916            for (k, v) in &frame.arrays {
1917                if capture_skip_bootstrap_array(k) {
1918                    continue;
1919                }
1920                if frame.frozen_arrays.contains(k) {
1921                    scalars.push((format!("@frozen:{}", k), PerlValue::array(v.clone())));
1922                } else {
1923                    scalars.push((format!("@{}", k), PerlValue::array(v.clone())));
1924                }
1925            }
1926            for (k, v) in &frame.hashes {
1927                if capture_skip_bootstrap_hash(k) {
1928                    continue;
1929                }
1930                if frame.frozen_hashes.contains(k) {
1931                    scalars.push((format!("%frozen:{}", k), PerlValue::hash(v.clone())));
1932                } else {
1933                    scalars.push((format!("%{}", k), PerlValue::hash(v.clone())));
1934                }
1935            }
1936            for (k, aa) in &frame.atomic_arrays {
1937                arrays.push((k.clone(), aa.clone()));
1938            }
1939            for (k, ah) in &frame.atomic_hashes {
1940                hashes.push((k.clone(), ah.clone()));
1941            }
1942        }
1943        (scalars, arrays, hashes)
1944    }
1945
1946    pub fn restore_capture(&mut self, captured: &[(String, PerlValue)]) {
1947        for (name, val) in captured {
1948            if let Some(rest) = name.strip_prefix("$slot:") {
1949                // "$slot:INDEX:NAME" — restore into both scalar_slots and scalars.
1950                if let Some(colon) = rest.find(':') {
1951                    let idx: usize = rest[..colon].parse().unwrap_or(0);
1952                    let sname = &rest[colon + 1..];
1953                    self.declare_scalar_slot(idx as u8, val.clone(), Some(sname));
1954                    self.declare_scalar(sname, val.clone());
1955                }
1956            } else if let Some(stripped) = name.strip_prefix('$') {
1957                self.declare_scalar(stripped, val.clone());
1958            } else if let Some(rest) = name.strip_prefix("@frozen:") {
1959                let arr = val.as_array_vec().unwrap_or_else(|| val.to_list());
1960                self.declare_array_frozen(rest, arr, true);
1961            } else if let Some(rest) = name.strip_prefix("%frozen:") {
1962                if let Some(h) = val.as_hash_map() {
1963                    self.declare_hash_frozen(rest, h.clone(), true);
1964                }
1965            } else if let Some(rest) = name.strip_prefix('@') {
1966                if rest.starts_with("sync_") {
1967                    continue;
1968                }
1969                let arr = val.as_array_vec().unwrap_or_else(|| val.to_list());
1970                self.declare_array(rest, arr);
1971            } else if let Some(rest) = name.strip_prefix('%') {
1972                if rest.starts_with("sync_") {
1973                    continue;
1974                }
1975                if let Some(h) = val.as_hash_map() {
1976                    self.declare_hash(rest, h.clone());
1977                }
1978            }
1979        }
1980    }
1981
1982    /// Restore atomic arrays/hashes from capture_with_atomics.
1983    pub fn restore_atomics(
1984        &mut self,
1985        arrays: &[(String, AtomicArray)],
1986        hashes: &[(String, AtomicHash)],
1987    ) {
1988        if let Some(frame) = self.frames.last_mut() {
1989            for (name, aa) in arrays {
1990                frame.atomic_arrays.push((name.clone(), aa.clone()));
1991            }
1992            for (name, ah) in hashes {
1993                frame.atomic_hashes.push((name.clone(), ah.clone()));
1994            }
1995        }
1996    }
1997}
1998
1999#[cfg(test)]
2000mod tests {
2001    use super::*;
2002    use crate::value::PerlValue;
2003
2004    #[test]
2005    fn missing_scalar_is_undef() {
2006        let s = Scope::new();
2007        assert!(s.get_scalar("not_declared").is_undef());
2008    }
2009
2010    #[test]
2011    fn inner_frame_shadows_outer_scalar() {
2012        let mut s = Scope::new();
2013        s.declare_scalar("a", PerlValue::integer(1));
2014        s.push_frame();
2015        s.declare_scalar("a", PerlValue::integer(2));
2016        assert_eq!(s.get_scalar("a").to_int(), 2);
2017        s.pop_frame();
2018        assert_eq!(s.get_scalar("a").to_int(), 1);
2019    }
2020
2021    #[test]
2022    fn set_scalar_updates_innermost_binding() {
2023        let mut s = Scope::new();
2024        s.declare_scalar("a", PerlValue::integer(1));
2025        s.push_frame();
2026        s.declare_scalar("a", PerlValue::integer(2));
2027        let _ = s.set_scalar("a", PerlValue::integer(99));
2028        assert_eq!(s.get_scalar("a").to_int(), 99);
2029        s.pop_frame();
2030        assert_eq!(s.get_scalar("a").to_int(), 1);
2031    }
2032
2033    #[test]
2034    fn array_negative_index_reads_from_end() {
2035        let mut s = Scope::new();
2036        s.declare_array(
2037            "a",
2038            vec![
2039                PerlValue::integer(10),
2040                PerlValue::integer(20),
2041                PerlValue::integer(30),
2042            ],
2043        );
2044        assert_eq!(s.get_array_element("a", -1).to_int(), 30);
2045    }
2046
2047    #[test]
2048    fn set_array_element_extends_array_with_undef_gaps() {
2049        let mut s = Scope::new();
2050        s.declare_array("a", vec![]);
2051        s.set_array_element("a", 2, PerlValue::integer(7)).unwrap();
2052        assert_eq!(s.get_array_element("a", 2).to_int(), 7);
2053        assert!(s.get_array_element("a", 0).is_undef());
2054    }
2055
2056    #[test]
2057    fn capture_restore_roundtrip_scalar() {
2058        let mut s = Scope::new();
2059        s.declare_scalar("n", PerlValue::integer(42));
2060        let cap = s.capture();
2061        let mut t = Scope::new();
2062        t.restore_capture(&cap);
2063        assert_eq!(t.get_scalar("n").to_int(), 42);
2064    }
2065
2066    #[test]
2067    fn capture_restore_roundtrip_lexical_array_and_hash() {
2068        let mut s = Scope::new();
2069        s.declare_array("a", vec![PerlValue::integer(1), PerlValue::integer(2)]);
2070        let mut m = IndexMap::new();
2071        m.insert("k".to_string(), PerlValue::integer(99));
2072        s.declare_hash("h", m);
2073        let cap = s.capture();
2074        let mut t = Scope::new();
2075        t.restore_capture(&cap);
2076        assert_eq!(t.get_array_element("a", 1).to_int(), 2);
2077        assert_eq!(t.get_hash_element("h", "k").to_int(), 99);
2078    }
2079
2080    #[test]
2081    fn hash_get_set_delete_exists() {
2082        let mut s = Scope::new();
2083        let mut m = IndexMap::new();
2084        m.insert("k".to_string(), PerlValue::integer(1));
2085        s.declare_hash("h", m);
2086        assert_eq!(s.get_hash_element("h", "k").to_int(), 1);
2087        assert!(s.exists_hash_element("h", "k"));
2088        s.set_hash_element("h", "k", PerlValue::integer(99))
2089            .unwrap();
2090        assert_eq!(s.get_hash_element("h", "k").to_int(), 99);
2091        let del = s.delete_hash_element("h", "k").unwrap();
2092        assert_eq!(del.to_int(), 99);
2093        assert!(!s.exists_hash_element("h", "k"));
2094    }
2095
2096    #[test]
2097    fn inner_frame_shadows_outer_hash_name() {
2098        let mut s = Scope::new();
2099        let mut outer = IndexMap::new();
2100        outer.insert("k".to_string(), PerlValue::integer(1));
2101        s.declare_hash("h", outer);
2102        s.push_frame();
2103        let mut inner = IndexMap::new();
2104        inner.insert("k".to_string(), PerlValue::integer(2));
2105        s.declare_hash("h", inner);
2106        assert_eq!(s.get_hash_element("h", "k").to_int(), 2);
2107        s.pop_frame();
2108        assert_eq!(s.get_hash_element("h", "k").to_int(), 1);
2109    }
2110
2111    #[test]
2112    fn inner_frame_shadows_outer_array_name() {
2113        let mut s = Scope::new();
2114        s.declare_array("a", vec![PerlValue::integer(1)]);
2115        s.push_frame();
2116        s.declare_array("a", vec![PerlValue::integer(2), PerlValue::integer(3)]);
2117        assert_eq!(s.get_array_element("a", 1).to_int(), 3);
2118        s.pop_frame();
2119        assert_eq!(s.get_array_element("a", 0).to_int(), 1);
2120    }
2121
2122    #[test]
2123    fn pop_frame_never_removes_global_frame() {
2124        let mut s = Scope::new();
2125        s.declare_scalar("x", PerlValue::integer(1));
2126        s.pop_frame();
2127        s.pop_frame();
2128        assert_eq!(s.get_scalar("x").to_int(), 1);
2129    }
2130
2131    #[test]
2132    fn empty_array_declared_has_zero_length() {
2133        let mut s = Scope::new();
2134        s.declare_array("a", vec![]);
2135        assert_eq!(s.get_array("a").len(), 0);
2136    }
2137
2138    #[test]
2139    fn depth_increments_with_push_frame() {
2140        let mut s = Scope::new();
2141        let d0 = s.depth();
2142        s.push_frame();
2143        assert_eq!(s.depth(), d0 + 1);
2144        s.pop_frame();
2145        assert_eq!(s.depth(), d0);
2146    }
2147
2148    #[test]
2149    fn pop_to_depth_unwinds_to_target() {
2150        let mut s = Scope::new();
2151        s.push_frame();
2152        s.push_frame();
2153        let target = s.depth() - 1;
2154        s.pop_to_depth(target);
2155        assert_eq!(s.depth(), target);
2156    }
2157
2158    #[test]
2159    fn array_len_and_push_pop_roundtrip() {
2160        let mut s = Scope::new();
2161        s.declare_array("a", vec![]);
2162        assert_eq!(s.array_len("a"), 0);
2163        s.push_to_array("a", PerlValue::integer(1)).unwrap();
2164        s.push_to_array("a", PerlValue::integer(2)).unwrap();
2165        assert_eq!(s.array_len("a"), 2);
2166        assert_eq!(s.pop_from_array("a").unwrap().to_int(), 2);
2167        assert_eq!(s.pop_from_array("a").unwrap().to_int(), 1);
2168        assert!(s.pop_from_array("a").unwrap().is_undef());
2169    }
2170
2171    #[test]
2172    fn shift_from_array_drops_front() {
2173        let mut s = Scope::new();
2174        s.declare_array("a", vec![PerlValue::integer(1), PerlValue::integer(2)]);
2175        assert_eq!(s.shift_from_array("a").unwrap().to_int(), 1);
2176        assert_eq!(s.array_len("a"), 1);
2177    }
2178
2179    #[test]
2180    fn atomic_mutate_increments_wrapped_scalar() {
2181        use parking_lot::Mutex;
2182        use std::sync::Arc;
2183        let mut s = Scope::new();
2184        s.declare_scalar(
2185            "n",
2186            PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(10)))),
2187        );
2188        let v = s.atomic_mutate("n", |old| PerlValue::integer(old.to_int() + 5));
2189        assert_eq!(v.to_int(), 15);
2190        assert_eq!(s.get_scalar("n").to_int(), 15);
2191    }
2192
2193    #[test]
2194    fn atomic_mutate_post_returns_old_value() {
2195        use parking_lot::Mutex;
2196        use std::sync::Arc;
2197        let mut s = Scope::new();
2198        s.declare_scalar(
2199            "n",
2200            PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(7)))),
2201        );
2202        let old = s.atomic_mutate_post("n", |v| PerlValue::integer(v.to_int() + 1));
2203        assert_eq!(old.to_int(), 7);
2204        assert_eq!(s.get_scalar("n").to_int(), 8);
2205    }
2206
2207    #[test]
2208    fn get_scalar_raw_keeps_atomic_wrapper() {
2209        use parking_lot::Mutex;
2210        use std::sync::Arc;
2211        let mut s = Scope::new();
2212        s.declare_scalar(
2213            "n",
2214            PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(3)))),
2215        );
2216        assert!(s.get_scalar_raw("n").is_atomic());
2217        assert!(!s.get_scalar("n").is_atomic());
2218    }
2219
2220    #[test]
2221    fn missing_array_element_is_undef() {
2222        let mut s = Scope::new();
2223        s.declare_array("a", vec![PerlValue::integer(1)]);
2224        assert!(s.get_array_element("a", 99).is_undef());
2225    }
2226
2227    #[test]
2228    fn restore_atomics_puts_atomic_containers_in_frame() {
2229        use indexmap::IndexMap;
2230        use parking_lot::Mutex;
2231        use std::sync::Arc;
2232        let mut s = Scope::new();
2233        let aa = AtomicArray(Arc::new(Mutex::new(vec![PerlValue::integer(1)])));
2234        let ah = AtomicHash(Arc::new(Mutex::new(IndexMap::new())));
2235        s.restore_atomics(&[("ax".into(), aa.clone())], &[("hx".into(), ah.clone())]);
2236        assert_eq!(s.get_array_element("ax", 0).to_int(), 1);
2237        assert_eq!(s.array_len("ax"), 1);
2238        s.set_hash_element("hx", "k", PerlValue::integer(2))
2239            .unwrap();
2240        assert_eq!(s.get_hash_element("hx", "k").to_int(), 2);
2241    }
2242}