Skip to main content

stryke/
value.rs

1use crossbeam::channel::{Receiver, Sender};
2use indexmap::IndexMap;
3use parking_lot::{Mutex, RwLock};
4use std::cmp::Ordering;
5use std::collections::VecDeque;
6use std::fmt;
7use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
8use std::sync::Arc;
9use std::sync::Barrier;
10
11use crate::ast::{Block, ClassDef, EnumDef, StructDef, SubSigParam};
12use crate::error::PerlResult;
13use crate::nanbox;
14use crate::perl_decode::decode_utf8_or_latin1;
15use crate::perl_regex::PerlCompiledRegex;
16
17/// Handle returned by `async { ... }` / `spawn { ... }`; join with `await`.
18#[derive(Debug)]
19pub struct PerlAsyncTask {
20    pub(crate) result: Arc<Mutex<Option<PerlResult<PerlValue>>>>,
21    pub(crate) join: Arc<Mutex<Option<std::thread::JoinHandle<()>>>>,
22}
23
24impl Clone for PerlAsyncTask {
25    fn clone(&self) -> Self {
26        Self {
27            result: self.result.clone(),
28            join: self.join.clone(),
29        }
30    }
31}
32
33impl PerlAsyncTask {
34    /// Join the worker thread (once) and return the block's value or error.
35    pub fn await_result(&self) -> PerlResult<PerlValue> {
36        if let Some(h) = self.join.lock().take() {
37            let _ = h.join();
38        }
39        self.result
40            .lock()
41            .clone()
42            .unwrap_or_else(|| Ok(PerlValue::UNDEF))
43    }
44}
45
46// ── Lazy iterator protocol (`|>` streaming) ─────────────────────────────────
47
48/// Pull-based lazy iterator.  Sources (`frs`, `drs`) produce one; transform
49/// stages (`rev`) wrap one; terminals (`e`/`fore`) consume one item at a time.
50pub trait PerlIterator: Send + Sync {
51    /// Return the next item, or `None` when exhausted.
52    fn next_item(&self) -> Option<PerlValue>;
53
54    /// Collect all remaining items into a `Vec`.
55    fn collect_all(&self) -> Vec<PerlValue> {
56        let mut out = Vec::new();
57        while let Some(v) = self.next_item() {
58            out.push(v);
59        }
60        out
61    }
62}
63
64impl fmt::Debug for dyn PerlIterator {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str("PerlIterator")
67    }
68}
69
70/// Lazy recursive file walker — yields one relative path per `next_item()` call.
71pub struct FsWalkIterator {
72    /// `(base_path, relative_prefix)` stack.
73    stack: Mutex<Vec<(std::path::PathBuf, String)>>,
74    /// Buffered sorted entries from the current directory level.
75    buf: Mutex<Vec<(String, bool)>>, // (child_rel, is_dir)
76    /// Pending subdirs to push (reversed, so first is popped next).
77    pending_dirs: Mutex<Vec<(std::path::PathBuf, String)>>,
78    files_only: bool,
79}
80
81impl FsWalkIterator {
82    pub fn new(dir: &str, files_only: bool) -> Self {
83        Self {
84            stack: Mutex::new(vec![(std::path::PathBuf::from(dir), String::new())]),
85            buf: Mutex::new(Vec::new()),
86            pending_dirs: Mutex::new(Vec::new()),
87            files_only,
88        }
89    }
90
91    /// Refill `buf` from the next directory on the stack.
92    /// Loops until items are found or the stack is fully exhausted.
93    fn refill(&self) -> bool {
94        loop {
95            let mut stack = self.stack.lock();
96            // Push any pending subdirs from the previous level.
97            let mut pending = self.pending_dirs.lock();
98            while let Some(d) = pending.pop() {
99                stack.push(d);
100            }
101            drop(pending);
102
103            let (base, rel) = match stack.pop() {
104                Some(v) => v,
105                None => return false,
106            };
107            drop(stack);
108
109            let entries = match std::fs::read_dir(&base) {
110                Ok(e) => e,
111                Err(_) => continue, // skip unreadable, try next
112            };
113            let mut children: Vec<(std::ffi::OsString, String, bool, bool)> = Vec::new();
114            for entry in entries.flatten() {
115                let ft = match entry.file_type() {
116                    Ok(ft) => ft,
117                    Err(_) => continue,
118                };
119                let os_name = entry.file_name();
120                let name = match os_name.to_str() {
121                    Some(n) => n.to_string(),
122                    None => continue,
123                };
124                let child_rel = if rel.is_empty() {
125                    name.clone()
126                } else {
127                    format!("{rel}/{name}")
128                };
129                children.push((os_name, child_rel, ft.is_file(), ft.is_dir()));
130            }
131            children.sort_by(|a, b| a.0.cmp(&b.0));
132
133            let mut buf = self.buf.lock();
134            let mut pending = self.pending_dirs.lock();
135            let mut subdirs = Vec::new();
136            for (os_name, child_rel, is_file, is_dir) in children {
137                if is_dir {
138                    if !self.files_only {
139                        buf.push((child_rel.clone(), true));
140                    }
141                    subdirs.push((base.join(os_name), child_rel));
142                } else if is_file && self.files_only {
143                    buf.push((child_rel, false));
144                }
145            }
146            for s in subdirs.into_iter().rev() {
147                pending.push(s);
148            }
149            buf.reverse();
150            if !buf.is_empty() {
151                return true;
152            }
153            // buf empty but pending_dirs may have subdirs to explore — loop.
154        }
155    }
156}
157
158impl PerlIterator for FsWalkIterator {
159    fn next_item(&self) -> Option<PerlValue> {
160        loop {
161            {
162                let mut buf = self.buf.lock();
163                if let Some((path, _)) = buf.pop() {
164                    return Some(PerlValue::string(path));
165                }
166            }
167            if !self.refill() {
168                return None;
169            }
170        }
171    }
172}
173
174/// Wraps a source iterator, applying `scalar reverse` (char-reverse) to each string.
175pub struct RevIterator {
176    source: Arc<dyn PerlIterator>,
177}
178
179impl RevIterator {
180    pub fn new(source: Arc<dyn PerlIterator>) -> Self {
181        Self { source }
182    }
183}
184
185impl PerlIterator for RevIterator {
186    fn next_item(&self) -> Option<PerlValue> {
187        let item = self.source.next_item()?;
188        let s = item.to_string();
189        Some(PerlValue::string(s.chars().rev().collect()))
190    }
191}
192
193/// Lazy generator from `gen { }`; resume with `->next` on the value.
194#[derive(Debug)]
195pub struct PerlGenerator {
196    pub(crate) block: Block,
197    pub(crate) pc: Mutex<usize>,
198    pub(crate) scope_started: Mutex<bool>,
199    pub(crate) exhausted: Mutex<bool>,
200}
201
202/// `Set->new` storage: canonical key → member value (insertion order preserved).
203pub type PerlSet = IndexMap<String, PerlValue>;
204
205/// Min-heap ordered by a Perl comparator (`$a` / `$b` in scope, like `sort { }`).
206#[derive(Debug, Clone)]
207pub struct PerlHeap {
208    pub items: Vec<PerlValue>,
209    pub cmp: Arc<PerlSub>,
210}
211
212/// One SSH worker lane: a single `ssh HOST PE_PATH --remote-worker` process. The persistent
213/// dispatcher in [`crate::cluster`] holds one of these per concurrent worker thread.
214///
215/// `pe_path` is the path to the `stryke` binary on the **remote** host — the basic implementation
216/// used `std::env::current_exe()` which is wrong by definition (a local `/Users/...` path
217/// rarely exists on a remote machine). Default is the bare string `"stryke"` so the remote
218/// host's `$PATH` resolves it like any other ssh command.
219#[derive(Debug, Clone)]
220pub struct RemoteSlot {
221    /// Argument passed to `ssh` (e.g. `host`, `user@host`, `host` with `~/.ssh/config` host alias).
222    pub host: String,
223    /// Path to `stryke` on the remote host. `"stryke"` resolves via remote `$PATH`.
224    pub pe_path: String,
225}
226
227#[cfg(test)]
228mod cluster_parsing_tests {
229    use super::*;
230
231    fn s(v: &str) -> PerlValue {
232        PerlValue::string(v.to_string())
233    }
234
235    #[test]
236    fn parses_simple_host() {
237        let c = RemoteCluster::from_list_args(&[s("host1")]).expect("parse");
238        assert_eq!(c.slots.len(), 1);
239        assert_eq!(c.slots[0].host, "host1");
240        assert_eq!(c.slots[0].pe_path, "stryke");
241    }
242
243    #[test]
244    fn parses_host_with_slot_count() {
245        let c = RemoteCluster::from_list_args(&[s("host1:4")]).expect("parse");
246        assert_eq!(c.slots.len(), 4);
247        assert!(c.slots.iter().all(|s| s.host == "host1"));
248    }
249
250    #[test]
251    fn parses_user_at_host_with_slots() {
252        let c = RemoteCluster::from_list_args(&[s("alice@build1:2")]).expect("parse");
253        assert_eq!(c.slots.len(), 2);
254        assert_eq!(c.slots[0].host, "alice@build1");
255    }
256
257    #[test]
258    fn parses_host_slots_stryke_path_triple() {
259        let c =
260            RemoteCluster::from_list_args(&[s("build1:3:/usr/local/bin/stryke")]).expect("parse");
261        assert_eq!(c.slots.len(), 3);
262        assert!(c.slots.iter().all(|sl| sl.host == "build1"));
263        assert!(c
264            .slots
265            .iter()
266            .all(|sl| sl.pe_path == "/usr/local/bin/stryke"));
267    }
268
269    #[test]
270    fn parses_multiple_hosts_in_one_call() {
271        let c = RemoteCluster::from_list_args(&[s("host1:2"), s("host2:1")]).expect("parse");
272        assert_eq!(c.slots.len(), 3);
273        assert_eq!(c.slots[0].host, "host1");
274        assert_eq!(c.slots[1].host, "host1");
275        assert_eq!(c.slots[2].host, "host2");
276    }
277
278    #[test]
279    fn parses_hashref_slot_form() {
280        let mut h = indexmap::IndexMap::new();
281        h.insert("host".to_string(), s("data1"));
282        h.insert("slots".to_string(), PerlValue::integer(2));
283        h.insert("stryke".to_string(), s("/opt/stryke"));
284        let c = RemoteCluster::from_list_args(&[PerlValue::hash(h)]).expect("parse");
285        assert_eq!(c.slots.len(), 2);
286        assert_eq!(c.slots[0].host, "data1");
287        assert_eq!(c.slots[0].pe_path, "/opt/stryke");
288    }
289
290    #[test]
291    fn parses_trailing_tunables_hashref() {
292        let mut tun = indexmap::IndexMap::new();
293        tun.insert("timeout".to_string(), PerlValue::integer(30));
294        tun.insert("retries".to_string(), PerlValue::integer(2));
295        tun.insert("connect_timeout".to_string(), PerlValue::integer(5));
296        let c = RemoteCluster::from_list_args(&[s("h1:1"), PerlValue::hash(tun)]).expect("parse");
297        // Tunables hash should NOT be treated as a slot.
298        assert_eq!(c.slots.len(), 1);
299        assert_eq!(c.job_timeout_ms, 30_000);
300        assert_eq!(c.max_attempts, 3); // retries=2 + initial = 3
301        assert_eq!(c.connect_timeout_ms, 5_000);
302    }
303
304    #[test]
305    fn defaults_when_no_tunables() {
306        let c = RemoteCluster::from_list_args(&[s("h1")]).expect("parse");
307        assert_eq!(c.job_timeout_ms, RemoteCluster::DEFAULT_JOB_TIMEOUT_MS);
308        assert_eq!(c.max_attempts, RemoteCluster::DEFAULT_MAX_ATTEMPTS);
309        assert_eq!(
310            c.connect_timeout_ms,
311            RemoteCluster::DEFAULT_CONNECT_TIMEOUT_MS
312        );
313    }
314
315    #[test]
316    fn rejects_empty_cluster() {
317        assert!(RemoteCluster::from_list_args(&[]).is_err());
318    }
319
320    #[test]
321    fn slot_count_minimum_one() {
322        let c = RemoteCluster::from_list_args(&[s("h1:0")]).expect("parse");
323        // `host:0` clamps to 1 slot — better to give the user something than to silently
324        // produce a cluster that does nothing.
325        assert_eq!(c.slots.len(), 1);
326    }
327}
328
329/// SSH worker pool for `pmap_on`. The dispatcher spawns one persistent ssh process per slot,
330/// performs HELLO + SESSION_INIT once, then streams JOB frames over the same stdin/stdout.
331///
332/// **Tunables:**
333/// - `job_timeout_ms` — per-job wall-clock budget. A slot that exceeds this is killed and the
334///   job is re-enqueued (counted against the retry budget).
335/// - `max_attempts` — total attempts (initial + retries) per job before it is failed.
336/// - `connect_timeout_ms` — `ssh -o ConnectTimeout=N`-equivalent for the initial handshake.
337#[derive(Debug, Clone)]
338pub struct RemoteCluster {
339    pub slots: Vec<RemoteSlot>,
340    pub job_timeout_ms: u64,
341    pub max_attempts: u32,
342    pub connect_timeout_ms: u64,
343}
344
345impl RemoteCluster {
346    pub const DEFAULT_JOB_TIMEOUT_MS: u64 = 60_000;
347    pub const DEFAULT_MAX_ATTEMPTS: u32 = 3;
348    pub const DEFAULT_CONNECT_TIMEOUT_MS: u64 = 10_000;
349
350    /// Parse a list of cluster spec values into a [`RemoteCluster`]. Accepted forms (any may
351    /// appear in the same call):
352    ///
353    /// - `"host"`                       — 1 slot, default `stryke` path
354    /// - `"host:N"`                     — N slots
355    /// - `"host:N:/path/to/stryke"`         — N slots, custom remote `stryke`
356    /// - `"user@host:N"`                — ssh user override (kept verbatim in `host`)
357    /// - hashref `{ host => "h", slots => N, stryke => "/usr/local/bin/stryke" }`
358    /// - trailing hashref `{ timeout => 30, retries => 2, connect_timeout => 5 }` — global
359    ///   tunables that apply to the whole cluster (must be the **last** argument; consumed
360    ///   only when its keys are all known tunable names so it cannot be confused with a slot)
361    ///
362    /// Backwards compatible with the basic v1 `"host:N"` syntax.
363    pub fn from_list_args(items: &[PerlValue]) -> Result<Self, String> {
364        let mut slots: Vec<RemoteSlot> = Vec::new();
365        let mut job_timeout_ms = Self::DEFAULT_JOB_TIMEOUT_MS;
366        let mut max_attempts = Self::DEFAULT_MAX_ATTEMPTS;
367        let mut connect_timeout_ms = Self::DEFAULT_CONNECT_TIMEOUT_MS;
368
369        // Trailing tunable hashref: peel it off if all its keys are known tunable names.
370        let (slot_items, tunables) = if let Some(last) = items.last() {
371            let h = last
372                .as_hash_map()
373                .or_else(|| last.as_hash_ref().map(|r| r.read().clone()));
374            if let Some(map) = h {
375                let known = |k: &str| {
376                    matches!(k, "timeout" | "retries" | "connect_timeout" | "job_timeout")
377                };
378                if !map.is_empty() && map.keys().all(|k| known(k.as_str())) {
379                    (&items[..items.len() - 1], Some(map))
380                } else {
381                    (items, None)
382                }
383            } else {
384                (items, None)
385            }
386        } else {
387            (items, None)
388        };
389
390        if let Some(map) = tunables {
391            if let Some(v) = map.get("timeout").or_else(|| map.get("job_timeout")) {
392                job_timeout_ms = (v.to_number() * 1000.0) as u64;
393            }
394            if let Some(v) = map.get("retries") {
395                // `retries=2` means 2 RETRIES on top of the first attempt → 3 total.
396                max_attempts = v.to_int().max(0) as u32 + 1;
397            }
398            if let Some(v) = map.get("connect_timeout") {
399                connect_timeout_ms = (v.to_number() * 1000.0) as u64;
400            }
401        }
402
403        for it in slot_items {
404            // Hashref form: { host => "h", slots => N, stryke => "/path" }
405            if let Some(map) = it
406                .as_hash_map()
407                .or_else(|| it.as_hash_ref().map(|r| r.read().clone()))
408            {
409                let host = map
410                    .get("host")
411                    .map(|v| v.to_string())
412                    .ok_or_else(|| "cluster: hashref slot needs `host`".to_string())?;
413                let n = map.get("slots").map(|v| v.to_int().max(1)).unwrap_or(1) as usize;
414                let stryke = map
415                    .get("stryke")
416                    .or_else(|| map.get("pe_path"))
417                    .map(|v| v.to_string())
418                    .unwrap_or_else(|| "stryke".to_string());
419                for _ in 0..n {
420                    slots.push(RemoteSlot {
421                        host: host.clone(),
422                        pe_path: stryke.clone(),
423                    });
424                }
425                continue;
426            }
427
428            // String form. Split into up to 3 colon-separated fields, but be careful: a
429            // pe_path may itself contain a colon (rare but possible). We use rsplitn(2) to
430            // peel off the optional stryke path only when the segment after the second colon
431            // looks like a path (starts with `/` or `.`) — otherwise treat the trailing
432            // segment as part of the stryke path candidate.
433            let s = it.to_string();
434            // Heuristic: split into (left = host[:N], pe_path) if the third field is present.
435            let (left, pe_path) = if let Some(idx) = s.find(':') {
436                // first colon is host:rest
437                let rest = &s[idx + 1..];
438                if let Some(jdx) = rest.find(':') {
439                    // host:N:pe_path
440                    let count_seg = &rest[..jdx];
441                    if count_seg.parse::<usize>().is_ok() {
442                        (
443                            format!("{}:{}", &s[..idx], count_seg),
444                            Some(rest[jdx + 1..].to_string()),
445                        )
446                    } else {
447                        (s.clone(), None)
448                    }
449                } else {
450                    (s.clone(), None)
451                }
452            } else {
453                (s.clone(), None)
454            };
455            let pe_path = pe_path.unwrap_or_else(|| "stryke".to_string());
456
457            // Now `left` is either `host` or `host:N`. The N suffix is digits only, so
458            // `user@host` (which contains `@` but no trailing `:digits`) is preserved.
459            let (host, n) = if let Some((h, nstr)) = left.rsplit_once(':') {
460                if let Ok(n) = nstr.parse::<usize>() {
461                    (h.to_string(), n.max(1))
462                } else {
463                    (left.clone(), 1)
464                }
465            } else {
466                (left.clone(), 1)
467            };
468            for _ in 0..n {
469                slots.push(RemoteSlot {
470                    host: host.clone(),
471                    pe_path: pe_path.clone(),
472                });
473            }
474        }
475
476        if slots.is_empty() {
477            return Err("cluster: need at least one host".into());
478        }
479        Ok(RemoteCluster {
480            slots,
481            job_timeout_ms,
482            max_attempts,
483            connect_timeout_ms,
484        })
485    }
486}
487
488/// `barrier(N)` — `std::sync::Barrier` for phased parallelism (`->wait`).
489#[derive(Clone)]
490pub struct PerlBarrier(pub Arc<Barrier>);
491
492impl fmt::Debug for PerlBarrier {
493    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
494        f.write_str("Barrier")
495    }
496}
497
498/// Structured stdout/stderr/exit from `capture("cmd")`.
499#[derive(Debug, Clone)]
500pub struct CaptureResult {
501    pub stdout: String,
502    pub stderr: String,
503    pub exitcode: i64,
504}
505
506/// Columnar table from `dataframe(path)`; chain `filter`, `group_by`, `sum`, `nrow`.
507#[derive(Debug, Clone)]
508pub struct PerlDataFrame {
509    pub columns: Vec<String>,
510    pub cols: Vec<Vec<PerlValue>>,
511    /// When set, `sum(col)` aggregates rows by this column.
512    pub group_by: Option<String>,
513}
514
515impl PerlDataFrame {
516    #[inline]
517    pub fn nrows(&self) -> usize {
518        self.cols.first().map(|c| c.len()).unwrap_or(0)
519    }
520
521    #[inline]
522    pub fn ncols(&self) -> usize {
523        self.columns.len()
524    }
525
526    #[inline]
527    pub fn col_index(&self, name: &str) -> Option<usize> {
528        self.columns.iter().position(|c| c == name)
529    }
530}
531
532/// Heap payload when [`PerlValue`] is not an immediate or raw [`f64`] bits.
533#[derive(Debug, Clone)]
534pub(crate) enum HeapObject {
535    Integer(i64),
536    Float(f64),
537    String(String),
538    Bytes(Arc<Vec<u8>>),
539    Array(Vec<PerlValue>),
540    Hash(IndexMap<String, PerlValue>),
541    ArrayRef(Arc<RwLock<Vec<PerlValue>>>),
542    HashRef(Arc<RwLock<IndexMap<String, PerlValue>>>),
543    ScalarRef(Arc<RwLock<PerlValue>>),
544    /// Closure-capture cell: same Arc<RwLock> sharing as ScalarRef but transparently unwrapped
545    /// by [`crate::scope::Scope::get_scalar_slot`] and [`crate::scope::Scope::get_scalar`].
546    /// Created by [`crate::scope::Scope::capture`] to share lexical scalars between closures.
547    CaptureCell(Arc<RwLock<PerlValue>>),
548    /// `\\$name` when `name` is a plain scalar variable — aliases that binding (Perl ref to lexical).
549    ScalarBindingRef(String),
550    /// `\\@name` — aliases the live array in [`crate::scope::Scope`] (same stash key as [`Op::GetArray`]).
551    ArrayBindingRef(String),
552    /// `\\%name` — aliases the live hash in scope.
553    HashBindingRef(String),
554    CodeRef(Arc<PerlSub>),
555    /// Compiled regex: pattern source and flag chars (e.g. `"i"`, `"g"`) for re-match without re-parse.
556    Regex(Arc<PerlCompiledRegex>, String, String),
557    Blessed(Arc<BlessedRef>),
558    IOHandle(String),
559    Atomic(Arc<Mutex<PerlValue>>),
560    Set(Arc<PerlSet>),
561    ChannelTx(Arc<Sender<PerlValue>>),
562    ChannelRx(Arc<Receiver<PerlValue>>),
563    AsyncTask(Arc<PerlAsyncTask>),
564    Generator(Arc<PerlGenerator>),
565    Deque(Arc<Mutex<VecDeque<PerlValue>>>),
566    Heap(Arc<Mutex<PerlHeap>>),
567    Pipeline(Arc<Mutex<PipelineInner>>),
568    Capture(Arc<CaptureResult>),
569    Ppool(PerlPpool),
570    RemoteCluster(Arc<RemoteCluster>),
571    Barrier(PerlBarrier),
572    SqliteConn(Arc<Mutex<rusqlite::Connection>>),
573    StructInst(Arc<StructInstance>),
574    DataFrame(Arc<Mutex<PerlDataFrame>>),
575    EnumInst(Arc<EnumInstance>),
576    ClassInst(Arc<ClassInstance>),
577    /// Lazy pull-based iterator (`frs`, `drs`, `rev` wrapping, etc.).
578    Iterator(Arc<dyn PerlIterator>),
579    /// Numeric/string dualvar: **`$!`** (errno + message) and **`$@`** (numeric flag or code + message).
580    ErrnoDual {
581        code: i32,
582        msg: String,
583    },
584}
585
586/// NaN-boxed value: one `u64` (immediates, raw float bits, or tagged heap pointer).
587#[repr(transparent)]
588pub struct PerlValue(pub(crate) u64);
589
590impl Default for PerlValue {
591    fn default() -> Self {
592        Self::UNDEF
593    }
594}
595
596impl Clone for PerlValue {
597    fn clone(&self) -> Self {
598        if nanbox::is_heap(self.0) {
599            let arc = self.heap_arc();
600            match &*arc {
601                HeapObject::Array(v) => {
602                    PerlValue::from_heap(Arc::new(HeapObject::Array(v.clone())))
603                }
604                HeapObject::Hash(h) => PerlValue::from_heap(Arc::new(HeapObject::Hash(h.clone()))),
605                HeapObject::String(s) => {
606                    PerlValue::from_heap(Arc::new(HeapObject::String(s.clone())))
607                }
608                HeapObject::Integer(n) => PerlValue::integer(*n),
609                HeapObject::Float(f) => PerlValue::float(*f),
610                _ => PerlValue::from_heap(Arc::clone(&arc)),
611            }
612        } else {
613            PerlValue(self.0)
614        }
615    }
616}
617
618impl PerlValue {
619    /// Stack duplicate (`Op::Dup`): share the outer heap [`Arc`] for arrays/hashes (COW on write),
620    /// matching Perl temporaries; other heap payloads keep [`Clone`] semantics.
621    #[inline]
622    pub fn dup_stack(&self) -> Self {
623        if nanbox::is_heap(self.0) {
624            let arc = self.heap_arc();
625            match &*arc {
626                HeapObject::Array(_) | HeapObject::Hash(_) => {
627                    PerlValue::from_heap(Arc::clone(&arc))
628                }
629                _ => self.clone(),
630            }
631        } else {
632            PerlValue(self.0)
633        }
634    }
635
636    /// Refcount-only clone: `Arc::clone` the heap pointer (no deep copy of the payload).
637    ///
638    /// Use this when producing a *second handle* to the same value that the caller
639    /// will read-only or consume via [`Self::into_string`] / [`Arc::try_unwrap`]-style
640    /// uniqueness checks. Cheap O(1) regardless of the payload size.
641    ///
642    /// The default [`Clone`] impl deep-copies `String`/`Array`/`Hash` payloads to
643    /// preserve "clone = independent writable value" semantics for legacy callers;
644    /// in hot RMW paths (`.=`, slot stash-and-return) that deep copy is O(N) and
645    /// must be avoided — use this instead.
646    #[inline]
647    pub fn shallow_clone(&self) -> Self {
648        if nanbox::is_heap(self.0) {
649            PerlValue::from_heap(self.heap_arc())
650        } else {
651            PerlValue(self.0)
652        }
653    }
654}
655
656impl Drop for PerlValue {
657    fn drop(&mut self) {
658        if nanbox::is_heap(self.0) {
659            unsafe {
660                let p = nanbox::decode_heap_ptr::<HeapObject>(self.0) as *mut HeapObject;
661                drop(Arc::from_raw(p));
662            }
663        }
664    }
665}
666
667impl fmt::Debug for PerlValue {
668    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
669        write!(f, "{self}")
670    }
671}
672
673/// Handle returned by `ppool(N)`; use `->submit(CODE, $topic?)` and `->collect()`.
674/// One-arg `submit` copies the caller's `$_` into the worker (so postfix `for` works).
675#[derive(Clone)]
676pub struct PerlPpool(pub(crate) Arc<crate::ppool::PpoolInner>);
677
678impl fmt::Debug for PerlPpool {
679    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
680        f.write_str("PerlPpool")
681    }
682}
683
684/// See [`crate::fib_like_tail::detect_fib_like_recursive_add`] — iterative fast path for
685/// `return f($p-a)+f($p-b)` with a simple integer base case.
686#[derive(Debug, Clone, PartialEq, Eq)]
687pub struct FibLikeRecAddPattern {
688    /// Scalar from `my $p = shift` (e.g. `n`).
689    pub param: String,
690    /// `n <= base_k` ⇒ return `n`.
691    pub base_k: i64,
692    /// Left call uses `$param - left_k`.
693    pub left_k: i64,
694    /// Right call uses `$param - right_k`.
695    pub right_k: i64,
696}
697
698#[derive(Debug, Clone)]
699pub struct PerlSub {
700    pub name: String,
701    pub params: Vec<SubSigParam>,
702    pub body: Block,
703    /// Captured lexical scope (for closures)
704    pub closure_env: Option<Vec<(String, PerlValue)>>,
705    /// Prototype string from `sub name (PROTO) { }`, or `None`.
706    pub prototype: Option<String>,
707    /// When set, [`Interpreter::call_sub`](crate::interpreter::Interpreter::call_sub) may evaluate
708    /// this sub with an explicit stack instead of recursive scope frames.
709    pub fib_like: Option<FibLikeRecAddPattern>,
710}
711
712/// Operations queued on a [`PerlValue::pipeline`](crate::value::PerlValue::pipeline) value until `collect()`.
713#[derive(Debug, Clone)]
714pub enum PipelineOp {
715    Filter(Arc<PerlSub>),
716    Map(Arc<PerlSub>),
717    /// `tap` / `peek` — run block for side effects; `@_` is the current stage list; value unchanged.
718    Tap(Arc<PerlSub>),
719    Take(i64),
720    /// Parallel map (`pmap`) — optional stderr progress bar (same as `pmap ..., progress => 1`).
721    PMap {
722        sub: Arc<PerlSub>,
723        progress: bool,
724    },
725    /// Parallel grep (`pgrep`).
726    PGrep {
727        sub: Arc<PerlSub>,
728        progress: bool,
729    },
730    /// Parallel foreach (`pfor`) — side effects only; stream order preserved.
731    PFor {
732        sub: Arc<PerlSub>,
733        progress: bool,
734    },
735    /// `pmap_chunked N { }` — chunk size + block.
736    PMapChunked {
737        chunk: i64,
738        sub: Arc<PerlSub>,
739        progress: bool,
740    },
741    /// `psort` / `psort { $a <=> $b }` — parallel sort.
742    PSort {
743        cmp: Option<Arc<PerlSub>>,
744        progress: bool,
745    },
746    /// `pcache { }` — parallel memoized map.
747    PCache {
748        sub: Arc<PerlSub>,
749        progress: bool,
750    },
751    /// `preduce { }` — must be last before `collect()`; `collect()` returns a scalar.
752    PReduce {
753        sub: Arc<PerlSub>,
754        progress: bool,
755    },
756    /// `preduce_init EXPR, { }` — scalar result; must be last before `collect()`.
757    PReduceInit {
758        init: PerlValue,
759        sub: Arc<PerlSub>,
760        progress: bool,
761    },
762    /// `pmap_reduce { } { }` — scalar result; must be last before `collect()`.
763    PMapReduce {
764        map: Arc<PerlSub>,
765        reduce: Arc<PerlSub>,
766        progress: bool,
767    },
768}
769
770#[derive(Debug)]
771pub struct PipelineInner {
772    pub source: Vec<PerlValue>,
773    pub ops: Vec<PipelineOp>,
774    /// Set after `preduce` / `preduce_init` / `pmap_reduce` — no further `->` ops allowed.
775    pub has_scalar_terminal: bool,
776    /// When true (from `par_pipeline(LIST)`), `->filter` / `->map` run in parallel with **input order preserved** on `collect()`.
777    pub par_stream: bool,
778    /// When true (from `par_pipeline_stream(LIST)`), `collect()` wires ops through bounded
779    /// channels so items stream between stages concurrently (order **not** preserved).
780    pub streaming: bool,
781    /// Per-stage worker count for streaming mode (default: available parallelism).
782    pub streaming_workers: usize,
783    /// Bounded channel capacity for streaming mode (default: 256).
784    pub streaming_buffer: usize,
785}
786
787#[derive(Debug)]
788pub struct BlessedRef {
789    pub class: String,
790    pub data: RwLock<PerlValue>,
791    /// When true, dropping does not enqueue `DESTROY` (temporary invocant built while running a destructor).
792    pub(crate) suppress_destroy_queue: AtomicBool,
793}
794
795impl BlessedRef {
796    pub(crate) fn new_blessed(class: String, data: PerlValue) -> Self {
797        Self {
798            class,
799            data: RwLock::new(data),
800            suppress_destroy_queue: AtomicBool::new(false),
801        }
802    }
803
804    /// Invocant for a running `DESTROY` — must not re-queue when dropped after the call.
805    pub(crate) fn new_for_destroy_invocant(class: String, data: PerlValue) -> Self {
806        Self {
807            class,
808            data: RwLock::new(data),
809            suppress_destroy_queue: AtomicBool::new(true),
810        }
811    }
812}
813
814impl Clone for BlessedRef {
815    fn clone(&self) -> Self {
816        Self {
817            class: self.class.clone(),
818            data: RwLock::new(self.data.read().clone()),
819            suppress_destroy_queue: AtomicBool::new(false),
820        }
821    }
822}
823
824impl Drop for BlessedRef {
825    fn drop(&mut self) {
826        if self.suppress_destroy_queue.load(AtomicOrdering::Acquire) {
827            return;
828        }
829        let inner = {
830            let mut g = self.data.write();
831            std::mem::take(&mut *g)
832        };
833        crate::pending_destroy::enqueue(self.class.clone(), inner);
834    }
835}
836
837/// Instance of a `struct Name { ... }` definition; field access via `$obj->name`.
838#[derive(Debug)]
839pub struct StructInstance {
840    pub def: Arc<StructDef>,
841    pub values: RwLock<Vec<PerlValue>>,
842}
843
844impl StructInstance {
845    /// Create a new struct instance with the given definition and values.
846    pub fn new(def: Arc<StructDef>, values: Vec<PerlValue>) -> Self {
847        Self {
848            def,
849            values: RwLock::new(values),
850        }
851    }
852
853    /// Get a field value by index (clones the value).
854    #[inline]
855    pub fn get_field(&self, idx: usize) -> Option<PerlValue> {
856        self.values.read().get(idx).cloned()
857    }
858
859    /// Set a field value by index.
860    #[inline]
861    pub fn set_field(&self, idx: usize, val: PerlValue) {
862        if let Some(slot) = self.values.write().get_mut(idx) {
863            *slot = val;
864        }
865    }
866
867    /// Get all field values (clones the vector).
868    #[inline]
869    pub fn get_values(&self) -> Vec<PerlValue> {
870        self.values.read().clone()
871    }
872}
873
874impl Clone for StructInstance {
875    fn clone(&self) -> Self {
876        Self {
877            def: Arc::clone(&self.def),
878            values: RwLock::new(self.values.read().clone()),
879        }
880    }
881}
882
883/// Instance of an `enum Name { Variant ... }` definition.
884#[derive(Debug)]
885pub struct EnumInstance {
886    pub def: Arc<EnumDef>,
887    pub variant_idx: usize,
888    /// Data carried by this variant. For variants with no data, this is UNDEF.
889    pub data: PerlValue,
890}
891
892impl EnumInstance {
893    pub fn new(def: Arc<EnumDef>, variant_idx: usize, data: PerlValue) -> Self {
894        Self {
895            def,
896            variant_idx,
897            data,
898        }
899    }
900
901    pub fn variant_name(&self) -> &str {
902        &self.def.variants[self.variant_idx].name
903    }
904}
905
906impl Clone for EnumInstance {
907    fn clone(&self) -> Self {
908        Self {
909            def: Arc::clone(&self.def),
910            variant_idx: self.variant_idx,
911            data: self.data.clone(),
912        }
913    }
914}
915
916/// Instance of a `class Name extends ... impl ... { ... }` definition.
917#[derive(Debug)]
918pub struct ClassInstance {
919    pub def: Arc<ClassDef>,
920    pub values: RwLock<Vec<PerlValue>>,
921    /// Full ISA chain for this class (all ancestors, computed at instantiation).
922    pub isa_chain: Vec<String>,
923}
924
925impl ClassInstance {
926    pub fn new(def: Arc<ClassDef>, values: Vec<PerlValue>) -> Self {
927        Self {
928            def,
929            values: RwLock::new(values),
930            isa_chain: Vec::new(),
931        }
932    }
933
934    pub fn new_with_isa(
935        def: Arc<ClassDef>,
936        values: Vec<PerlValue>,
937        isa_chain: Vec<String>,
938    ) -> Self {
939        Self {
940            def,
941            values: RwLock::new(values),
942            isa_chain,
943        }
944    }
945
946    /// Check if this instance is-a given class name (direct or inherited).
947    #[inline]
948    pub fn isa(&self, name: &str) -> bool {
949        self.def.name == name || self.isa_chain.contains(&name.to_string())
950    }
951
952    #[inline]
953    pub fn get_field(&self, idx: usize) -> Option<PerlValue> {
954        self.values.read().get(idx).cloned()
955    }
956
957    #[inline]
958    pub fn set_field(&self, idx: usize, val: PerlValue) {
959        if let Some(slot) = self.values.write().get_mut(idx) {
960            *slot = val;
961        }
962    }
963
964    #[inline]
965    pub fn get_values(&self) -> Vec<PerlValue> {
966        self.values.read().clone()
967    }
968
969    /// Get field value by name (searches through class and parent hierarchies).
970    pub fn get_field_by_name(&self, name: &str) -> Option<PerlValue> {
971        self.def
972            .field_index(name)
973            .and_then(|idx| self.get_field(idx))
974    }
975
976    /// Set field value by name.
977    pub fn set_field_by_name(&self, name: &str, val: PerlValue) -> bool {
978        if let Some(idx) = self.def.field_index(name) {
979            self.set_field(idx, val);
980            true
981        } else {
982            false
983        }
984    }
985}
986
987impl Clone for ClassInstance {
988    fn clone(&self) -> Self {
989        Self {
990            def: Arc::clone(&self.def),
991            values: RwLock::new(self.values.read().clone()),
992            isa_chain: self.isa_chain.clone(),
993        }
994    }
995}
996
997impl PerlValue {
998    pub const UNDEF: PerlValue = PerlValue(nanbox::encode_imm_undef());
999
1000    #[inline]
1001    fn from_heap(arc: Arc<HeapObject>) -> PerlValue {
1002        let ptr = Arc::into_raw(arc);
1003        PerlValue(nanbox::encode_heap_ptr(ptr))
1004    }
1005
1006    #[inline]
1007    pub(crate) fn heap_arc(&self) -> Arc<HeapObject> {
1008        debug_assert!(nanbox::is_heap(self.0));
1009        unsafe {
1010            let p = nanbox::decode_heap_ptr::<HeapObject>(self.0);
1011            Arc::increment_strong_count(p);
1012            Arc::from_raw(p as *mut HeapObject)
1013        }
1014    }
1015
1016    /// Borrow the `Arc`-allocated [`HeapObject`] without refcount traffic (`Arc::clone` / `drop`).
1017    ///
1018    /// # Safety
1019    /// `nanbox::is_heap(self.0)` must hold (same invariant as [`Self::heap_arc`]).
1020    #[inline]
1021    pub(crate) unsafe fn heap_ref(&self) -> &HeapObject {
1022        &*nanbox::decode_heap_ptr::<HeapObject>(self.0)
1023    }
1024
1025    #[inline]
1026    pub(crate) fn with_heap<R>(&self, f: impl FnOnce(&HeapObject) -> R) -> Option<R> {
1027        if !nanbox::is_heap(self.0) {
1028            return None;
1029        }
1030        // SAFETY: `is_heap` matches the contract of [`Self::heap_ref`].
1031        Some(f(unsafe { self.heap_ref() }))
1032    }
1033
1034    /// Raw NaN-box bits for internal identity (e.g. [`crate::jit`] cache keys).
1035    #[inline]
1036    pub(crate) fn raw_bits(&self) -> u64 {
1037        self.0
1038    }
1039
1040    /// Reconstruct from [`Self::raw_bits`] (e.g. block JIT returning a full [`PerlValue`] encoding in `i64`).
1041    #[inline]
1042    pub(crate) fn from_raw_bits(bits: u64) -> Self {
1043        Self(bits)
1044    }
1045
1046    /// `typed : Int` — inline `i32` or heap `i64`.
1047    #[inline]
1048    pub fn is_integer_like(&self) -> bool {
1049        nanbox::as_imm_int32(self.0).is_some()
1050            || matches!(
1051                self.with_heap(|h| matches!(h, HeapObject::Integer(_))),
1052                Some(true)
1053            )
1054    }
1055
1056    /// Raw `f64` bits or heap boxed float (NaN/Inf).
1057    #[inline]
1058    pub fn is_float_like(&self) -> bool {
1059        nanbox::is_raw_float_bits(self.0)
1060            || matches!(
1061                self.with_heap(|h| matches!(h, HeapObject::Float(_))),
1062                Some(true)
1063            )
1064    }
1065
1066    /// Heap UTF-8 string only.
1067    #[inline]
1068    pub fn is_string_like(&self) -> bool {
1069        matches!(
1070            self.with_heap(|h| matches!(h, HeapObject::String(_))),
1071            Some(true)
1072        )
1073    }
1074
1075    #[inline]
1076    pub fn integer(n: i64) -> Self {
1077        if n >= i32::MIN as i64 && n <= i32::MAX as i64 {
1078            PerlValue(nanbox::encode_imm_int32(n as i32))
1079        } else {
1080            Self::from_heap(Arc::new(HeapObject::Integer(n)))
1081        }
1082    }
1083
1084    #[inline]
1085    pub fn float(f: f64) -> Self {
1086        if nanbox::float_needs_box(f) {
1087            Self::from_heap(Arc::new(HeapObject::Float(f)))
1088        } else {
1089            PerlValue(f.to_bits())
1090        }
1091    }
1092
1093    #[inline]
1094    pub fn string(s: String) -> Self {
1095        Self::from_heap(Arc::new(HeapObject::String(s)))
1096    }
1097
1098    #[inline]
1099    pub fn bytes(b: Arc<Vec<u8>>) -> Self {
1100        Self::from_heap(Arc::new(HeapObject::Bytes(b)))
1101    }
1102
1103    #[inline]
1104    pub fn array(v: Vec<PerlValue>) -> Self {
1105        Self::from_heap(Arc::new(HeapObject::Array(v)))
1106    }
1107
1108    /// Wrap a lazy iterator as a PerlValue.
1109    #[inline]
1110    pub fn iterator(it: Arc<dyn PerlIterator>) -> Self {
1111        Self::from_heap(Arc::new(HeapObject::Iterator(it)))
1112    }
1113
1114    /// True when this value is a lazy iterator.
1115    #[inline]
1116    pub fn is_iterator(&self) -> bool {
1117        if !nanbox::is_heap(self.0) {
1118            return false;
1119        }
1120        matches!(unsafe { self.heap_ref() }, HeapObject::Iterator(_))
1121    }
1122
1123    /// Extract the iterator Arc (panics if not an iterator).
1124    pub fn into_iterator(&self) -> Arc<dyn PerlIterator> {
1125        if nanbox::is_heap(self.0) {
1126            if let HeapObject::Iterator(it) = &*self.heap_arc() {
1127                return Arc::clone(it);
1128            }
1129        }
1130        panic!("into_iterator on non-iterator value");
1131    }
1132
1133    #[inline]
1134    pub fn hash(h: IndexMap<String, PerlValue>) -> Self {
1135        Self::from_heap(Arc::new(HeapObject::Hash(h)))
1136    }
1137
1138    #[inline]
1139    pub fn array_ref(a: Arc<RwLock<Vec<PerlValue>>>) -> Self {
1140        Self::from_heap(Arc::new(HeapObject::ArrayRef(a)))
1141    }
1142
1143    #[inline]
1144    pub fn hash_ref(h: Arc<RwLock<IndexMap<String, PerlValue>>>) -> Self {
1145        Self::from_heap(Arc::new(HeapObject::HashRef(h)))
1146    }
1147
1148    #[inline]
1149    pub fn scalar_ref(r: Arc<RwLock<PerlValue>>) -> Self {
1150        Self::from_heap(Arc::new(HeapObject::ScalarRef(r)))
1151    }
1152
1153    #[inline]
1154    pub fn capture_cell(r: Arc<RwLock<PerlValue>>) -> Self {
1155        Self::from_heap(Arc::new(HeapObject::CaptureCell(r)))
1156    }
1157
1158    #[inline]
1159    pub fn scalar_binding_ref(name: String) -> Self {
1160        Self::from_heap(Arc::new(HeapObject::ScalarBindingRef(name)))
1161    }
1162
1163    #[inline]
1164    pub fn array_binding_ref(name: String) -> Self {
1165        Self::from_heap(Arc::new(HeapObject::ArrayBindingRef(name)))
1166    }
1167
1168    #[inline]
1169    pub fn hash_binding_ref(name: String) -> Self {
1170        Self::from_heap(Arc::new(HeapObject::HashBindingRef(name)))
1171    }
1172
1173    #[inline]
1174    pub fn code_ref(c: Arc<PerlSub>) -> Self {
1175        Self::from_heap(Arc::new(HeapObject::CodeRef(c)))
1176    }
1177
1178    #[inline]
1179    pub fn as_code_ref(&self) -> Option<Arc<PerlSub>> {
1180        self.with_heap(|h| match h {
1181            HeapObject::CodeRef(sub) => Some(Arc::clone(sub)),
1182            _ => None,
1183        })
1184        .flatten()
1185    }
1186
1187    #[inline]
1188    pub fn as_regex(&self) -> Option<Arc<PerlCompiledRegex>> {
1189        self.with_heap(|h| match h {
1190            HeapObject::Regex(re, _, _) => Some(Arc::clone(re)),
1191            _ => None,
1192        })
1193        .flatten()
1194    }
1195
1196    #[inline]
1197    pub fn as_blessed_ref(&self) -> Option<Arc<BlessedRef>> {
1198        self.with_heap(|h| match h {
1199            HeapObject::Blessed(b) => Some(Arc::clone(b)),
1200            _ => None,
1201        })
1202        .flatten()
1203    }
1204
1205    /// Hash lookup when this value is a plain `HeapObject::Hash` (not a ref).
1206    #[inline]
1207    pub fn hash_get(&self, key: &str) -> Option<PerlValue> {
1208        self.with_heap(|h| match h {
1209            HeapObject::Hash(h) => h.get(key).cloned(),
1210            _ => None,
1211        })
1212        .flatten()
1213    }
1214
1215    #[inline]
1216    pub fn is_undef(&self) -> bool {
1217        nanbox::is_imm_undef(self.0)
1218    }
1219
1220    /// True for simple scalar values (integer, float, string, undef, bytes) that should be
1221    /// wrapped in ScalarRef for closure variable sharing. Complex heap objects like
1222    /// refs, blessed objects, code refs, etc. should NOT be wrapped because they already
1223    /// share state via Arc and wrapping breaks type detection.
1224    pub fn is_simple_scalar(&self) -> bool {
1225        if self.is_undef() {
1226            return true;
1227        }
1228        if !nanbox::is_heap(self.0) {
1229            return true; // immediate int32
1230        }
1231        matches!(
1232            unsafe { self.heap_ref() },
1233            HeapObject::Integer(_)
1234                | HeapObject::Float(_)
1235                | HeapObject::String(_)
1236                | HeapObject::Bytes(_)
1237        )
1238    }
1239
1240    /// Immediate `int32` or heap `Integer` (not float / string).
1241    #[inline]
1242    pub fn as_integer(&self) -> Option<i64> {
1243        if let Some(n) = nanbox::as_imm_int32(self.0) {
1244            return Some(n as i64);
1245        }
1246        if nanbox::is_raw_float_bits(self.0) {
1247            return None;
1248        }
1249        self.with_heap(|h| match h {
1250            HeapObject::Integer(n) => Some(*n),
1251            _ => None,
1252        })
1253        .flatten()
1254    }
1255
1256    #[inline]
1257    pub fn as_float(&self) -> Option<f64> {
1258        if nanbox::is_raw_float_bits(self.0) {
1259            return Some(f64::from_bits(self.0));
1260        }
1261        self.with_heap(|h| match h {
1262            HeapObject::Float(f) => Some(*f),
1263            _ => None,
1264        })
1265        .flatten()
1266    }
1267
1268    #[inline]
1269    pub fn as_array_vec(&self) -> Option<Vec<PerlValue>> {
1270        self.with_heap(|h| match h {
1271            HeapObject::Array(v) => Some(v.clone()),
1272            _ => None,
1273        })
1274        .flatten()
1275    }
1276
1277    /// Expand a `map` / `flat_map` / `pflat_map` block result into list elements. Plain arrays
1278    /// expand; when `peel_array_ref`, a single ARRAY ref is dereferenced one level (stryke
1279    /// `flat_map` / `pflat_map`; stock `map` uses `peel_array_ref == false`).
1280    pub fn map_flatten_outputs(&self, peel_array_ref: bool) -> Vec<PerlValue> {
1281        if let Some(a) = self.as_array_vec() {
1282            return a;
1283        }
1284        if peel_array_ref {
1285            if let Some(r) = self.as_array_ref() {
1286                return r.read().clone();
1287            }
1288        }
1289        if self.is_iterator() {
1290            return self.into_iterator().collect_all();
1291        }
1292        vec![self.clone()]
1293    }
1294
1295    #[inline]
1296    pub fn as_hash_map(&self) -> Option<IndexMap<String, PerlValue>> {
1297        self.with_heap(|h| match h {
1298            HeapObject::Hash(h) => Some(h.clone()),
1299            _ => None,
1300        })
1301        .flatten()
1302    }
1303
1304    #[inline]
1305    pub fn as_bytes_arc(&self) -> Option<Arc<Vec<u8>>> {
1306        self.with_heap(|h| match h {
1307            HeapObject::Bytes(b) => Some(Arc::clone(b)),
1308            _ => None,
1309        })
1310        .flatten()
1311    }
1312
1313    #[inline]
1314    pub fn as_async_task(&self) -> Option<Arc<PerlAsyncTask>> {
1315        self.with_heap(|h| match h {
1316            HeapObject::AsyncTask(t) => Some(Arc::clone(t)),
1317            _ => None,
1318        })
1319        .flatten()
1320    }
1321
1322    #[inline]
1323    pub fn as_generator(&self) -> Option<Arc<PerlGenerator>> {
1324        self.with_heap(|h| match h {
1325            HeapObject::Generator(g) => Some(Arc::clone(g)),
1326            _ => None,
1327        })
1328        .flatten()
1329    }
1330
1331    #[inline]
1332    pub fn as_atomic_arc(&self) -> Option<Arc<Mutex<PerlValue>>> {
1333        self.with_heap(|h| match h {
1334            HeapObject::Atomic(a) => Some(Arc::clone(a)),
1335            _ => None,
1336        })
1337        .flatten()
1338    }
1339
1340    #[inline]
1341    pub fn as_io_handle_name(&self) -> Option<String> {
1342        self.with_heap(|h| match h {
1343            HeapObject::IOHandle(n) => Some(n.clone()),
1344            _ => None,
1345        })
1346        .flatten()
1347    }
1348
1349    #[inline]
1350    pub fn as_sqlite_conn(&self) -> Option<Arc<Mutex<rusqlite::Connection>>> {
1351        self.with_heap(|h| match h {
1352            HeapObject::SqliteConn(c) => Some(Arc::clone(c)),
1353            _ => None,
1354        })
1355        .flatten()
1356    }
1357
1358    #[inline]
1359    pub fn as_struct_inst(&self) -> Option<Arc<StructInstance>> {
1360        self.with_heap(|h| match h {
1361            HeapObject::StructInst(s) => Some(Arc::clone(s)),
1362            _ => None,
1363        })
1364        .flatten()
1365    }
1366
1367    #[inline]
1368    pub fn as_enum_inst(&self) -> Option<Arc<EnumInstance>> {
1369        self.with_heap(|h| match h {
1370            HeapObject::EnumInst(e) => Some(Arc::clone(e)),
1371            _ => None,
1372        })
1373        .flatten()
1374    }
1375
1376    #[inline]
1377    pub fn as_class_inst(&self) -> Option<Arc<ClassInstance>> {
1378        self.with_heap(|h| match h {
1379            HeapObject::ClassInst(c) => Some(Arc::clone(c)),
1380            _ => None,
1381        })
1382        .flatten()
1383    }
1384
1385    #[inline]
1386    pub fn as_dataframe(&self) -> Option<Arc<Mutex<PerlDataFrame>>> {
1387        self.with_heap(|h| match h {
1388            HeapObject::DataFrame(d) => Some(Arc::clone(d)),
1389            _ => None,
1390        })
1391        .flatten()
1392    }
1393
1394    #[inline]
1395    pub fn as_deque(&self) -> Option<Arc<Mutex<VecDeque<PerlValue>>>> {
1396        self.with_heap(|h| match h {
1397            HeapObject::Deque(d) => Some(Arc::clone(d)),
1398            _ => None,
1399        })
1400        .flatten()
1401    }
1402
1403    #[inline]
1404    pub fn as_heap_pq(&self) -> Option<Arc<Mutex<PerlHeap>>> {
1405        self.with_heap(|h| match h {
1406            HeapObject::Heap(h) => Some(Arc::clone(h)),
1407            _ => None,
1408        })
1409        .flatten()
1410    }
1411
1412    #[inline]
1413    pub fn as_pipeline(&self) -> Option<Arc<Mutex<PipelineInner>>> {
1414        self.with_heap(|h| match h {
1415            HeapObject::Pipeline(p) => Some(Arc::clone(p)),
1416            _ => None,
1417        })
1418        .flatten()
1419    }
1420
1421    #[inline]
1422    pub fn as_capture(&self) -> Option<Arc<CaptureResult>> {
1423        self.with_heap(|h| match h {
1424            HeapObject::Capture(c) => Some(Arc::clone(c)),
1425            _ => None,
1426        })
1427        .flatten()
1428    }
1429
1430    #[inline]
1431    pub fn as_ppool(&self) -> Option<PerlPpool> {
1432        self.with_heap(|h| match h {
1433            HeapObject::Ppool(p) => Some(p.clone()),
1434            _ => None,
1435        })
1436        .flatten()
1437    }
1438
1439    #[inline]
1440    pub fn as_remote_cluster(&self) -> Option<Arc<RemoteCluster>> {
1441        self.with_heap(|h| match h {
1442            HeapObject::RemoteCluster(c) => Some(Arc::clone(c)),
1443            _ => None,
1444        })
1445        .flatten()
1446    }
1447
1448    #[inline]
1449    pub fn as_barrier(&self) -> Option<PerlBarrier> {
1450        self.with_heap(|h| match h {
1451            HeapObject::Barrier(b) => Some(b.clone()),
1452            _ => None,
1453        })
1454        .flatten()
1455    }
1456
1457    #[inline]
1458    pub fn as_channel_tx(&self) -> Option<Arc<Sender<PerlValue>>> {
1459        self.with_heap(|h| match h {
1460            HeapObject::ChannelTx(t) => Some(Arc::clone(t)),
1461            _ => None,
1462        })
1463        .flatten()
1464    }
1465
1466    #[inline]
1467    pub fn as_channel_rx(&self) -> Option<Arc<Receiver<PerlValue>>> {
1468        self.with_heap(|h| match h {
1469            HeapObject::ChannelRx(r) => Some(Arc::clone(r)),
1470            _ => None,
1471        })
1472        .flatten()
1473    }
1474
1475    #[inline]
1476    pub fn as_scalar_ref(&self) -> Option<Arc<RwLock<PerlValue>>> {
1477        self.with_heap(|h| match h {
1478            HeapObject::ScalarRef(r) => Some(Arc::clone(r)),
1479            _ => None,
1480        })
1481        .flatten()
1482    }
1483
1484    /// Returns the inner Arc if this is a [`HeapObject::CaptureCell`].
1485    #[inline]
1486    pub fn as_capture_cell(&self) -> Option<Arc<RwLock<PerlValue>>> {
1487        self.with_heap(|h| match h {
1488            HeapObject::CaptureCell(r) => Some(Arc::clone(r)),
1489            _ => None,
1490        })
1491        .flatten()
1492    }
1493
1494    /// Name of the scalar slot for [`HeapObject::ScalarBindingRef`], if any.
1495    #[inline]
1496    pub fn as_scalar_binding_name(&self) -> Option<String> {
1497        self.with_heap(|h| match h {
1498            HeapObject::ScalarBindingRef(s) => Some(s.clone()),
1499            _ => None,
1500        })
1501        .flatten()
1502    }
1503
1504    /// Stash-qualified array name for [`HeapObject::ArrayBindingRef`], if any.
1505    #[inline]
1506    pub fn as_array_binding_name(&self) -> Option<String> {
1507        self.with_heap(|h| match h {
1508            HeapObject::ArrayBindingRef(s) => Some(s.clone()),
1509            _ => None,
1510        })
1511        .flatten()
1512    }
1513
1514    /// Hash name for [`HeapObject::HashBindingRef`], if any.
1515    #[inline]
1516    pub fn as_hash_binding_name(&self) -> Option<String> {
1517        self.with_heap(|h| match h {
1518            HeapObject::HashBindingRef(s) => Some(s.clone()),
1519            _ => None,
1520        })
1521        .flatten()
1522    }
1523
1524    #[inline]
1525    pub fn as_array_ref(&self) -> Option<Arc<RwLock<Vec<PerlValue>>>> {
1526        self.with_heap(|h| match h {
1527            HeapObject::ArrayRef(r) => Some(Arc::clone(r)),
1528            _ => None,
1529        })
1530        .flatten()
1531    }
1532
1533    #[inline]
1534    pub fn as_hash_ref(&self) -> Option<Arc<RwLock<IndexMap<String, PerlValue>>>> {
1535        self.with_heap(|h| match h {
1536            HeapObject::HashRef(r) => Some(Arc::clone(r)),
1537            _ => None,
1538        })
1539        .flatten()
1540    }
1541
1542    /// `mysync`: `deque` / priority `heap` — already `Arc<Mutex<…>>`.
1543    #[inline]
1544    pub fn is_mysync_deque_or_heap(&self) -> bool {
1545        matches!(
1546            self.with_heap(|h| matches!(h, HeapObject::Deque(_) | HeapObject::Heap(_))),
1547            Some(true)
1548        )
1549    }
1550
1551    #[inline]
1552    pub fn regex(rx: Arc<PerlCompiledRegex>, pattern_src: String, flags: String) -> Self {
1553        Self::from_heap(Arc::new(HeapObject::Regex(rx, pattern_src, flags)))
1554    }
1555
1556    /// Pattern and flag string stored with a compiled regex (for `=~` / [`Op::RegexMatchDyn`]).
1557    #[inline]
1558    pub fn regex_src_and_flags(&self) -> Option<(String, String)> {
1559        self.with_heap(|h| match h {
1560            HeapObject::Regex(_, pat, fl) => Some((pat.clone(), fl.clone())),
1561            _ => None,
1562        })
1563        .flatten()
1564    }
1565
1566    #[inline]
1567    pub fn blessed(b: Arc<BlessedRef>) -> Self {
1568        Self::from_heap(Arc::new(HeapObject::Blessed(b)))
1569    }
1570
1571    #[inline]
1572    pub fn io_handle(name: String) -> Self {
1573        Self::from_heap(Arc::new(HeapObject::IOHandle(name)))
1574    }
1575
1576    #[inline]
1577    pub fn atomic(a: Arc<Mutex<PerlValue>>) -> Self {
1578        Self::from_heap(Arc::new(HeapObject::Atomic(a)))
1579    }
1580
1581    #[inline]
1582    pub fn set(s: Arc<PerlSet>) -> Self {
1583        Self::from_heap(Arc::new(HeapObject::Set(s)))
1584    }
1585
1586    #[inline]
1587    pub fn channel_tx(tx: Arc<Sender<PerlValue>>) -> Self {
1588        Self::from_heap(Arc::new(HeapObject::ChannelTx(tx)))
1589    }
1590
1591    #[inline]
1592    pub fn channel_rx(rx: Arc<Receiver<PerlValue>>) -> Self {
1593        Self::from_heap(Arc::new(HeapObject::ChannelRx(rx)))
1594    }
1595
1596    #[inline]
1597    pub fn async_task(t: Arc<PerlAsyncTask>) -> Self {
1598        Self::from_heap(Arc::new(HeapObject::AsyncTask(t)))
1599    }
1600
1601    #[inline]
1602    pub fn generator(g: Arc<PerlGenerator>) -> Self {
1603        Self::from_heap(Arc::new(HeapObject::Generator(g)))
1604    }
1605
1606    #[inline]
1607    pub fn deque(d: Arc<Mutex<VecDeque<PerlValue>>>) -> Self {
1608        Self::from_heap(Arc::new(HeapObject::Deque(d)))
1609    }
1610
1611    #[inline]
1612    pub fn heap(h: Arc<Mutex<PerlHeap>>) -> Self {
1613        Self::from_heap(Arc::new(HeapObject::Heap(h)))
1614    }
1615
1616    #[inline]
1617    pub fn pipeline(p: Arc<Mutex<PipelineInner>>) -> Self {
1618        Self::from_heap(Arc::new(HeapObject::Pipeline(p)))
1619    }
1620
1621    #[inline]
1622    pub fn capture(c: Arc<CaptureResult>) -> Self {
1623        Self::from_heap(Arc::new(HeapObject::Capture(c)))
1624    }
1625
1626    #[inline]
1627    pub fn ppool(p: PerlPpool) -> Self {
1628        Self::from_heap(Arc::new(HeapObject::Ppool(p)))
1629    }
1630
1631    #[inline]
1632    pub fn remote_cluster(c: Arc<RemoteCluster>) -> Self {
1633        Self::from_heap(Arc::new(HeapObject::RemoteCluster(c)))
1634    }
1635
1636    #[inline]
1637    pub fn barrier(b: PerlBarrier) -> Self {
1638        Self::from_heap(Arc::new(HeapObject::Barrier(b)))
1639    }
1640
1641    #[inline]
1642    pub fn sqlite_conn(c: Arc<Mutex<rusqlite::Connection>>) -> Self {
1643        Self::from_heap(Arc::new(HeapObject::SqliteConn(c)))
1644    }
1645
1646    #[inline]
1647    pub fn struct_inst(s: Arc<StructInstance>) -> Self {
1648        Self::from_heap(Arc::new(HeapObject::StructInst(s)))
1649    }
1650
1651    #[inline]
1652    pub fn enum_inst(e: Arc<EnumInstance>) -> Self {
1653        Self::from_heap(Arc::new(HeapObject::EnumInst(e)))
1654    }
1655
1656    #[inline]
1657    pub fn class_inst(c: Arc<ClassInstance>) -> Self {
1658        Self::from_heap(Arc::new(HeapObject::ClassInst(c)))
1659    }
1660
1661    #[inline]
1662    pub fn dataframe(df: Arc<Mutex<PerlDataFrame>>) -> Self {
1663        Self::from_heap(Arc::new(HeapObject::DataFrame(df)))
1664    }
1665
1666    /// OS errno dualvar (`$!`) or eval-error dualvar (`$@`): `to_int`/`to_number` use `code`; string context uses `msg`.
1667    #[inline]
1668    pub fn errno_dual(code: i32, msg: String) -> Self {
1669        Self::from_heap(Arc::new(HeapObject::ErrnoDual { code, msg }))
1670    }
1671
1672    /// If this value is a numeric/string dualvar (`$!` / `$@`), return `(code, msg)`.
1673    #[inline]
1674    pub(crate) fn errno_dual_parts(&self) -> Option<(i32, String)> {
1675        if !nanbox::is_heap(self.0) {
1676            return None;
1677        }
1678        match unsafe { self.heap_ref() } {
1679            HeapObject::ErrnoDual { code, msg } => Some((*code, msg.clone())),
1680            _ => None,
1681        }
1682    }
1683
1684    /// Heap string payload, if any (allocates).
1685    #[inline]
1686    pub fn as_str(&self) -> Option<String> {
1687        if !nanbox::is_heap(self.0) {
1688            return None;
1689        }
1690        match unsafe { self.heap_ref() } {
1691            HeapObject::String(s) => Some(s.clone()),
1692            _ => None,
1693        }
1694    }
1695
1696    #[inline]
1697    pub fn append_to(&self, buf: &mut String) {
1698        if nanbox::is_imm_undef(self.0) {
1699            return;
1700        }
1701        if let Some(n) = nanbox::as_imm_int32(self.0) {
1702            let mut b = itoa::Buffer::new();
1703            buf.push_str(b.format(n));
1704            return;
1705        }
1706        if nanbox::is_raw_float_bits(self.0) {
1707            buf.push_str(&format_float(f64::from_bits(self.0)));
1708            return;
1709        }
1710        match unsafe { self.heap_ref() } {
1711            HeapObject::String(s) => buf.push_str(s),
1712            HeapObject::ErrnoDual { msg, .. } => buf.push_str(msg),
1713            HeapObject::Bytes(b) => buf.push_str(&decode_utf8_or_latin1(b)),
1714            HeapObject::Atomic(arc) => arc.lock().append_to(buf),
1715            HeapObject::Set(s) => {
1716                buf.push('{');
1717                let mut first = true;
1718                for v in s.values() {
1719                    if !first {
1720                        buf.push(',');
1721                    }
1722                    first = false;
1723                    v.append_to(buf);
1724                }
1725                buf.push('}');
1726            }
1727            HeapObject::ChannelTx(_) => buf.push_str("PCHANNEL::Tx"),
1728            HeapObject::ChannelRx(_) => buf.push_str("PCHANNEL::Rx"),
1729            HeapObject::AsyncTask(_) => buf.push_str("AsyncTask"),
1730            HeapObject::Generator(_) => buf.push_str("Generator"),
1731            HeapObject::Pipeline(_) => buf.push_str("Pipeline"),
1732            HeapObject::DataFrame(d) => {
1733                let g = d.lock();
1734                buf.push_str(&format!("DataFrame({}x{})", g.nrows(), g.ncols()));
1735            }
1736            HeapObject::Capture(_) => buf.push_str("Capture"),
1737            HeapObject::Ppool(_) => buf.push_str("Ppool"),
1738            HeapObject::RemoteCluster(_) => buf.push_str("Cluster"),
1739            HeapObject::Barrier(_) => buf.push_str("Barrier"),
1740            HeapObject::SqliteConn(_) => buf.push_str("SqliteConn"),
1741            HeapObject::StructInst(s) => buf.push_str(&s.def.name),
1742            _ => buf.push_str(&self.to_string()),
1743        }
1744    }
1745
1746    #[inline]
1747    pub fn unwrap_atomic(&self) -> PerlValue {
1748        if !nanbox::is_heap(self.0) {
1749            return self.clone();
1750        }
1751        match unsafe { self.heap_ref() } {
1752            HeapObject::Atomic(a) => a.lock().clone(),
1753            _ => self.clone(),
1754        }
1755    }
1756
1757    #[inline]
1758    pub fn is_atomic(&self) -> bool {
1759        if !nanbox::is_heap(self.0) {
1760            return false;
1761        }
1762        matches!(unsafe { self.heap_ref() }, HeapObject::Atomic(_))
1763    }
1764
1765    #[inline]
1766    pub fn is_true(&self) -> bool {
1767        if nanbox::is_imm_undef(self.0) {
1768            return false;
1769        }
1770        if let Some(n) = nanbox::as_imm_int32(self.0) {
1771            return n != 0;
1772        }
1773        if nanbox::is_raw_float_bits(self.0) {
1774            return f64::from_bits(self.0) != 0.0;
1775        }
1776        match unsafe { self.heap_ref() } {
1777            HeapObject::ErrnoDual { code, msg } => *code != 0 || !msg.is_empty(),
1778            HeapObject::String(s) => !s.is_empty() && s != "0",
1779            HeapObject::Bytes(b) => !b.is_empty(),
1780            HeapObject::Array(a) => !a.is_empty(),
1781            HeapObject::Hash(h) => !h.is_empty(),
1782            HeapObject::Atomic(arc) => arc.lock().is_true(),
1783            HeapObject::Set(s) => !s.is_empty(),
1784            HeapObject::Deque(d) => !d.lock().is_empty(),
1785            HeapObject::Heap(h) => !h.lock().items.is_empty(),
1786            HeapObject::DataFrame(d) => d.lock().nrows() > 0,
1787            HeapObject::Pipeline(_) | HeapObject::Capture(_) => true,
1788            _ => true,
1789        }
1790    }
1791
1792    /// String concat with owned LHS: moves out a uniquely held heap string when possible
1793    /// ([`Self::into_string`]), then appends `rhs`. Used for `.=` and VM concat-append ops.
1794    #[inline]
1795    pub(crate) fn concat_append_owned(self, rhs: &PerlValue) -> PerlValue {
1796        let mut s = self.into_string();
1797        rhs.append_to(&mut s);
1798        PerlValue::string(s)
1799    }
1800
1801    /// In-place repeated `.=` for the fused counted-loop superinstruction:
1802    /// append `rhs` exactly `n` times to the sole-owned heap `String` behind
1803    /// `self`, reserving once. Returns `false` (leaving `self` untouched) when
1804    /// the value is not a uniquely-held `HeapObject::String` — the VM then
1805    /// falls back to the per-iteration slow path.
1806    #[inline]
1807    pub(crate) fn try_concat_repeat_inplace(&mut self, rhs: &str, n: usize) -> bool {
1808        if !nanbox::is_heap(self.0) || n == 0 {
1809            // n==0 is trivially "done" in the caller's sense — nothing to append.
1810            return n == 0 && nanbox::is_heap(self.0);
1811        }
1812        unsafe {
1813            if !matches!(self.heap_ref(), HeapObject::String(_)) {
1814                return false;
1815            }
1816            let raw = nanbox::decode_heap_ptr::<HeapObject>(self.0) as *mut HeapObject
1817                as *const HeapObject;
1818            let mut arc: Arc<HeapObject> = Arc::from_raw(raw);
1819            let did = if let Some(HeapObject::String(s)) = Arc::get_mut(&mut arc) {
1820                if !rhs.is_empty() {
1821                    s.reserve(rhs.len().saturating_mul(n));
1822                    for _ in 0..n {
1823                        s.push_str(rhs);
1824                    }
1825                }
1826                true
1827            } else {
1828                false
1829            };
1830            let restored = Arc::into_raw(arc);
1831            self.0 = nanbox::encode_heap_ptr(restored);
1832            did
1833        }
1834    }
1835
1836    /// In-place `.=` fast path: when `self` is the **sole owner** of a heap
1837    /// `HeapObject::String`, append `rhs` straight into the existing `String`
1838    /// buffer — no `Arc` allocation, no unwrap/rewrap churn, `String::push_str`
1839    /// reuses spare capacity and only reallocates on growth.
1840    ///
1841    /// Returns `true` if the in-place path ran (no further work for the caller),
1842    /// `false` when the value was not a heap String or the `Arc` was shared —
1843    /// the caller must then fall back to [`Self::concat_append_owned`] so that a
1844    /// second handle to the same `Arc` never observes a torn midway write.
1845    #[inline]
1846    pub(crate) fn try_concat_append_inplace(&mut self, rhs: &PerlValue) -> bool {
1847        if !nanbox::is_heap(self.0) {
1848            return false;
1849        }
1850        // Peek without bumping the refcount to bail early on non-String payloads.
1851        // SAFETY: nanbox::is_heap holds (checked above), so the payload is a live
1852        // `Arc<HeapObject>` whose pointer we decode below.
1853        unsafe {
1854            if !matches!(self.heap_ref(), HeapObject::String(_)) {
1855                return false;
1856            }
1857            // Reconstitute the Arc to consult its strong count; `Arc::get_mut`
1858            // returns `Some` iff both strong and weak counts are 1.
1859            let raw = nanbox::decode_heap_ptr::<HeapObject>(self.0) as *mut HeapObject
1860                as *const HeapObject;
1861            let mut arc: Arc<HeapObject> = Arc::from_raw(raw);
1862            let did_append = if let Some(HeapObject::String(s)) = Arc::get_mut(&mut arc) {
1863                rhs.append_to(s);
1864                true
1865            } else {
1866                false
1867            };
1868            // Either way, hand the Arc back to the nanbox slot — we only ever
1869            // borrowed the single strong reference we started with.
1870            let restored = Arc::into_raw(arc);
1871            self.0 = nanbox::encode_heap_ptr(restored);
1872            did_append
1873        }
1874    }
1875
1876    #[inline]
1877    pub fn into_string(self) -> String {
1878        let bits = self.0;
1879        std::mem::forget(self);
1880        if nanbox::is_imm_undef(bits) {
1881            return String::new();
1882        }
1883        if let Some(n) = nanbox::as_imm_int32(bits) {
1884            let mut buf = itoa::Buffer::new();
1885            return buf.format(n).to_owned();
1886        }
1887        if nanbox::is_raw_float_bits(bits) {
1888            return format_float(f64::from_bits(bits));
1889        }
1890        if nanbox::is_heap(bits) {
1891            unsafe {
1892                let arc =
1893                    Arc::from_raw(nanbox::decode_heap_ptr::<HeapObject>(bits) as *mut HeapObject);
1894                match Arc::try_unwrap(arc) {
1895                    Ok(HeapObject::String(s)) => return s,
1896                    Ok(o) => return PerlValue::from_heap(Arc::new(o)).to_string(),
1897                    Err(arc) => {
1898                        return match &*arc {
1899                            HeapObject::String(s) => s.clone(),
1900                            _ => PerlValue::from_heap(Arc::clone(&arc)).to_string(),
1901                        };
1902                    }
1903                }
1904            }
1905        }
1906        String::new()
1907    }
1908
1909    #[inline]
1910    pub fn as_str_or_empty(&self) -> String {
1911        if !nanbox::is_heap(self.0) {
1912            return String::new();
1913        }
1914        match unsafe { self.heap_ref() } {
1915            HeapObject::String(s) => s.clone(),
1916            HeapObject::ErrnoDual { msg, .. } => msg.clone(),
1917            _ => String::new(),
1918        }
1919    }
1920
1921    #[inline]
1922    pub fn to_number(&self) -> f64 {
1923        if nanbox::is_imm_undef(self.0) {
1924            return 0.0;
1925        }
1926        if let Some(n) = nanbox::as_imm_int32(self.0) {
1927            return n as f64;
1928        }
1929        if nanbox::is_raw_float_bits(self.0) {
1930            return f64::from_bits(self.0);
1931        }
1932        match unsafe { self.heap_ref() } {
1933            HeapObject::Integer(n) => *n as f64,
1934            HeapObject::Float(f) => *f,
1935            HeapObject::ErrnoDual { code, .. } => *code as f64,
1936            HeapObject::String(s) => parse_number(s),
1937            HeapObject::Bytes(b) => b.len() as f64,
1938            HeapObject::Array(a) => a.len() as f64,
1939            HeapObject::Atomic(arc) => arc.lock().to_number(),
1940            HeapObject::Set(s) => s.len() as f64,
1941            HeapObject::ChannelTx(_)
1942            | HeapObject::ChannelRx(_)
1943            | HeapObject::AsyncTask(_)
1944            | HeapObject::Generator(_) => 1.0,
1945            HeapObject::Deque(d) => d.lock().len() as f64,
1946            HeapObject::Heap(h) => h.lock().items.len() as f64,
1947            HeapObject::Pipeline(p) => p.lock().source.len() as f64,
1948            HeapObject::DataFrame(d) => d.lock().nrows() as f64,
1949            HeapObject::Capture(_)
1950            | HeapObject::Ppool(_)
1951            | HeapObject::RemoteCluster(_)
1952            | HeapObject::Barrier(_)
1953            | HeapObject::SqliteConn(_)
1954            | HeapObject::StructInst(_)
1955            | HeapObject::IOHandle(_) => 1.0,
1956            _ => 0.0,
1957        }
1958    }
1959
1960    #[inline]
1961    pub fn to_int(&self) -> i64 {
1962        if nanbox::is_imm_undef(self.0) {
1963            return 0;
1964        }
1965        if let Some(n) = nanbox::as_imm_int32(self.0) {
1966            return n as i64;
1967        }
1968        if nanbox::is_raw_float_bits(self.0) {
1969            return f64::from_bits(self.0) as i64;
1970        }
1971        match unsafe { self.heap_ref() } {
1972            HeapObject::Integer(n) => *n,
1973            HeapObject::Float(f) => *f as i64,
1974            HeapObject::ErrnoDual { code, .. } => *code as i64,
1975            HeapObject::String(s) => parse_number(s) as i64,
1976            HeapObject::Bytes(b) => b.len() as i64,
1977            HeapObject::Array(a) => a.len() as i64,
1978            HeapObject::Atomic(arc) => arc.lock().to_int(),
1979            HeapObject::Set(s) => s.len() as i64,
1980            HeapObject::ChannelTx(_)
1981            | HeapObject::ChannelRx(_)
1982            | HeapObject::AsyncTask(_)
1983            | HeapObject::Generator(_) => 1,
1984            HeapObject::Deque(d) => d.lock().len() as i64,
1985            HeapObject::Heap(h) => h.lock().items.len() as i64,
1986            HeapObject::Pipeline(p) => p.lock().source.len() as i64,
1987            HeapObject::DataFrame(d) => d.lock().nrows() as i64,
1988            HeapObject::Capture(_)
1989            | HeapObject::Ppool(_)
1990            | HeapObject::RemoteCluster(_)
1991            | HeapObject::Barrier(_)
1992            | HeapObject::SqliteConn(_)
1993            | HeapObject::StructInst(_)
1994            | HeapObject::IOHandle(_) => 1,
1995            _ => 0,
1996        }
1997    }
1998
1999    pub fn type_name(&self) -> String {
2000        if nanbox::is_imm_undef(self.0) {
2001            return "undef".to_string();
2002        }
2003        if nanbox::as_imm_int32(self.0).is_some() {
2004            return "INTEGER".to_string();
2005        }
2006        if nanbox::is_raw_float_bits(self.0) {
2007            return "FLOAT".to_string();
2008        }
2009        match unsafe { self.heap_ref() } {
2010            HeapObject::String(_) => "STRING".to_string(),
2011            HeapObject::Bytes(_) => "BYTES".to_string(),
2012            HeapObject::Array(_) => "ARRAY".to_string(),
2013            HeapObject::Hash(_) => "HASH".to_string(),
2014            HeapObject::ArrayRef(_) | HeapObject::ArrayBindingRef(_) => "ARRAY".to_string(),
2015            HeapObject::HashRef(_) | HeapObject::HashBindingRef(_) => "HASH".to_string(),
2016            HeapObject::ScalarRef(_)
2017            | HeapObject::ScalarBindingRef(_)
2018            | HeapObject::CaptureCell(_) => "SCALAR".to_string(),
2019            HeapObject::CodeRef(_) => "CODE".to_string(),
2020            HeapObject::Regex(_, _, _) => "Regexp".to_string(),
2021            HeapObject::Blessed(b) => b.class.clone(),
2022            HeapObject::IOHandle(_) => "GLOB".to_string(),
2023            HeapObject::Atomic(_) => "ATOMIC".to_string(),
2024            HeapObject::Set(_) => "Set".to_string(),
2025            HeapObject::ChannelTx(_) => "PCHANNEL::Tx".to_string(),
2026            HeapObject::ChannelRx(_) => "PCHANNEL::Rx".to_string(),
2027            HeapObject::AsyncTask(_) => "ASYNCTASK".to_string(),
2028            HeapObject::Generator(_) => "Generator".to_string(),
2029            HeapObject::Deque(_) => "Deque".to_string(),
2030            HeapObject::Heap(_) => "Heap".to_string(),
2031            HeapObject::Pipeline(_) => "Pipeline".to_string(),
2032            HeapObject::DataFrame(_) => "DataFrame".to_string(),
2033            HeapObject::Capture(_) => "Capture".to_string(),
2034            HeapObject::Ppool(_) => "Ppool".to_string(),
2035            HeapObject::RemoteCluster(_) => "Cluster".to_string(),
2036            HeapObject::Barrier(_) => "Barrier".to_string(),
2037            HeapObject::SqliteConn(_) => "SqliteConn".to_string(),
2038            HeapObject::StructInst(s) => s.def.name.to_string(),
2039            HeapObject::EnumInst(e) => e.def.name.to_string(),
2040            HeapObject::ClassInst(c) => c.def.name.to_string(),
2041            HeapObject::Iterator(_) => "Iterator".to_string(),
2042            HeapObject::ErrnoDual { .. } => "Errno".to_string(),
2043            HeapObject::Integer(_) => "INTEGER".to_string(),
2044            HeapObject::Float(_) => "FLOAT".to_string(),
2045        }
2046    }
2047
2048    pub fn ref_type(&self) -> PerlValue {
2049        if !nanbox::is_heap(self.0) {
2050            return PerlValue::string(String::new());
2051        }
2052        match unsafe { self.heap_ref() } {
2053            HeapObject::ArrayRef(_) | HeapObject::ArrayBindingRef(_) => {
2054                PerlValue::string("ARRAY".into())
2055            }
2056            HeapObject::HashRef(_) | HeapObject::HashBindingRef(_) => {
2057                PerlValue::string("HASH".into())
2058            }
2059            HeapObject::ScalarRef(_) | HeapObject::ScalarBindingRef(_) => {
2060                PerlValue::string("SCALAR".into())
2061            }
2062            HeapObject::CodeRef(_) => PerlValue::string("CODE".into()),
2063            HeapObject::Regex(_, _, _) => PerlValue::string("Regexp".into()),
2064            HeapObject::Atomic(_) => PerlValue::string("ATOMIC".into()),
2065            HeapObject::Set(_) => PerlValue::string("Set".into()),
2066            HeapObject::ChannelTx(_) => PerlValue::string("PCHANNEL::Tx".into()),
2067            HeapObject::ChannelRx(_) => PerlValue::string("PCHANNEL::Rx".into()),
2068            HeapObject::AsyncTask(_) => PerlValue::string("ASYNCTASK".into()),
2069            HeapObject::Generator(_) => PerlValue::string("Generator".into()),
2070            HeapObject::Deque(_) => PerlValue::string("Deque".into()),
2071            HeapObject::Heap(_) => PerlValue::string("Heap".into()),
2072            HeapObject::Pipeline(_) => PerlValue::string("Pipeline".into()),
2073            HeapObject::DataFrame(_) => PerlValue::string("DataFrame".into()),
2074            HeapObject::Capture(_) => PerlValue::string("Capture".into()),
2075            HeapObject::Ppool(_) => PerlValue::string("Ppool".into()),
2076            HeapObject::RemoteCluster(_) => PerlValue::string("Cluster".into()),
2077            HeapObject::Barrier(_) => PerlValue::string("Barrier".into()),
2078            HeapObject::SqliteConn(_) => PerlValue::string("SqliteConn".into()),
2079            HeapObject::StructInst(s) => PerlValue::string(s.def.name.clone()),
2080            HeapObject::EnumInst(e) => PerlValue::string(e.def.name.clone()),
2081            HeapObject::Bytes(_) => PerlValue::string("BYTES".into()),
2082            HeapObject::Blessed(b) => PerlValue::string(b.class.clone()),
2083            _ => PerlValue::string(String::new()),
2084        }
2085    }
2086
2087    pub fn num_cmp(&self, other: &PerlValue) -> Ordering {
2088        let a = self.to_number();
2089        let b = other.to_number();
2090        a.partial_cmp(&b).unwrap_or(Ordering::Equal)
2091    }
2092
2093    /// String equality for `eq` / `cmp` without allocating when both sides are heap strings.
2094    #[inline]
2095    pub fn str_eq(&self, other: &PerlValue) -> bool {
2096        if nanbox::is_heap(self.0) && nanbox::is_heap(other.0) {
2097            if let (HeapObject::String(a), HeapObject::String(b)) =
2098                unsafe { (self.heap_ref(), other.heap_ref()) }
2099            {
2100                return a == b;
2101            }
2102        }
2103        self.to_string() == other.to_string()
2104    }
2105
2106    pub fn str_cmp(&self, other: &PerlValue) -> Ordering {
2107        if nanbox::is_heap(self.0) && nanbox::is_heap(other.0) {
2108            if let (HeapObject::String(a), HeapObject::String(b)) =
2109                unsafe { (self.heap_ref(), other.heap_ref()) }
2110            {
2111                return a.cmp(b);
2112            }
2113        }
2114        self.to_string().cmp(&other.to_string())
2115    }
2116
2117    /// Deep equality for struct fields (recursive).
2118    pub fn struct_field_eq(&self, other: &PerlValue) -> bool {
2119        if nanbox::is_imm_undef(self.0) && nanbox::is_imm_undef(other.0) {
2120            return true;
2121        }
2122        if let (Some(a), Some(b)) = (nanbox::as_imm_int32(self.0), nanbox::as_imm_int32(other.0)) {
2123            return a == b;
2124        }
2125        if nanbox::is_raw_float_bits(self.0) && nanbox::is_raw_float_bits(other.0) {
2126            return f64::from_bits(self.0) == f64::from_bits(other.0);
2127        }
2128        if !nanbox::is_heap(self.0) || !nanbox::is_heap(other.0) {
2129            return self.to_number() == other.to_number();
2130        }
2131        match (unsafe { self.heap_ref() }, unsafe { other.heap_ref() }) {
2132            (HeapObject::String(a), HeapObject::String(b)) => a == b,
2133            (HeapObject::Integer(a), HeapObject::Integer(b)) => a == b,
2134            (HeapObject::Float(a), HeapObject::Float(b)) => a == b,
2135            (HeapObject::Array(a), HeapObject::Array(b)) => {
2136                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.struct_field_eq(y))
2137            }
2138            (HeapObject::ArrayRef(a), HeapObject::ArrayRef(b)) => {
2139                let ag = a.read();
2140                let bg = b.read();
2141                ag.len() == bg.len() && ag.iter().zip(bg.iter()).all(|(x, y)| x.struct_field_eq(y))
2142            }
2143            (HeapObject::Hash(a), HeapObject::Hash(b)) => {
2144                a.len() == b.len()
2145                    && a.iter()
2146                        .all(|(k, v)| b.get(k).is_some_and(|bv| v.struct_field_eq(bv)))
2147            }
2148            (HeapObject::HashRef(a), HeapObject::HashRef(b)) => {
2149                let ag = a.read();
2150                let bg = b.read();
2151                ag.len() == bg.len()
2152                    && ag
2153                        .iter()
2154                        .all(|(k, v)| bg.get(k).is_some_and(|bv| v.struct_field_eq(bv)))
2155            }
2156            (HeapObject::StructInst(a), HeapObject::StructInst(b)) => {
2157                if a.def.name != b.def.name {
2158                    false
2159                } else {
2160                    let av = a.get_values();
2161                    let bv = b.get_values();
2162                    av.len() == bv.len()
2163                        && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y))
2164                }
2165            }
2166            _ => self.to_string() == other.to_string(),
2167        }
2168    }
2169
2170    /// Deep clone a value (used for struct clone).
2171    pub fn deep_clone(&self) -> PerlValue {
2172        if !nanbox::is_heap(self.0) {
2173            return self.clone();
2174        }
2175        match unsafe { self.heap_ref() } {
2176            HeapObject::Array(a) => PerlValue::array(a.iter().map(|v| v.deep_clone()).collect()),
2177            HeapObject::ArrayRef(a) => {
2178                let cloned: Vec<PerlValue> = a.read().iter().map(|v| v.deep_clone()).collect();
2179                PerlValue::array_ref(Arc::new(RwLock::new(cloned)))
2180            }
2181            HeapObject::Hash(h) => {
2182                let mut cloned = IndexMap::new();
2183                for (k, v) in h.iter() {
2184                    cloned.insert(k.clone(), v.deep_clone());
2185                }
2186                PerlValue::hash(cloned)
2187            }
2188            HeapObject::HashRef(h) => {
2189                let mut cloned = IndexMap::new();
2190                for (k, v) in h.read().iter() {
2191                    cloned.insert(k.clone(), v.deep_clone());
2192                }
2193                PerlValue::hash_ref(Arc::new(RwLock::new(cloned)))
2194            }
2195            HeapObject::StructInst(s) => {
2196                let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
2197                PerlValue::struct_inst(Arc::new(StructInstance::new(
2198                    Arc::clone(&s.def),
2199                    new_values,
2200                )))
2201            }
2202            _ => self.clone(),
2203        }
2204    }
2205
2206    pub fn to_list(&self) -> Vec<PerlValue> {
2207        if nanbox::is_imm_undef(self.0) {
2208            return vec![];
2209        }
2210        if !nanbox::is_heap(self.0) {
2211            return vec![self.clone()];
2212        }
2213        match unsafe { self.heap_ref() } {
2214            HeapObject::Array(a) => a.clone(),
2215            HeapObject::Hash(h) => h
2216                .iter()
2217                .flat_map(|(k, v)| vec![PerlValue::string(k.clone()), v.clone()])
2218                .collect(),
2219            HeapObject::Atomic(arc) => arc.lock().to_list(),
2220            HeapObject::Set(s) => s.values().cloned().collect(),
2221            HeapObject::Deque(d) => d.lock().iter().cloned().collect(),
2222            HeapObject::Iterator(it) => {
2223                let mut out = Vec::new();
2224                while let Some(v) = it.next_item() {
2225                    out.push(v);
2226                }
2227                out
2228            }
2229            _ => vec![self.clone()],
2230        }
2231    }
2232
2233    pub fn scalar_context(&self) -> PerlValue {
2234        if !nanbox::is_heap(self.0) {
2235            return self.clone();
2236        }
2237        if let Some(arc) = self.as_atomic_arc() {
2238            return arc.lock().scalar_context();
2239        }
2240        match unsafe { self.heap_ref() } {
2241            HeapObject::Array(a) => PerlValue::integer(a.len() as i64),
2242            HeapObject::Hash(h) => {
2243                if h.is_empty() {
2244                    PerlValue::integer(0)
2245                } else {
2246                    PerlValue::string(format!("{}/{}", h.len(), h.capacity()))
2247                }
2248            }
2249            HeapObject::Set(s) => PerlValue::integer(s.len() as i64),
2250            HeapObject::Deque(d) => PerlValue::integer(d.lock().len() as i64),
2251            HeapObject::Heap(h) => PerlValue::integer(h.lock().items.len() as i64),
2252            HeapObject::Pipeline(p) => PerlValue::integer(p.lock().source.len() as i64),
2253            HeapObject::Capture(_)
2254            | HeapObject::Ppool(_)
2255            | HeapObject::RemoteCluster(_)
2256            | HeapObject::Barrier(_) => PerlValue::integer(1),
2257            HeapObject::Generator(_) => PerlValue::integer(1),
2258            _ => self.clone(),
2259        }
2260    }
2261}
2262
2263impl fmt::Display for PerlValue {
2264    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2265        if nanbox::is_imm_undef(self.0) {
2266            return Ok(());
2267        }
2268        if let Some(n) = nanbox::as_imm_int32(self.0) {
2269            return write!(f, "{n}");
2270        }
2271        if nanbox::is_raw_float_bits(self.0) {
2272            return write!(f, "{}", format_float(f64::from_bits(self.0)));
2273        }
2274        match unsafe { self.heap_ref() } {
2275            HeapObject::Integer(n) => write!(f, "{n}"),
2276            HeapObject::Float(val) => write!(f, "{}", format_float(*val)),
2277            HeapObject::ErrnoDual { msg, .. } => f.write_str(msg),
2278            HeapObject::String(s) => f.write_str(s),
2279            HeapObject::Bytes(b) => f.write_str(&decode_utf8_or_latin1(b)),
2280            HeapObject::Array(a) => {
2281                for v in a {
2282                    write!(f, "{v}")?;
2283                }
2284                Ok(())
2285            }
2286            HeapObject::Hash(h) => write!(f, "{}/{}", h.len(), h.capacity()),
2287            HeapObject::ArrayRef(_) | HeapObject::ArrayBindingRef(_) => f.write_str("ARRAY(0x...)"),
2288            HeapObject::HashRef(_) | HeapObject::HashBindingRef(_) => f.write_str("HASH(0x...)"),
2289            HeapObject::ScalarRef(_)
2290            | HeapObject::ScalarBindingRef(_)
2291            | HeapObject::CaptureCell(_) => f.write_str("SCALAR(0x...)"),
2292            HeapObject::CodeRef(sub) => write!(f, "CODE({})", sub.name),
2293            HeapObject::Regex(_, src, _) => write!(f, "(?:{src})"),
2294            HeapObject::Blessed(b) => write!(f, "{}=HASH(0x...)", b.class),
2295            HeapObject::IOHandle(name) => f.write_str(name),
2296            HeapObject::Atomic(arc) => write!(f, "{}", arc.lock()),
2297            HeapObject::Set(s) => {
2298                f.write_str("{")?;
2299                if !s.is_empty() {
2300                    let mut iter = s.values();
2301                    if let Some(v) = iter.next() {
2302                        write!(f, "{v}")?;
2303                    }
2304                    for v in iter {
2305                        write!(f, ",{v}")?;
2306                    }
2307                }
2308                f.write_str("}")
2309            }
2310            HeapObject::ChannelTx(_) => f.write_str("PCHANNEL::Tx"),
2311            HeapObject::ChannelRx(_) => f.write_str("PCHANNEL::Rx"),
2312            HeapObject::AsyncTask(_) => f.write_str("AsyncTask"),
2313            HeapObject::Generator(g) => write!(f, "Generator({} stmts)", g.block.len()),
2314            HeapObject::Deque(d) => write!(f, "Deque({})", d.lock().len()),
2315            HeapObject::Heap(h) => write!(f, "Heap({})", h.lock().items.len()),
2316            HeapObject::Pipeline(p) => {
2317                let g = p.lock();
2318                write!(f, "Pipeline({} ops)", g.ops.len())
2319            }
2320            HeapObject::Capture(c) => write!(f, "Capture(exit={})", c.exitcode),
2321            HeapObject::Ppool(_) => f.write_str("Ppool"),
2322            HeapObject::RemoteCluster(c) => write!(f, "Cluster({} slots)", c.slots.len()),
2323            HeapObject::Barrier(_) => f.write_str("Barrier"),
2324            HeapObject::SqliteConn(_) => f.write_str("SqliteConn"),
2325            HeapObject::StructInst(s) => {
2326                // Smart stringify: Point(x => 1.5, y => 2.0)
2327                write!(f, "{}(", s.def.name)?;
2328                let values = s.values.read();
2329                for (i, field) in s.def.fields.iter().enumerate() {
2330                    if i > 0 {
2331                        f.write_str(", ")?;
2332                    }
2333                    write!(
2334                        f,
2335                        "{} => {}",
2336                        field.name,
2337                        values.get(i).cloned().unwrap_or(PerlValue::UNDEF)
2338                    )?;
2339                }
2340                f.write_str(")")
2341            }
2342            HeapObject::EnumInst(e) => {
2343                // Smart stringify: Color::Red or Maybe::Some(value)
2344                write!(f, "{}::{}", e.def.name, e.variant_name())?;
2345                if e.def.variants[e.variant_idx].ty.is_some() {
2346                    write!(f, "({})", e.data)?;
2347                }
2348                Ok(())
2349            }
2350            HeapObject::ClassInst(c) => {
2351                // Smart stringify: Dog(name => "Rex", age => 5)
2352                write!(f, "{}(", c.def.name)?;
2353                let values = c.values.read();
2354                for (i, field) in c.def.fields.iter().enumerate() {
2355                    if i > 0 {
2356                        f.write_str(", ")?;
2357                    }
2358                    write!(
2359                        f,
2360                        "{} => {}",
2361                        field.name,
2362                        values.get(i).cloned().unwrap_or(PerlValue::UNDEF)
2363                    )?;
2364                }
2365                f.write_str(")")
2366            }
2367            HeapObject::DataFrame(d) => {
2368                let g = d.lock();
2369                write!(f, "DataFrame({} rows)", g.nrows())
2370            }
2371            HeapObject::Iterator(_) => f.write_str("Iterator"),
2372        }
2373    }
2374}
2375
2376/// Stable key for set membership (dedup of `PerlValue` in this runtime).
2377pub fn set_member_key(v: &PerlValue) -> String {
2378    if nanbox::is_imm_undef(v.0) {
2379        return "u:".to_string();
2380    }
2381    if let Some(n) = nanbox::as_imm_int32(v.0) {
2382        return format!("i:{n}");
2383    }
2384    if nanbox::is_raw_float_bits(v.0) {
2385        return format!("f:{}", f64::from_bits(v.0).to_bits());
2386    }
2387    match unsafe { v.heap_ref() } {
2388        HeapObject::String(s) => format!("s:{s}"),
2389        HeapObject::Bytes(b) => {
2390            use std::fmt::Write as _;
2391            let mut h = String::with_capacity(b.len() * 2);
2392            for &x in b.iter() {
2393                let _ = write!(&mut h, "{:02x}", x);
2394            }
2395            format!("by:{h}")
2396        }
2397        HeapObject::Array(a) => {
2398            let parts: Vec<_> = a.iter().map(set_member_key).collect();
2399            format!("a:{}", parts.join(","))
2400        }
2401        HeapObject::Hash(h) => {
2402            let mut keys: Vec<_> = h.keys().cloned().collect();
2403            keys.sort();
2404            let parts: Vec<_> = keys
2405                .iter()
2406                .map(|k| format!("{}={}", k, set_member_key(h.get(k).unwrap())))
2407                .collect();
2408            format!("h:{}", parts.join(","))
2409        }
2410        HeapObject::Set(inner) => {
2411            let mut keys: Vec<_> = inner.keys().cloned().collect();
2412            keys.sort();
2413            format!("S:{}", keys.join(","))
2414        }
2415        HeapObject::ArrayRef(a) => {
2416            let g = a.read();
2417            let parts: Vec<_> = g.iter().map(set_member_key).collect();
2418            format!("ar:{}", parts.join(","))
2419        }
2420        HeapObject::HashRef(h) => {
2421            let g = h.read();
2422            let mut keys: Vec<_> = g.keys().cloned().collect();
2423            keys.sort();
2424            let parts: Vec<_> = keys
2425                .iter()
2426                .map(|k| format!("{}={}", k, set_member_key(g.get(k).unwrap())))
2427                .collect();
2428            format!("hr:{}", parts.join(","))
2429        }
2430        HeapObject::Blessed(b) => {
2431            let d = b.data.read();
2432            format!("b:{}:{}", b.class, set_member_key(&d))
2433        }
2434        HeapObject::ScalarRef(_) | HeapObject::ScalarBindingRef(_) | HeapObject::CaptureCell(_) => {
2435            format!("sr:{v}")
2436        }
2437        HeapObject::ArrayBindingRef(n) => format!("abind:{n}"),
2438        HeapObject::HashBindingRef(n) => format!("hbind:{n}"),
2439        HeapObject::CodeRef(_) => format!("c:{v}"),
2440        HeapObject::Regex(_, src, _) => format!("r:{src}"),
2441        HeapObject::IOHandle(s) => format!("io:{s}"),
2442        HeapObject::Atomic(arc) => format!("at:{}", set_member_key(&arc.lock())),
2443        HeapObject::ChannelTx(tx) => format!("chtx:{:p}", Arc::as_ptr(tx)),
2444        HeapObject::ChannelRx(rx) => format!("chrx:{:p}", Arc::as_ptr(rx)),
2445        HeapObject::AsyncTask(t) => format!("async:{:p}", Arc::as_ptr(t)),
2446        HeapObject::Generator(g) => format!("gen:{:p}", Arc::as_ptr(g)),
2447        HeapObject::Deque(d) => format!("dq:{:p}", Arc::as_ptr(d)),
2448        HeapObject::Heap(h) => format!("hp:{:p}", Arc::as_ptr(h)),
2449        HeapObject::Pipeline(p) => format!("pl:{:p}", Arc::as_ptr(p)),
2450        HeapObject::Capture(c) => format!("cap:{:p}", Arc::as_ptr(c)),
2451        HeapObject::Ppool(p) => format!("pp:{:p}", Arc::as_ptr(&p.0)),
2452        HeapObject::RemoteCluster(c) => format!("rcl:{:p}", Arc::as_ptr(c)),
2453        HeapObject::Barrier(b) => format!("br:{:p}", Arc::as_ptr(&b.0)),
2454        HeapObject::SqliteConn(c) => format!("sql:{:p}", Arc::as_ptr(c)),
2455        HeapObject::StructInst(s) => format!("st:{}:{:?}", s.def.name, s.values),
2456        HeapObject::EnumInst(e) => {
2457            format!("en:{}::{}:{}", e.def.name, e.variant_name(), e.data)
2458        }
2459        HeapObject::ClassInst(c) => format!("cl:{}:{:?}", c.def.name, c.values),
2460        HeapObject::DataFrame(d) => format!("df:{:p}", Arc::as_ptr(d)),
2461        HeapObject::Iterator(_) => "iter".to_string(),
2462        HeapObject::ErrnoDual { code, msg } => format!("e:{code}:{msg}"),
2463        HeapObject::Integer(n) => format!("i:{n}"),
2464        HeapObject::Float(fl) => format!("f:{}", fl.to_bits()),
2465    }
2466}
2467
2468pub fn set_from_elements<I: IntoIterator<Item = PerlValue>>(items: I) -> PerlValue {
2469    let mut map = PerlSet::new();
2470    for v in items {
2471        let k = set_member_key(&v);
2472        map.insert(k, v);
2473    }
2474    PerlValue::set(Arc::new(map))
2475}
2476
2477/// Underlying set for union/intersection, including `mysync $s` (`Atomic` wrapping `Set`).
2478#[inline]
2479pub fn set_payload(v: &PerlValue) -> Option<Arc<PerlSet>> {
2480    if !nanbox::is_heap(v.0) {
2481        return None;
2482    }
2483    match unsafe { v.heap_ref() } {
2484        HeapObject::Set(s) => Some(Arc::clone(s)),
2485        HeapObject::Atomic(a) => set_payload(&a.lock()),
2486        _ => None,
2487    }
2488}
2489
2490pub fn set_union(a: &PerlValue, b: &PerlValue) -> Option<PerlValue> {
2491    let ia = set_payload(a)?;
2492    let ib = set_payload(b)?;
2493    let mut m = (*ia).clone();
2494    for (k, v) in ib.iter() {
2495        m.entry(k.clone()).or_insert_with(|| v.clone());
2496    }
2497    Some(PerlValue::set(Arc::new(m)))
2498}
2499
2500pub fn set_intersection(a: &PerlValue, b: &PerlValue) -> Option<PerlValue> {
2501    let ia = set_payload(a)?;
2502    let ib = set_payload(b)?;
2503    let mut m = PerlSet::new();
2504    for (k, v) in ia.iter() {
2505        if ib.contains_key(k) {
2506            m.insert(k.clone(), v.clone());
2507        }
2508    }
2509    Some(PerlValue::set(Arc::new(m)))
2510}
2511fn parse_number(s: &str) -> f64 {
2512    let s = s.trim();
2513    if s.is_empty() {
2514        return 0.0;
2515    }
2516    // Perl extracts leading numeric portion
2517    let mut end = 0;
2518    let bytes = s.as_bytes();
2519    if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
2520        end += 1;
2521    }
2522    while end < bytes.len() && bytes[end].is_ascii_digit() {
2523        end += 1;
2524    }
2525    if end < bytes.len() && bytes[end] == b'.' {
2526        end += 1;
2527        while end < bytes.len() && bytes[end].is_ascii_digit() {
2528            end += 1;
2529        }
2530    }
2531    if end < bytes.len() && (bytes[end] == b'e' || bytes[end] == b'E') {
2532        end += 1;
2533        if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
2534            end += 1;
2535        }
2536        while end < bytes.len() && bytes[end].is_ascii_digit() {
2537            end += 1;
2538        }
2539    }
2540    if end == 0 {
2541        return 0.0;
2542    }
2543    s[..end].parse::<f64>().unwrap_or(0.0)
2544}
2545
2546fn format_float(f: f64) -> String {
2547    if f.fract() == 0.0 && f.abs() < 1e16 {
2548        format!("{}", f as i64)
2549    } else {
2550        // Perl uses Gconvert which is sprintf("%.15g", f) on most platforms.
2551        let mut buf = [0u8; 64];
2552        unsafe {
2553            libc::snprintf(
2554                buf.as_mut_ptr() as *mut libc::c_char,
2555                buf.len(),
2556                c"%.15g".as_ptr(),
2557                f,
2558            );
2559            std::ffi::CStr::from_ptr(buf.as_ptr() as *const libc::c_char)
2560                .to_string_lossy()
2561                .into_owned()
2562        }
2563    }
2564}
2565
2566/// Result of one magical string increment step in a list-context `..` range (Perl `sv_inc`).
2567#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2568pub(crate) enum PerlListRangeIncOutcome {
2569    Continue,
2570    /// Perl upgraded the scalar to a numeric form (`SvNIOKp`); list range stops after this step.
2571    BecameNumeric,
2572}
2573
2574/// Perl `looks_like_number` / `grok_number` subset: `s` must be **entirely** a numeric string
2575/// (after trim), with no trailing garbage. Used for `RANGE_IS_NUMERIC` in `pp_flop`.
2576fn perl_str_looks_like_number_for_range(s: &str) -> bool {
2577    let t = s.trim();
2578    if t.is_empty() {
2579        return s.is_empty();
2580    }
2581    let b = t.as_bytes();
2582    let mut i = 0usize;
2583    if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2584        i += 1;
2585    }
2586    if i >= b.len() {
2587        return false;
2588    }
2589    let mut saw_digit = false;
2590    while i < b.len() && b[i].is_ascii_digit() {
2591        saw_digit = true;
2592        i += 1;
2593    }
2594    if i < b.len() && b[i] == b'.' {
2595        i += 1;
2596        while i < b.len() && b[i].is_ascii_digit() {
2597            saw_digit = true;
2598            i += 1;
2599        }
2600    }
2601    if !saw_digit {
2602        return false;
2603    }
2604    if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
2605        i += 1;
2606        if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2607            i += 1;
2608        }
2609        let exp0 = i;
2610        while i < b.len() && b[i].is_ascii_digit() {
2611            i += 1;
2612        }
2613        if i == exp0 {
2614            return false;
2615        }
2616    }
2617    i == b.len()
2618}
2619
2620/// Whether list-context `..` uses Perl's **numeric** counting (`pp_flop` `RANGE_IS_NUMERIC`).
2621pub(crate) fn perl_list_range_pair_is_numeric(left: &PerlValue, right: &PerlValue) -> bool {
2622    if left.is_integer_like() || left.is_float_like() {
2623        return true;
2624    }
2625    if !left.is_undef() && !left.is_string_like() {
2626        return true;
2627    }
2628    if right.is_integer_like() || right.is_float_like() {
2629        return true;
2630    }
2631    if !right.is_undef() && !right.is_string_like() {
2632        return true;
2633    }
2634
2635    let left_ok = !left.is_undef();
2636    let right_ok = !right.is_undef();
2637    let left_pok = left.is_string_like();
2638    let left_pv = left.as_str_or_empty();
2639    let right_pv = right.as_str_or_empty();
2640
2641    let left_n = perl_str_looks_like_number_for_range(&left_pv);
2642    let right_n = perl_str_looks_like_number_for_range(&right_pv);
2643
2644    let left_zero_prefix =
2645        left_pok && left_pv.len() > 1 && left_pv.as_bytes().first() == Some(&b'0');
2646
2647    let clause5_left =
2648        (!left_ok && right_ok) || ((!left_ok || left_n) && left_pok && !left_zero_prefix);
2649    clause5_left && (!right_ok || right_n)
2650}
2651
2652/// Magical string `++` for ASCII letter/digit runs (Perl `sv_inc_nomg`, non-EBCDIC).
2653pub(crate) fn perl_magic_string_increment_for_range(s: &mut String) -> PerlListRangeIncOutcome {
2654    if s.is_empty() {
2655        return PerlListRangeIncOutcome::BecameNumeric;
2656    }
2657    let b = s.as_bytes();
2658    let mut i = 0usize;
2659    while i < b.len() && b[i].is_ascii_alphabetic() {
2660        i += 1;
2661    }
2662    while i < b.len() && b[i].is_ascii_digit() {
2663        i += 1;
2664    }
2665    if i < b.len() {
2666        let n = parse_number(s) + 1.0;
2667        *s = format_float(n);
2668        return PerlListRangeIncOutcome::BecameNumeric;
2669    }
2670
2671    let bytes = unsafe { s.as_mut_vec() };
2672    let mut idx = bytes.len() - 1;
2673    loop {
2674        if bytes[idx].is_ascii_digit() {
2675            bytes[idx] += 1;
2676            if bytes[idx] <= b'9' {
2677                return PerlListRangeIncOutcome::Continue;
2678            }
2679            bytes[idx] = b'0';
2680            if idx == 0 {
2681                bytes.insert(0, b'1');
2682                return PerlListRangeIncOutcome::Continue;
2683            }
2684            idx -= 1;
2685        } else {
2686            bytes[idx] = bytes[idx].wrapping_add(1);
2687            if bytes[idx].is_ascii_alphabetic() {
2688                return PerlListRangeIncOutcome::Continue;
2689            }
2690            bytes[idx] = bytes[idx].wrapping_sub(b'z' - b'a' + 1);
2691            if idx == 0 {
2692                let c = bytes[0];
2693                bytes.insert(0, if c.is_ascii_digit() { b'1' } else { c });
2694                return PerlListRangeIncOutcome::Continue;
2695            }
2696            idx -= 1;
2697        }
2698    }
2699}
2700
2701/// Magical string `--` for ASCII letter/digit runs (stryke extension — Perl doesn't have this).
2702/// Returns `None` if we've hit the floor (e.g., "a" can't decrement, "aa" → "z").
2703pub(crate) fn perl_magic_string_decrement_for_range(s: &mut String) -> Option<()> {
2704    if s.is_empty() {
2705        return None;
2706    }
2707    // Validate: must be all alpha then all digit (like increment)
2708    let b = s.as_bytes();
2709    let mut i = 0usize;
2710    while i < b.len() && b[i].is_ascii_alphabetic() {
2711        i += 1;
2712    }
2713    while i < b.len() && b[i].is_ascii_digit() {
2714        i += 1;
2715    }
2716    if i < b.len() {
2717        return None; // Not a pure alpha/digit string
2718    }
2719
2720    let bytes = unsafe { s.as_mut_vec() };
2721    let mut idx = bytes.len() - 1;
2722    loop {
2723        if bytes[idx].is_ascii_digit() {
2724            if bytes[idx] > b'0' {
2725                bytes[idx] -= 1;
2726                return Some(());
2727            }
2728            // Borrow: '0' becomes '9', continue to next position
2729            bytes[idx] = b'9';
2730            if idx == 0 {
2731                // "0" → can't go lower, or "00" → "9" (shrink)
2732                if bytes.len() == 1 {
2733                    bytes[0] = b'0'; // restore, signal floor
2734                    return None;
2735                }
2736                bytes.remove(0);
2737                return Some(());
2738            }
2739            idx -= 1;
2740        } else if bytes[idx].is_ascii_lowercase() {
2741            if bytes[idx] > b'a' {
2742                bytes[idx] -= 1;
2743                return Some(());
2744            }
2745            // Borrow: 'a' becomes 'z', continue to next position
2746            bytes[idx] = b'z';
2747            if idx == 0 {
2748                // "a" can't decrement, "aa" → "z"
2749                if bytes.len() == 1 {
2750                    bytes[0] = b'a'; // restore
2751                    return None;
2752                }
2753                bytes.remove(0);
2754                return Some(());
2755            }
2756            idx -= 1;
2757        } else if bytes[idx].is_ascii_uppercase() {
2758            if bytes[idx] > b'A' {
2759                bytes[idx] -= 1;
2760                return Some(());
2761            }
2762            // Borrow: 'A' becomes 'Z', continue to next position
2763            bytes[idx] = b'Z';
2764            if idx == 0 {
2765                if bytes.len() == 1 {
2766                    bytes[0] = b'A'; // restore
2767                    return None;
2768                }
2769                bytes.remove(0);
2770                return Some(());
2771            }
2772            idx -= 1;
2773        } else {
2774            return None;
2775        }
2776    }
2777}
2778
2779fn perl_list_range_max_bound(right: &str) -> usize {
2780    if right.is_ascii() {
2781        right.len()
2782    } else {
2783        right.chars().count()
2784    }
2785}
2786
2787fn perl_list_range_cur_bound(cur: &str, right_is_ascii: bool) -> usize {
2788    if right_is_ascii {
2789        cur.len()
2790    } else {
2791        cur.chars().count()
2792    }
2793}
2794
2795fn perl_list_range_expand_string_magic(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
2796    let mut cur = from.into_string();
2797    let right = to.into_string();
2798    let right_ascii = right.is_ascii();
2799    let max_bound = perl_list_range_max_bound(&right);
2800    let mut out = Vec::new();
2801    let mut guard = 0usize;
2802    loop {
2803        guard += 1;
2804        if guard > 50_000_000 {
2805            break;
2806        }
2807        let cur_bound = perl_list_range_cur_bound(&cur, right_ascii);
2808        if cur_bound > max_bound {
2809            break;
2810        }
2811        out.push(PerlValue::string(cur.clone()));
2812        if cur == right {
2813            break;
2814        }
2815        match perl_magic_string_increment_for_range(&mut cur) {
2816            PerlListRangeIncOutcome::Continue => {}
2817            PerlListRangeIncOutcome::BecameNumeric => break,
2818        }
2819    }
2820    out
2821}
2822
2823/// Perl list-context `..` (`pp_flop`): numeric counting or magical string sequence.
2824pub(crate) fn perl_list_range_expand(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
2825    if perl_list_range_pair_is_numeric(&from, &to) {
2826        let i = from.to_int();
2827        let j = to.to_int();
2828        if j >= i {
2829            (i..=j).map(PerlValue::integer).collect()
2830        } else {
2831            Vec::new()
2832        }
2833    } else {
2834        perl_list_range_expand_string_magic(from, to)
2835    }
2836}
2837
2838// ═══════════════════════════════════════════════════════════════════════════════
2839// Polymorphic range types — stryke extension (world first!)
2840// ═══════════════════════════════════════════════════════════════════════════════
2841
2842/// Check if string is a valid Roman numeral.
2843fn is_roman_numeral(s: &str) -> bool {
2844    if s.is_empty() {
2845        return false;
2846    }
2847    let upper = s.to_ascii_uppercase();
2848    upper
2849        .chars()
2850        .all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))
2851}
2852
2853/// Check if string is an IPv4 address.
2854fn is_ipv4(s: &str) -> bool {
2855    let parts: Vec<&str> = s.split('.').collect();
2856    parts.len() == 4 && parts.iter().all(|p| p.parse::<u8>().is_ok())
2857}
2858
2859/// Parse IPv4 to u32.
2860fn ipv4_to_u32(s: &str) -> Option<u32> {
2861    let parts: Vec<u8> = s.split('.').filter_map(|p| p.parse().ok()).collect();
2862    if parts.len() != 4 {
2863        return None;
2864    }
2865    Some(
2866        ((parts[0] as u32) << 24)
2867            | ((parts[1] as u32) << 16)
2868            | ((parts[2] as u32) << 8)
2869            | (parts[3] as u32),
2870    )
2871}
2872
2873/// Convert u32 to IPv4 string.
2874fn u32_to_ipv4(n: u32) -> String {
2875    format!(
2876        "{}.{}.{}.{}",
2877        (n >> 24) & 0xFF,
2878        (n >> 16) & 0xFF,
2879        (n >> 8) & 0xFF,
2880        n & 0xFF
2881    )
2882}
2883
2884/// IPv4 range with step.
2885fn ipv4_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
2886    let Some(start) = ipv4_to_u32(from) else {
2887        return vec![];
2888    };
2889    let Some(end) = ipv4_to_u32(to) else {
2890        return vec![];
2891    };
2892    let mut out = Vec::new();
2893    if step > 0 {
2894        let mut cur = start as i64;
2895        while cur <= end as i64 {
2896            out.push(PerlValue::string(u32_to_ipv4(cur as u32)));
2897            cur += step;
2898        }
2899    } else {
2900        let mut cur = start as i64;
2901        while cur >= end as i64 {
2902            out.push(PerlValue::string(u32_to_ipv4(cur as u32)));
2903            cur += step;
2904        }
2905    }
2906    out
2907}
2908
2909/// Check if string is ISO date YYYY-MM-DD.
2910fn is_iso_date(s: &str) -> bool {
2911    if s.len() != 10 {
2912        return false;
2913    }
2914    let parts: Vec<&str> = s.split('-').collect();
2915    parts.len() == 3
2916        && parts[0].len() == 4
2917        && parts[0].parse::<u16>().is_ok()
2918        && parts[1].len() == 2
2919        && parts[1]
2920            .parse::<u8>()
2921            .map(|m| m >= 1 && m <= 12)
2922            .unwrap_or(false)
2923        && parts[2].len() == 2
2924        && parts[2]
2925            .parse::<u8>()
2926            .map(|d| d >= 1 && d <= 31)
2927            .unwrap_or(false)
2928}
2929
2930/// Check if string is YYYY-MM (month range).
2931fn is_year_month(s: &str) -> bool {
2932    if s.len() != 7 {
2933        return false;
2934    }
2935    let parts: Vec<&str> = s.split('-').collect();
2936    parts.len() == 2
2937        && parts[0].len() == 4
2938        && parts[0].parse::<u16>().is_ok()
2939        && parts[1].len() == 2
2940        && parts[1]
2941            .parse::<u8>()
2942            .map(|m| m >= 1 && m <= 12)
2943            .unwrap_or(false)
2944}
2945
2946/// Parse ISO date to (year, month, day).
2947fn parse_iso_date(s: &str) -> Option<(i32, u32, u32)> {
2948    let parts: Vec<&str> = s.split('-').collect();
2949    if parts.len() != 3 {
2950        return None;
2951    }
2952    Some((
2953        parts[0].parse().ok()?,
2954        parts[1].parse().ok()?,
2955        parts[2].parse().ok()?,
2956    ))
2957}
2958
2959/// Parse YYYY-MM to (year, month).
2960fn parse_year_month(s: &str) -> Option<(i32, u32)> {
2961    let parts: Vec<&str> = s.split('-').collect();
2962    if parts.len() != 2 {
2963        return None;
2964    }
2965    Some((parts[0].parse().ok()?, parts[1].parse().ok()?))
2966}
2967
2968/// Days in month (handles leap years).
2969fn days_in_month(year: i32, month: u32) -> u32 {
2970    match month {
2971        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
2972        4 | 6 | 9 | 11 => 30,
2973        2 => {
2974            if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
2975                29
2976            } else {
2977                28
2978            }
2979        }
2980        _ => 30,
2981    }
2982}
2983
2984/// Add days to a date, returning new (year, month, day).
2985fn add_days(mut year: i32, mut month: u32, mut day: u32, mut delta: i64) -> (i32, u32, u32) {
2986    if delta > 0 {
2987        while delta > 0 {
2988            let dim = days_in_month(year, month);
2989            let remaining = dim - day;
2990            if delta <= remaining as i64 {
2991                day += delta as u32;
2992                break;
2993            }
2994            delta -= (remaining + 1) as i64;
2995            day = 1;
2996            month += 1;
2997            if month > 12 {
2998                month = 1;
2999                year += 1;
3000            }
3001        }
3002    } else {
3003        while delta < 0 {
3004            if (-delta) < day as i64 {
3005                day = (day as i64 + delta) as u32;
3006                break;
3007            }
3008            delta += day as i64;
3009            month -= 1;
3010            if month == 0 {
3011                month = 12;
3012                year -= 1;
3013            }
3014            day = days_in_month(year, month);
3015        }
3016    }
3017    (year, month, day)
3018}
3019
3020/// ISO date range with step (step = days).
3021fn iso_date_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3022    let Some((mut y, mut m, mut d)) = parse_iso_date(from) else {
3023        return vec![];
3024    };
3025    let Some((ey, em, ed)) = parse_iso_date(to) else {
3026        return vec![];
3027    };
3028    let mut out = Vec::new();
3029    let mut guard = 0;
3030    if step > 0 {
3031        while (y, m, d) <= (ey, em, ed) && guard < 50_000 {
3032            out.push(PerlValue::string(format!("{:04}-{:02}-{:02}", y, m, d)));
3033            (y, m, d) = add_days(y, m, d, step);
3034            guard += 1;
3035        }
3036    } else {
3037        while (y, m, d) >= (ey, em, ed) && guard < 50_000 {
3038            out.push(PerlValue::string(format!("{:04}-{:02}-{:02}", y, m, d)));
3039            (y, m, d) = add_days(y, m, d, step);
3040            guard += 1;
3041        }
3042    }
3043    out
3044}
3045
3046/// Add months to (year, month).
3047fn add_months(mut year: i32, mut month: u32, delta: i64) -> (i32, u32) {
3048    let total = (year as i64 * 12 + month as i64 - 1) + delta;
3049    year = (total / 12) as i32;
3050    month = ((total % 12) + 1) as u32;
3051    if month == 0 {
3052        month = 12;
3053        year -= 1;
3054    }
3055    (year, month)
3056}
3057
3058/// YYYY-MM range with step (step = months).
3059fn year_month_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3060    let Some((mut y, mut m)) = parse_year_month(from) else {
3061        return vec![];
3062    };
3063    let Some((ey, em)) = parse_year_month(to) else {
3064        return vec![];
3065    };
3066    let mut out = Vec::new();
3067    let mut guard = 0;
3068    if step > 0 {
3069        while (y, m) <= (ey, em) && guard < 50_000 {
3070            out.push(PerlValue::string(format!("{:04}-{:02}", y, m)));
3071            (y, m) = add_months(y, m, step);
3072            guard += 1;
3073        }
3074    } else {
3075        while (y, m) >= (ey, em) && guard < 50_000 {
3076            out.push(PerlValue::string(format!("{:04}-{:02}", y, m)));
3077            (y, m) = add_months(y, m, step);
3078            guard += 1;
3079        }
3080    }
3081    out
3082}
3083
3084/// Check if string looks like HH:MM time.
3085fn is_time_hhmm(s: &str) -> bool {
3086    if s.len() != 5 {
3087        return false;
3088    }
3089    let parts: Vec<&str> = s.split(':').collect();
3090    parts.len() == 2
3091        && parts[0].len() == 2
3092        && parts[0].parse::<u8>().map(|h| h < 24).unwrap_or(false)
3093        && parts[1].len() == 2
3094        && parts[1].parse::<u8>().map(|m| m < 60).unwrap_or(false)
3095}
3096
3097/// Parse HH:MM to minutes since midnight.
3098fn parse_time_hhmm(s: &str) -> Option<i32> {
3099    let parts: Vec<&str> = s.split(':').collect();
3100    if parts.len() != 2 {
3101        return None;
3102    }
3103    let h: i32 = parts[0].parse().ok()?;
3104    let m: i32 = parts[1].parse().ok()?;
3105    Some(h * 60 + m)
3106}
3107
3108/// Minutes to HH:MM string.
3109fn minutes_to_hhmm(mins: i32) -> String {
3110    let h = (mins / 60) % 24;
3111    let m = mins % 60;
3112    format!("{:02}:{:02}", h, m)
3113}
3114
3115/// HH:MM time range with step (step = minutes).
3116fn time_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3117    let Some(start) = parse_time_hhmm(from) else {
3118        return vec![];
3119    };
3120    let Some(end) = parse_time_hhmm(to) else {
3121        return vec![];
3122    };
3123    let mut out = Vec::new();
3124    let mut guard = 0;
3125    if step > 0 {
3126        let mut cur = start;
3127        while cur <= end && guard < 50_000 {
3128            out.push(PerlValue::string(minutes_to_hhmm(cur)));
3129            cur += step as i32;
3130            guard += 1;
3131        }
3132    } else {
3133        let mut cur = start;
3134        while cur >= end && guard < 50_000 {
3135            out.push(PerlValue::string(minutes_to_hhmm(cur)));
3136            cur += step as i32;
3137            guard += 1;
3138        }
3139    }
3140    out
3141}
3142
3143const WEEKDAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
3144const WEEKDAYS_FULL: [&str; 7] = [
3145    "Monday",
3146    "Tuesday",
3147    "Wednesday",
3148    "Thursday",
3149    "Friday",
3150    "Saturday",
3151    "Sunday",
3152];
3153
3154/// Check if string is a weekday name.
3155fn weekday_index(s: &str) -> Option<usize> {
3156    let lower = s.to_ascii_lowercase();
3157    for (i, &d) in WEEKDAYS.iter().enumerate() {
3158        if d.to_ascii_lowercase() == lower {
3159            return Some(i);
3160        }
3161    }
3162    for (i, &d) in WEEKDAYS_FULL.iter().enumerate() {
3163        if d.to_ascii_lowercase() == lower {
3164            return Some(i);
3165        }
3166    }
3167    None
3168}
3169
3170/// Weekday range with step.
3171fn weekday_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3172    let Some(start) = weekday_index(from) else {
3173        return vec![];
3174    };
3175    let Some(end) = weekday_index(to) else {
3176        return vec![];
3177    };
3178    let full = from.len() > 3;
3179    let names = if full { &WEEKDAYS_FULL } else { &WEEKDAYS };
3180    let mut out = Vec::new();
3181    if step > 0 {
3182        let mut cur = start as i64;
3183        let target = if end >= start {
3184            end as i64
3185        } else {
3186            end as i64 + 7
3187        };
3188        while cur <= target {
3189            out.push(PerlValue::string(names[(cur % 7) as usize].to_string()));
3190            cur += step;
3191        }
3192    } else {
3193        let mut cur = start as i64;
3194        let target = if end <= start {
3195            end as i64
3196        } else {
3197            end as i64 - 7
3198        };
3199        while cur >= target {
3200            out.push(PerlValue::string(
3201                names[((cur % 7 + 7) % 7) as usize].to_string(),
3202            ));
3203            cur += step;
3204        }
3205    }
3206    out
3207}
3208
3209const MONTHS: [&str; 12] = [
3210    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
3211];
3212const MONTHS_FULL: [&str; 12] = [
3213    "January",
3214    "February",
3215    "March",
3216    "April",
3217    "May",
3218    "June",
3219    "July",
3220    "August",
3221    "September",
3222    "October",
3223    "November",
3224    "December",
3225];
3226
3227/// Check if string is a month name.
3228fn month_name_index(s: &str) -> Option<usize> {
3229    let lower = s.to_ascii_lowercase();
3230    for (i, &m) in MONTHS.iter().enumerate() {
3231        if m.to_ascii_lowercase() == lower {
3232            return Some(i);
3233        }
3234    }
3235    for (i, &m) in MONTHS_FULL.iter().enumerate() {
3236        if m.to_ascii_lowercase() == lower {
3237            return Some(i);
3238        }
3239    }
3240    None
3241}
3242
3243/// Month name range with step.
3244fn month_name_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3245    let Some(start) = month_name_index(from) else {
3246        return vec![];
3247    };
3248    let Some(end) = month_name_index(to) else {
3249        return vec![];
3250    };
3251    let full = from.len() > 3;
3252    let names = if full { &MONTHS_FULL } else { &MONTHS };
3253    let mut out = Vec::new();
3254    if step > 0 {
3255        let mut cur = start as i64;
3256        let target = if end >= start {
3257            end as i64
3258        } else {
3259            end as i64 + 12
3260        };
3261        while cur <= target {
3262            out.push(PerlValue::string(names[(cur % 12) as usize].to_string()));
3263            cur += step;
3264        }
3265    } else {
3266        let mut cur = start as i64;
3267        let target = if end <= start {
3268            end as i64
3269        } else {
3270            end as i64 - 12
3271        };
3272        while cur >= target {
3273            out.push(PerlValue::string(
3274                names[((cur % 12 + 12) % 12) as usize].to_string(),
3275            ));
3276            cur += step;
3277        }
3278    }
3279    out
3280}
3281
3282/// Check if both operands are float-like (contain decimal point, not date/time/IP).
3283fn is_float_pair(from: &str, to: &str) -> bool {
3284    fn is_float(s: &str) -> bool {
3285        s.contains('.')
3286            && !s.contains(':')
3287            && s.matches('.').count() == 1
3288            && s.parse::<f64>().is_ok()
3289    }
3290    is_float(from) && is_float(to)
3291}
3292
3293/// Float range with step.
3294fn float_range_stepped(from: &str, to: &str, step: f64) -> Vec<PerlValue> {
3295    let Ok(start) = from.parse::<f64>() else {
3296        return vec![];
3297    };
3298    let Ok(end) = to.parse::<f64>() else {
3299        return vec![];
3300    };
3301    let mut out = Vec::new();
3302    let mut guard = 0;
3303    // Use integer counting to avoid floating point accumulation errors
3304    if step > 0.0 {
3305        let mut i = 0i64;
3306        loop {
3307            let cur = start + (i as f64) * step;
3308            if cur > end + step.abs() * f64::EPSILON * 10.0 || guard >= 50_000 {
3309                break;
3310            }
3311            // Round to avoid floating point noise
3312            let rounded = (cur * 1e12).round() / 1e12;
3313            out.push(PerlValue::float(rounded));
3314            i += 1;
3315            guard += 1;
3316        }
3317    } else if step < 0.0 {
3318        let mut i = 0i64;
3319        loop {
3320            let cur = start + (i as f64) * step;
3321            if cur < end - step.abs() * f64::EPSILON * 10.0 || guard >= 50_000 {
3322                break;
3323            }
3324            let rounded = (cur * 1e12).round() / 1e12;
3325            out.push(PerlValue::float(rounded));
3326            i += 1;
3327            guard += 1;
3328        }
3329    }
3330    out
3331}
3332
3333/// Convert Roman numeral string to integer.
3334fn roman_to_int(s: &str) -> Option<i64> {
3335    let upper = s.to_ascii_uppercase();
3336    let mut result = 0i64;
3337    let mut prev = 0i64;
3338    for c in upper.chars().rev() {
3339        let val = match c {
3340            'I' => 1,
3341            'V' => 5,
3342            'X' => 10,
3343            'L' => 50,
3344            'C' => 100,
3345            'D' => 500,
3346            'M' => 1000,
3347            _ => return None,
3348        };
3349        if val < prev {
3350            result -= val;
3351        } else {
3352            result += val;
3353        }
3354        prev = val;
3355    }
3356    if result > 0 {
3357        Some(result)
3358    } else {
3359        None
3360    }
3361}
3362
3363/// Convert integer to Roman numeral string.
3364fn int_to_roman(mut n: i64, lowercase: bool) -> Option<String> {
3365    if n <= 0 || n > 3999 {
3366        return None;
3367    }
3368    let numerals = [
3369        (1000, "M"),
3370        (900, "CM"),
3371        (500, "D"),
3372        (400, "CD"),
3373        (100, "C"),
3374        (90, "XC"),
3375        (50, "L"),
3376        (40, "XL"),
3377        (10, "X"),
3378        (9, "IX"),
3379        (5, "V"),
3380        (4, "IV"),
3381        (1, "I"),
3382    ];
3383    let mut result = String::new();
3384    for (val, sym) in numerals {
3385        while n >= val {
3386            result.push_str(sym);
3387            n -= val;
3388        }
3389    }
3390    if lowercase {
3391        Some(result.to_ascii_lowercase())
3392    } else {
3393        Some(result)
3394    }
3395}
3396
3397/// Expand a Roman numeral range with step.
3398fn roman_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3399    let Some(start) = roman_to_int(from) else {
3400        return vec![];
3401    };
3402    let Some(end) = roman_to_int(to) else {
3403        return vec![];
3404    };
3405    let lowercase = from
3406        .chars()
3407        .next()
3408        .map(|c| c.is_ascii_lowercase())
3409        .unwrap_or(false);
3410
3411    let mut out = Vec::new();
3412    if step > 0 {
3413        let mut cur = start;
3414        while cur <= end {
3415            if let Some(r) = int_to_roman(cur, lowercase) {
3416                out.push(PerlValue::string(r));
3417            }
3418            cur += step;
3419        }
3420    } else {
3421        let mut cur = start;
3422        while cur >= end {
3423            if let Some(r) = int_to_roman(cur, lowercase) {
3424                out.push(PerlValue::string(r));
3425            }
3426            cur += step; // step is negative
3427        }
3428    }
3429    out
3430}
3431
3432/// Stepped range expansion — polymorphic across many types (stryke world first!).
3433/// Supports: integers, floats, strings, Roman numerals, dates, times, weekdays, months, IPv4.
3434pub(crate) fn perl_list_range_expand_stepped(
3435    from: PerlValue,
3436    to: PerlValue,
3437    step_val: PerlValue,
3438) -> Vec<PerlValue> {
3439    let from_str = from.to_string();
3440    let to_str = to.to_string();
3441
3442    // Check if this is a float range (operands have decimal points)
3443    let is_float_range = is_float_pair(&from_str, &to_str);
3444
3445    // Get step as float or int depending on context
3446    let step_float = step_val.as_float().unwrap_or(step_val.to_int() as f64);
3447    let step_int = step_val.to_int();
3448
3449    if step_int == 0 && step_float == 0.0 {
3450        return vec![];
3451    }
3452
3453    // Float ranges use float step
3454    if is_float_range {
3455        return float_range_stepped(&from_str, &to_str, step_float);
3456    }
3457
3458    // Pure numeric integers
3459    if perl_list_range_pair_is_numeric(&from, &to) {
3460        let i = from.to_int();
3461        let j = to.to_int();
3462        if step_int > 0 {
3463            (i..=j)
3464                .step_by(step_int as usize)
3465                .map(PerlValue::integer)
3466                .collect()
3467        } else {
3468            std::iter::successors(Some(i), |&x| {
3469                let next = x + step_int;
3470                if next >= j {
3471                    Some(next)
3472                } else {
3473                    None
3474                }
3475            })
3476            .map(PerlValue::integer)
3477            .collect()
3478        }
3479    } else {
3480        // Check special types in order of specificity
3481
3482        // IPv4 addresses (must check before floats due to dots)
3483        if is_ipv4(&from_str) && is_ipv4(&to_str) {
3484            return ipv4_range_stepped(&from_str, &to_str, step_int);
3485        }
3486
3487        // ISO dates YYYY-MM-DD (step = days)
3488        if is_iso_date(&from_str) && is_iso_date(&to_str) {
3489            return iso_date_range_stepped(&from_str, &to_str, step_int);
3490        }
3491
3492        // Year-month YYYY-MM (step = months)
3493        if is_year_month(&from_str) && is_year_month(&to_str) {
3494            return year_month_range_stepped(&from_str, &to_str, step_int);
3495        }
3496
3497        // Time HH:MM (step = minutes)
3498        if is_time_hhmm(&from_str) && is_time_hhmm(&to_str) {
3499            return time_range_stepped(&from_str, &to_str, step_int);
3500        }
3501
3502        // Weekday names
3503        if weekday_index(&from_str).is_some() && weekday_index(&to_str).is_some() {
3504            return weekday_range_stepped(&from_str, &to_str, step_int);
3505        }
3506
3507        // Month names
3508        if month_name_index(&from_str).is_some() && month_name_index(&to_str).is_some() {
3509            return month_name_range_stepped(&from_str, &to_str, step_int);
3510        }
3511
3512        // Roman numerals
3513        if is_roman_numeral(&from_str) && is_roman_numeral(&to_str) {
3514            return roman_range_stepped(&from_str, &to_str, step_int);
3515        }
3516
3517        // Fall back to magic string increment/decrement
3518        perl_list_range_expand_string_magic_stepped(from, to, step_int)
3519    }
3520}
3521
3522/// Coerce a slice endpoint to a strict integer. Used by [`Op::ArraySliceRange`] —
3523/// non-numeric strings, fractional floats, refs, and other non-integer types die.
3524/// `where_` is the diagnostic context (`"start"`, `"stop"`, `"step"`).
3525pub(crate) fn perl_slice_endpoint_to_strict_int(
3526    v: &PerlValue,
3527    where_: &str,
3528) -> Result<i64, String> {
3529    if let Some(n) = v.as_integer() {
3530        return Ok(n);
3531    }
3532    if let Some(f) = v.as_float() {
3533        if f.is_finite() && f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
3534            return Ok(f as i64);
3535        }
3536        return Err(format!(
3537            "array slice {}: non-integer float endpoint {}",
3538            where_, f
3539        ));
3540    }
3541    let s = v.as_str_or_empty();
3542    if !s.is_empty() {
3543        if let Ok(n) = s.trim().parse::<i64>() {
3544            return Ok(n);
3545        }
3546        return Err(format!(
3547            "array slice {}: non-integer string endpoint {:?}",
3548            where_, s
3549        ));
3550    }
3551    Err(format!(
3552        "array slice {}: endpoint must be an integer (got non-numeric value)",
3553        where_
3554    ))
3555}
3556
3557/// Resolve `from`/`to`/`step` for `@arr[FROM:TO:STEP]` (and open-ended forms) into the
3558/// concrete list of array indices. Closed inclusive on both ends. `Undef` endpoints
3559/// (the omitted-endpoint sentinel emitted by the compiler) default to:
3560/// - `step` → `1`
3561/// - `from` → `0` (positive step) or `arr_len-1` (negative step)
3562/// - `to`   → `arr_len-1` (positive step) or `0` (negative step)
3563///
3564/// Negative explicit indices count from the end (Perl semantics: `-1` = last element).
3565/// Returns `Err(msg)` for non-integer endpoints or zero step — caller dies with that.
3566pub(crate) fn compute_array_slice_indices(
3567    arr_len: i64,
3568    from: &PerlValue,
3569    to: &PerlValue,
3570    step: &PerlValue,
3571) -> Result<Vec<i64>, String> {
3572    let step_i = if step.is_undef() {
3573        1i64
3574    } else {
3575        perl_slice_endpoint_to_strict_int(step, "step")?
3576    };
3577    if step_i == 0 {
3578        return Err("array slice step cannot be 0".into());
3579    }
3580
3581    let normalize = |i: i64| -> i64 {
3582        if i < 0 {
3583            i + arr_len
3584        } else {
3585            i
3586        }
3587    };
3588
3589    // Open-ended slice (`@a[..3]`, `@a[-3..]`) is a stryke extension where each
3590    // explicit endpoint wraps once from the end. Closed `Range` slices
3591    // (`@a[0..-1]`, `@a[3..-1]`, `@a[-3..-1]`) follow Perl's raw-integer range
3592    // semantics: `0..-1` is empty, `-3..-1` is `(-3, -2, -1)`, and each
3593    // generated integer wraps individually when looked up.
3594    let any_undef = from.is_undef() || to.is_undef();
3595
3596    let from_raw = if from.is_undef() {
3597        if step_i > 0 {
3598            0
3599        } else {
3600            arr_len - 1
3601        }
3602    } else {
3603        perl_slice_endpoint_to_strict_int(from, "start")?
3604    };
3605
3606    let to_raw = if to.is_undef() {
3607        if step_i > 0 {
3608            arr_len - 1
3609        } else {
3610            0
3611        }
3612    } else {
3613        perl_slice_endpoint_to_strict_int(to, "stop")?
3614    };
3615
3616    let mut out = Vec::new();
3617    if arr_len == 0 {
3618        return Ok(out);
3619    }
3620
3621    let (from_i, to_i) = if any_undef {
3622        (normalize(from_raw), normalize(to_raw))
3623    } else {
3624        (from_raw, to_raw)
3625    };
3626
3627    if step_i > 0 {
3628        let mut i = from_i;
3629        while i <= to_i {
3630            out.push(if any_undef { i } else { normalize(i) });
3631            i += step_i;
3632        }
3633    } else {
3634        let mut i = from_i;
3635        while i >= to_i {
3636            out.push(if any_undef { i } else { normalize(i) });
3637            i += step_i; // step_i is negative
3638        }
3639    }
3640    Ok(out)
3641}
3642
3643/// Resolve `from`/`to`/`step` for `@h{FROM:TO:STEP}` into the concrete list of hash keys.
3644/// Both endpoints must be present (open-ended forms are nonsense for unordered hashes
3645/// and die). Endpoints stringify to keys; expansion uses the polymorphic stepped-range
3646/// machinery (numeric, magic-string-increment, Roman, etc.).
3647pub(crate) fn compute_hash_slice_keys(
3648    from: &PerlValue,
3649    to: &PerlValue,
3650    step: &PerlValue,
3651) -> Result<Vec<String>, String> {
3652    if from.is_undef() || to.is_undef() {
3653        return Err(
3654            "hash slice range requires both endpoints (open-ended forms not allowed)".into(),
3655        );
3656    }
3657    let step_val = if step.is_undef() {
3658        PerlValue::integer(1)
3659    } else {
3660        step.clone()
3661    };
3662    let expanded = perl_list_range_expand_stepped(from.clone(), to.clone(), step_val);
3663    Ok(expanded.into_iter().map(|v| v.to_string()).collect())
3664}
3665
3666fn perl_list_range_expand_string_magic_stepped(
3667    from: PerlValue,
3668    to: PerlValue,
3669    step: i64,
3670) -> Vec<PerlValue> {
3671    if step == 0 {
3672        return vec![];
3673    }
3674    let mut cur = from.into_string();
3675    let right = to.into_string();
3676
3677    if step > 0 {
3678        // Forward iteration
3679        let step = step as usize;
3680        let right_ascii = right.is_ascii();
3681        let max_bound = perl_list_range_max_bound(&right);
3682        let mut out = Vec::new();
3683        let mut guard = 0usize;
3684        let mut idx = 0usize;
3685        loop {
3686            guard += 1;
3687            if guard > 50_000_000 {
3688                break;
3689            }
3690            let cur_bound = perl_list_range_cur_bound(&cur, right_ascii);
3691            if cur_bound > max_bound {
3692                break;
3693            }
3694            if idx % step == 0 {
3695                out.push(PerlValue::string(cur.clone()));
3696            }
3697            if cur == right {
3698                break;
3699            }
3700            match perl_magic_string_increment_for_range(&mut cur) {
3701                PerlListRangeIncOutcome::Continue => {}
3702                PerlListRangeIncOutcome::BecameNumeric => break,
3703            }
3704            idx += 1;
3705        }
3706        out
3707    } else {
3708        // Reverse iteration (stryke extension)
3709        let step = (-step) as usize;
3710        let mut out = Vec::new();
3711        let mut guard = 0usize;
3712        let mut idx = 0usize;
3713        loop {
3714            guard += 1;
3715            if guard > 50_000_000 {
3716                break;
3717            }
3718            if idx % step == 0 {
3719                out.push(PerlValue::string(cur.clone()));
3720            }
3721            if cur == right {
3722                break;
3723            }
3724            // Check if we've gone past the target (cur < right lexicographically)
3725            if cur < right {
3726                break;
3727            }
3728            match perl_magic_string_decrement_for_range(&mut cur) {
3729                Some(()) => {}
3730                None => break, // Hit floor
3731            }
3732            idx += 1;
3733        }
3734        out
3735    }
3736}
3737
3738impl PerlDataFrame {
3739    /// One row as a hashref (`$_` in `filter`).
3740    pub fn row_hashref(&self, row: usize) -> PerlValue {
3741        let mut m = IndexMap::new();
3742        for (i, col) in self.columns.iter().enumerate() {
3743            m.insert(
3744                col.clone(),
3745                self.cols[i].get(row).cloned().unwrap_or(PerlValue::UNDEF),
3746            );
3747        }
3748        PerlValue::hash_ref(Arc::new(RwLock::new(m)))
3749    }
3750}
3751
3752#[cfg(test)]
3753mod tests {
3754    use super::PerlValue;
3755    use crate::perl_regex::PerlCompiledRegex;
3756    use indexmap::IndexMap;
3757    use parking_lot::RwLock;
3758    use std::cmp::Ordering;
3759    use std::sync::Arc;
3760
3761    #[test]
3762    fn undef_is_false() {
3763        assert!(!PerlValue::UNDEF.is_true());
3764    }
3765
3766    #[test]
3767    fn string_zero_is_false() {
3768        assert!(!PerlValue::string("0".into()).is_true());
3769        assert!(PerlValue::string("00".into()).is_true());
3770    }
3771
3772    #[test]
3773    fn empty_string_is_false() {
3774        assert!(!PerlValue::string(String::new()).is_true());
3775    }
3776
3777    #[test]
3778    fn integer_zero_is_false_nonzero_true() {
3779        assert!(!PerlValue::integer(0).is_true());
3780        assert!(PerlValue::integer(-1).is_true());
3781    }
3782
3783    #[test]
3784    fn float_zero_is_false_nonzero_true() {
3785        assert!(!PerlValue::float(0.0).is_true());
3786        assert!(PerlValue::float(0.1).is_true());
3787    }
3788
3789    #[test]
3790    fn num_cmp_orders_float_against_integer() {
3791        assert_eq!(
3792            PerlValue::float(2.5).num_cmp(&PerlValue::integer(3)),
3793            Ordering::Less
3794        );
3795    }
3796
3797    #[test]
3798    fn to_int_parses_leading_number_from_string() {
3799        assert_eq!(PerlValue::string("42xyz".into()).to_int(), 42);
3800        assert_eq!(PerlValue::string("  -3.7foo".into()).to_int(), -3);
3801    }
3802
3803    #[test]
3804    fn num_cmp_orders_as_numeric() {
3805        assert_eq!(
3806            PerlValue::integer(2).num_cmp(&PerlValue::integer(11)),
3807            Ordering::Less
3808        );
3809        assert_eq!(
3810            PerlValue::string("2foo".into()).num_cmp(&PerlValue::string("11".into())),
3811            Ordering::Less
3812        );
3813    }
3814
3815    #[test]
3816    fn str_cmp_orders_as_strings() {
3817        assert_eq!(
3818            PerlValue::string("2".into()).str_cmp(&PerlValue::string("11".into())),
3819            Ordering::Greater
3820        );
3821    }
3822
3823    #[test]
3824    fn str_eq_heap_strings_fast_path() {
3825        let a = PerlValue::string("hello".into());
3826        let b = PerlValue::string("hello".into());
3827        assert!(a.str_eq(&b));
3828        assert!(!a.str_eq(&PerlValue::string("hell".into())));
3829    }
3830
3831    #[test]
3832    fn str_eq_fallback_matches_stringified_equality() {
3833        let n = PerlValue::integer(42);
3834        let s = PerlValue::string("42".into());
3835        assert!(n.str_eq(&s));
3836        assert!(!PerlValue::integer(1).str_eq(&PerlValue::string("2".into())));
3837    }
3838
3839    #[test]
3840    fn str_cmp_heap_strings_fast_path() {
3841        assert_eq!(
3842            PerlValue::string("a".into()).str_cmp(&PerlValue::string("b".into())),
3843            Ordering::Less
3844        );
3845    }
3846
3847    #[test]
3848    fn scalar_context_array_and_hash() {
3849        let a =
3850            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).scalar_context();
3851        assert_eq!(a.to_int(), 2);
3852        let mut h = IndexMap::new();
3853        h.insert("a".into(), PerlValue::integer(1));
3854        let sc = PerlValue::hash(h).scalar_context();
3855        assert!(sc.is_string_like());
3856    }
3857
3858    #[test]
3859    fn to_list_array_hash_and_scalar() {
3860        assert_eq!(
3861            PerlValue::array(vec![PerlValue::integer(7)])
3862                .to_list()
3863                .len(),
3864            1
3865        );
3866        let mut h = IndexMap::new();
3867        h.insert("k".into(), PerlValue::integer(1));
3868        let list = PerlValue::hash(h).to_list();
3869        assert_eq!(list.len(), 2);
3870        let one = PerlValue::integer(99).to_list();
3871        assert_eq!(one.len(), 1);
3872        assert_eq!(one[0].to_int(), 99);
3873    }
3874
3875    #[test]
3876    fn type_name_and_ref_type_for_core_kinds() {
3877        assert_eq!(PerlValue::integer(0).type_name(), "INTEGER");
3878        assert_eq!(PerlValue::UNDEF.ref_type().to_string(), "");
3879        assert_eq!(
3880            PerlValue::array_ref(Arc::new(RwLock::new(vec![])))
3881                .ref_type()
3882                .to_string(),
3883            "ARRAY"
3884        );
3885    }
3886
3887    #[test]
3888    fn display_undef_is_empty_integer_is_decimal() {
3889        assert_eq!(PerlValue::UNDEF.to_string(), "");
3890        assert_eq!(PerlValue::integer(-7).to_string(), "-7");
3891    }
3892
3893    #[test]
3894    fn empty_array_is_false_nonempty_is_true() {
3895        assert!(!PerlValue::array(vec![]).is_true());
3896        assert!(PerlValue::array(vec![PerlValue::integer(0)]).is_true());
3897    }
3898
3899    #[test]
3900    fn to_number_undef_and_non_numeric_refs_are_zero() {
3901        use super::PerlSub;
3902
3903        assert_eq!(PerlValue::UNDEF.to_number(), 0.0);
3904        assert_eq!(
3905            PerlValue::code_ref(Arc::new(PerlSub {
3906                name: "f".into(),
3907                params: vec![],
3908                body: vec![],
3909                closure_env: None,
3910                prototype: None,
3911                fib_like: None,
3912            }))
3913            .to_number(),
3914            0.0
3915        );
3916    }
3917
3918    #[test]
3919    fn append_to_builds_string_without_extra_alloc_for_int_and_string() {
3920        let mut buf = String::new();
3921        PerlValue::integer(-12).append_to(&mut buf);
3922        PerlValue::string("ab".into()).append_to(&mut buf);
3923        assert_eq!(buf, "-12ab");
3924        let mut u = String::new();
3925        PerlValue::UNDEF.append_to(&mut u);
3926        assert!(u.is_empty());
3927    }
3928
3929    #[test]
3930    fn append_to_atomic_delegates_to_inner() {
3931        use parking_lot::Mutex;
3932        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::string("z".into()))));
3933        let mut buf = String::new();
3934        a.append_to(&mut buf);
3935        assert_eq!(buf, "z");
3936    }
3937
3938    #[test]
3939    fn unwrap_atomic_reads_inner_other_variants_clone() {
3940        use parking_lot::Mutex;
3941        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(9))));
3942        assert_eq!(a.unwrap_atomic().to_int(), 9);
3943        assert_eq!(PerlValue::integer(3).unwrap_atomic().to_int(), 3);
3944    }
3945
3946    #[test]
3947    fn is_atomic_only_true_for_atomic_variant() {
3948        use parking_lot::Mutex;
3949        assert!(PerlValue::atomic(Arc::new(Mutex::new(PerlValue::UNDEF))).is_atomic());
3950        assert!(!PerlValue::integer(0).is_atomic());
3951    }
3952
3953    #[test]
3954    fn as_str_only_on_string_variant() {
3955        assert_eq!(
3956            PerlValue::string("x".into()).as_str(),
3957            Some("x".to_string())
3958        );
3959        assert_eq!(PerlValue::integer(1).as_str(), None);
3960    }
3961
3962    #[test]
3963    fn as_str_or_empty_defaults_non_string() {
3964        assert_eq!(PerlValue::string("z".into()).as_str_or_empty(), "z");
3965        assert_eq!(PerlValue::integer(1).as_str_or_empty(), "");
3966    }
3967
3968    #[test]
3969    fn to_int_truncates_float_toward_zero() {
3970        assert_eq!(PerlValue::float(3.9).to_int(), 3);
3971        assert_eq!(PerlValue::float(-2.1).to_int(), -2);
3972    }
3973
3974    #[test]
3975    fn to_number_array_is_length() {
3976        assert_eq!(
3977            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).to_number(),
3978            2.0
3979        );
3980    }
3981
3982    #[test]
3983    fn scalar_context_empty_hash_is_zero() {
3984        let h = IndexMap::new();
3985        assert_eq!(PerlValue::hash(h).scalar_context().to_int(), 0);
3986    }
3987
3988    #[test]
3989    fn scalar_context_nonhash_nonarray_clones() {
3990        let v = PerlValue::integer(8);
3991        assert_eq!(v.scalar_context().to_int(), 8);
3992    }
3993
3994    #[test]
3995    fn display_float_integer_like_omits_decimal() {
3996        assert_eq!(PerlValue::float(4.0).to_string(), "4");
3997    }
3998
3999    #[test]
4000    fn display_array_concatenates_element_displays() {
4001        let a = PerlValue::array(vec![PerlValue::integer(1), PerlValue::string("b".into())]);
4002        assert_eq!(a.to_string(), "1b");
4003    }
4004
4005    #[test]
4006    fn display_code_ref_includes_sub_name() {
4007        use super::PerlSub;
4008        let c = PerlValue::code_ref(Arc::new(PerlSub {
4009            name: "foo".into(),
4010            params: vec![],
4011            body: vec![],
4012            closure_env: None,
4013            prototype: None,
4014            fib_like: None,
4015        }));
4016        assert!(c.to_string().contains("foo"));
4017    }
4018
4019    #[test]
4020    fn display_regex_shows_non_capturing_prefix() {
4021        let r = PerlValue::regex(
4022            PerlCompiledRegex::compile("x+").unwrap(),
4023            "x+".into(),
4024            "".into(),
4025        );
4026        assert_eq!(r.to_string(), "(?:x+)");
4027    }
4028
4029    #[test]
4030    fn display_iohandle_is_name() {
4031        assert_eq!(PerlValue::io_handle("STDOUT".into()).to_string(), "STDOUT");
4032    }
4033
4034    #[test]
4035    fn ref_type_blessed_uses_class_name() {
4036        let b = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
4037            "Pkg".into(),
4038            PerlValue::UNDEF,
4039        )));
4040        assert_eq!(b.ref_type().to_string(), "Pkg");
4041    }
4042
4043    #[test]
4044    fn blessed_drop_enqueues_pending_destroy() {
4045        let v = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
4046            "Z".into(),
4047            PerlValue::integer(7),
4048        )));
4049        drop(v);
4050        let q = crate::pending_destroy::take_queue();
4051        assert_eq!(q.len(), 1);
4052        assert_eq!(q[0].0, "Z");
4053        assert_eq!(q[0].1.to_int(), 7);
4054    }
4055
4056    #[test]
4057    fn type_name_iohandle_is_glob() {
4058        assert_eq!(PerlValue::io_handle("FH".into()).type_name(), "GLOB");
4059    }
4060
4061    #[test]
4062    fn empty_hash_is_false() {
4063        assert!(!PerlValue::hash(IndexMap::new()).is_true());
4064    }
4065
4066    #[test]
4067    fn hash_nonempty_is_true() {
4068        let mut h = IndexMap::new();
4069        h.insert("k".into(), PerlValue::UNDEF);
4070        assert!(PerlValue::hash(h).is_true());
4071    }
4072
4073    #[test]
4074    fn num_cmp_equal_integers() {
4075        assert_eq!(
4076            PerlValue::integer(5).num_cmp(&PerlValue::integer(5)),
4077            Ordering::Equal
4078        );
4079    }
4080
4081    #[test]
4082    fn str_cmp_compares_lexicographic_string_forms() {
4083        // Display forms "2" and "10" — string order differs from numeric order.
4084        assert_eq!(
4085            PerlValue::integer(2).str_cmp(&PerlValue::integer(10)),
4086            Ordering::Greater
4087        );
4088    }
4089
4090    #[test]
4091    fn to_list_undef_empty() {
4092        assert!(PerlValue::UNDEF.to_list().is_empty());
4093    }
4094
4095    #[test]
4096    fn unwrap_atomic_nested_atomic() {
4097        use parking_lot::Mutex;
4098        let inner = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(2))));
4099        let outer = PerlValue::atomic(Arc::new(Mutex::new(inner)));
4100        assert_eq!(outer.unwrap_atomic().to_int(), 2);
4101    }
4102
4103    #[test]
4104    fn errno_dual_parts_extracts_code_and_message() {
4105        let v = PerlValue::errno_dual(-2, "oops".into());
4106        assert_eq!(v.errno_dual_parts(), Some((-2, "oops".into())));
4107    }
4108
4109    #[test]
4110    fn errno_dual_parts_none_for_plain_string() {
4111        assert!(PerlValue::string("hi".into()).errno_dual_parts().is_none());
4112    }
4113
4114    #[test]
4115    fn errno_dual_parts_none_for_integer() {
4116        assert!(PerlValue::integer(1).errno_dual_parts().is_none());
4117    }
4118
4119    #[test]
4120    fn errno_dual_numeric_context_uses_code_string_uses_msg() {
4121        let v = PerlValue::errno_dual(5, "five".into());
4122        assert_eq!(v.to_int(), 5);
4123        assert_eq!(v.to_string(), "five");
4124    }
4125
4126    #[test]
4127    fn list_range_alpha_joins_like_perl() {
4128        use super::perl_list_range_expand;
4129        let v =
4130            perl_list_range_expand(PerlValue::string("a".into()), PerlValue::string("z".into()));
4131        let s: String = v.iter().map(|x| x.to_string()).collect();
4132        assert_eq!(s, "abcdefghijklmnopqrstuvwxyz");
4133    }
4134
4135    #[test]
4136    fn list_range_numeric_string_endpoints() {
4137        use super::perl_list_range_expand;
4138        let v = perl_list_range_expand(
4139            PerlValue::string("9".into()),
4140            PerlValue::string("11".into()),
4141        );
4142        assert_eq!(v.len(), 3);
4143        assert_eq!(
4144            v.iter().map(|x| x.to_int()).collect::<Vec<_>>(),
4145            vec![9, 10, 11]
4146        );
4147    }
4148
4149    #[test]
4150    fn list_range_leading_zero_is_string_mode() {
4151        use super::perl_list_range_expand;
4152        let v = perl_list_range_expand(
4153            PerlValue::string("01".into()),
4154            PerlValue::string("05".into()),
4155        );
4156        assert_eq!(v.len(), 5);
4157        assert_eq!(
4158            v.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
4159            vec!["01", "02", "03", "04", "05"]
4160        );
4161    }
4162
4163    #[test]
4164    fn list_range_empty_to_letter_one_element() {
4165        use super::perl_list_range_expand;
4166        let v = perl_list_range_expand(
4167            PerlValue::string(String::new()),
4168            PerlValue::string("c".into()),
4169        );
4170        assert_eq!(v.len(), 1);
4171        assert_eq!(v[0].to_string(), "");
4172    }
4173
4174    #[test]
4175    fn magic_string_inc_z_wraps_aa() {
4176        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
4177        let mut s = "z".to_string();
4178        assert_eq!(
4179            perl_magic_string_increment_for_range(&mut s),
4180            PerlListRangeIncOutcome::Continue
4181        );
4182        assert_eq!(s, "aa");
4183    }
4184
4185    #[test]
4186    fn test_boxed_numeric_stringification() {
4187        // Large integer outside i32 range
4188        let large_int = 10_000_000_000i64;
4189        let v_int = PerlValue::integer(large_int);
4190        assert_eq!(v_int.to_string(), "10000000000");
4191
4192        // Float that needs boxing (e.g. Infinity)
4193        let v_inf = PerlValue::float(f64::INFINITY);
4194        assert_eq!(v_inf.to_string(), "inf");
4195    }
4196
4197    #[test]
4198    fn magic_string_inc_nine_to_ten() {
4199        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
4200        let mut s = "9".to_string();
4201        assert_eq!(
4202            perl_magic_string_increment_for_range(&mut s),
4203            PerlListRangeIncOutcome::Continue
4204        );
4205        assert_eq!(s, "10");
4206    }
4207}