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(_) | HeapObject::ScalarBindingRef(_) | HeapObject::CaptureCell(_) => "SCALAR".to_string(),
2017            HeapObject::CodeRef(_) => "CODE".to_string(),
2018            HeapObject::Regex(_, _, _) => "Regexp".to_string(),
2019            HeapObject::Blessed(b) => b.class.clone(),
2020            HeapObject::IOHandle(_) => "GLOB".to_string(),
2021            HeapObject::Atomic(_) => "ATOMIC".to_string(),
2022            HeapObject::Set(_) => "Set".to_string(),
2023            HeapObject::ChannelTx(_) => "PCHANNEL::Tx".to_string(),
2024            HeapObject::ChannelRx(_) => "PCHANNEL::Rx".to_string(),
2025            HeapObject::AsyncTask(_) => "ASYNCTASK".to_string(),
2026            HeapObject::Generator(_) => "Generator".to_string(),
2027            HeapObject::Deque(_) => "Deque".to_string(),
2028            HeapObject::Heap(_) => "Heap".to_string(),
2029            HeapObject::Pipeline(_) => "Pipeline".to_string(),
2030            HeapObject::DataFrame(_) => "DataFrame".to_string(),
2031            HeapObject::Capture(_) => "Capture".to_string(),
2032            HeapObject::Ppool(_) => "Ppool".to_string(),
2033            HeapObject::RemoteCluster(_) => "Cluster".to_string(),
2034            HeapObject::Barrier(_) => "Barrier".to_string(),
2035            HeapObject::SqliteConn(_) => "SqliteConn".to_string(),
2036            HeapObject::StructInst(s) => s.def.name.to_string(),
2037            HeapObject::EnumInst(e) => e.def.name.to_string(),
2038            HeapObject::ClassInst(c) => c.def.name.to_string(),
2039            HeapObject::Iterator(_) => "Iterator".to_string(),
2040            HeapObject::ErrnoDual { .. } => "Errno".to_string(),
2041            HeapObject::Integer(_) => "INTEGER".to_string(),
2042            HeapObject::Float(_) => "FLOAT".to_string(),
2043        }
2044    }
2045
2046    pub fn ref_type(&self) -> PerlValue {
2047        if !nanbox::is_heap(self.0) {
2048            return PerlValue::string(String::new());
2049        }
2050        match unsafe { self.heap_ref() } {
2051            HeapObject::ArrayRef(_) | HeapObject::ArrayBindingRef(_) => {
2052                PerlValue::string("ARRAY".into())
2053            }
2054            HeapObject::HashRef(_) | HeapObject::HashBindingRef(_) => {
2055                PerlValue::string("HASH".into())
2056            }
2057            HeapObject::ScalarRef(_) | HeapObject::ScalarBindingRef(_) => {
2058                PerlValue::string("SCALAR".into())
2059            }
2060            HeapObject::CodeRef(_) => PerlValue::string("CODE".into()),
2061            HeapObject::Regex(_, _, _) => PerlValue::string("Regexp".into()),
2062            HeapObject::Atomic(_) => PerlValue::string("ATOMIC".into()),
2063            HeapObject::Set(_) => PerlValue::string("Set".into()),
2064            HeapObject::ChannelTx(_) => PerlValue::string("PCHANNEL::Tx".into()),
2065            HeapObject::ChannelRx(_) => PerlValue::string("PCHANNEL::Rx".into()),
2066            HeapObject::AsyncTask(_) => PerlValue::string("ASYNCTASK".into()),
2067            HeapObject::Generator(_) => PerlValue::string("Generator".into()),
2068            HeapObject::Deque(_) => PerlValue::string("Deque".into()),
2069            HeapObject::Heap(_) => PerlValue::string("Heap".into()),
2070            HeapObject::Pipeline(_) => PerlValue::string("Pipeline".into()),
2071            HeapObject::DataFrame(_) => PerlValue::string("DataFrame".into()),
2072            HeapObject::Capture(_) => PerlValue::string("Capture".into()),
2073            HeapObject::Ppool(_) => PerlValue::string("Ppool".into()),
2074            HeapObject::RemoteCluster(_) => PerlValue::string("Cluster".into()),
2075            HeapObject::Barrier(_) => PerlValue::string("Barrier".into()),
2076            HeapObject::SqliteConn(_) => PerlValue::string("SqliteConn".into()),
2077            HeapObject::StructInst(s) => PerlValue::string(s.def.name.clone()),
2078            HeapObject::EnumInst(e) => PerlValue::string(e.def.name.clone()),
2079            HeapObject::Bytes(_) => PerlValue::string("BYTES".into()),
2080            HeapObject::Blessed(b) => PerlValue::string(b.class.clone()),
2081            _ => PerlValue::string(String::new()),
2082        }
2083    }
2084
2085    pub fn num_cmp(&self, other: &PerlValue) -> Ordering {
2086        let a = self.to_number();
2087        let b = other.to_number();
2088        a.partial_cmp(&b).unwrap_or(Ordering::Equal)
2089    }
2090
2091    /// String equality for `eq` / `cmp` without allocating when both sides are heap strings.
2092    #[inline]
2093    pub fn str_eq(&self, other: &PerlValue) -> bool {
2094        if nanbox::is_heap(self.0) && nanbox::is_heap(other.0) {
2095            if let (HeapObject::String(a), HeapObject::String(b)) =
2096                unsafe { (self.heap_ref(), other.heap_ref()) }
2097            {
2098                return a == b;
2099            }
2100        }
2101        self.to_string() == other.to_string()
2102    }
2103
2104    pub fn str_cmp(&self, other: &PerlValue) -> Ordering {
2105        if nanbox::is_heap(self.0) && nanbox::is_heap(other.0) {
2106            if let (HeapObject::String(a), HeapObject::String(b)) =
2107                unsafe { (self.heap_ref(), other.heap_ref()) }
2108            {
2109                return a.cmp(b);
2110            }
2111        }
2112        self.to_string().cmp(&other.to_string())
2113    }
2114
2115    /// Deep equality for struct fields (recursive).
2116    pub fn struct_field_eq(&self, other: &PerlValue) -> bool {
2117        if nanbox::is_imm_undef(self.0) && nanbox::is_imm_undef(other.0) {
2118            return true;
2119        }
2120        if let (Some(a), Some(b)) = (nanbox::as_imm_int32(self.0), nanbox::as_imm_int32(other.0)) {
2121            return a == b;
2122        }
2123        if nanbox::is_raw_float_bits(self.0) && nanbox::is_raw_float_bits(other.0) {
2124            return f64::from_bits(self.0) == f64::from_bits(other.0);
2125        }
2126        if !nanbox::is_heap(self.0) || !nanbox::is_heap(other.0) {
2127            return self.to_number() == other.to_number();
2128        }
2129        match (unsafe { self.heap_ref() }, unsafe { other.heap_ref() }) {
2130            (HeapObject::String(a), HeapObject::String(b)) => a == b,
2131            (HeapObject::Integer(a), HeapObject::Integer(b)) => a == b,
2132            (HeapObject::Float(a), HeapObject::Float(b)) => a == b,
2133            (HeapObject::Array(a), HeapObject::Array(b)) => {
2134                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.struct_field_eq(y))
2135            }
2136            (HeapObject::ArrayRef(a), HeapObject::ArrayRef(b)) => {
2137                let ag = a.read();
2138                let bg = b.read();
2139                ag.len() == bg.len() && ag.iter().zip(bg.iter()).all(|(x, y)| x.struct_field_eq(y))
2140            }
2141            (HeapObject::Hash(a), HeapObject::Hash(b)) => {
2142                a.len() == b.len()
2143                    && a.iter()
2144                        .all(|(k, v)| b.get(k).is_some_and(|bv| v.struct_field_eq(bv)))
2145            }
2146            (HeapObject::HashRef(a), HeapObject::HashRef(b)) => {
2147                let ag = a.read();
2148                let bg = b.read();
2149                ag.len() == bg.len()
2150                    && ag
2151                        .iter()
2152                        .all(|(k, v)| bg.get(k).is_some_and(|bv| v.struct_field_eq(bv)))
2153            }
2154            (HeapObject::StructInst(a), HeapObject::StructInst(b)) => {
2155                if a.def.name != b.def.name {
2156                    false
2157                } else {
2158                    let av = a.get_values();
2159                    let bv = b.get_values();
2160                    av.len() == bv.len()
2161                        && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y))
2162                }
2163            }
2164            _ => self.to_string() == other.to_string(),
2165        }
2166    }
2167
2168    /// Deep clone a value (used for struct clone).
2169    pub fn deep_clone(&self) -> PerlValue {
2170        if !nanbox::is_heap(self.0) {
2171            return self.clone();
2172        }
2173        match unsafe { self.heap_ref() } {
2174            HeapObject::Array(a) => PerlValue::array(a.iter().map(|v| v.deep_clone()).collect()),
2175            HeapObject::ArrayRef(a) => {
2176                let cloned: Vec<PerlValue> = a.read().iter().map(|v| v.deep_clone()).collect();
2177                PerlValue::array_ref(Arc::new(RwLock::new(cloned)))
2178            }
2179            HeapObject::Hash(h) => {
2180                let mut cloned = IndexMap::new();
2181                for (k, v) in h.iter() {
2182                    cloned.insert(k.clone(), v.deep_clone());
2183                }
2184                PerlValue::hash(cloned)
2185            }
2186            HeapObject::HashRef(h) => {
2187                let mut cloned = IndexMap::new();
2188                for (k, v) in h.read().iter() {
2189                    cloned.insert(k.clone(), v.deep_clone());
2190                }
2191                PerlValue::hash_ref(Arc::new(RwLock::new(cloned)))
2192            }
2193            HeapObject::StructInst(s) => {
2194                let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
2195                PerlValue::struct_inst(Arc::new(StructInstance::new(
2196                    Arc::clone(&s.def),
2197                    new_values,
2198                )))
2199            }
2200            _ => self.clone(),
2201        }
2202    }
2203
2204    pub fn to_list(&self) -> Vec<PerlValue> {
2205        if nanbox::is_imm_undef(self.0) {
2206            return vec![];
2207        }
2208        if !nanbox::is_heap(self.0) {
2209            return vec![self.clone()];
2210        }
2211        match unsafe { self.heap_ref() } {
2212            HeapObject::Array(a) => a.clone(),
2213            HeapObject::Hash(h) => h
2214                .iter()
2215                .flat_map(|(k, v)| vec![PerlValue::string(k.clone()), v.clone()])
2216                .collect(),
2217            HeapObject::Atomic(arc) => arc.lock().to_list(),
2218            HeapObject::Set(s) => s.values().cloned().collect(),
2219            HeapObject::Deque(d) => d.lock().iter().cloned().collect(),
2220            HeapObject::Iterator(it) => {
2221                let mut out = Vec::new();
2222                while let Some(v) = it.next_item() {
2223                    out.push(v);
2224                }
2225                out
2226            }
2227            _ => vec![self.clone()],
2228        }
2229    }
2230
2231    pub fn scalar_context(&self) -> PerlValue {
2232        if !nanbox::is_heap(self.0) {
2233            return self.clone();
2234        }
2235        if let Some(arc) = self.as_atomic_arc() {
2236            return arc.lock().scalar_context();
2237        }
2238        match unsafe { self.heap_ref() } {
2239            HeapObject::Array(a) => PerlValue::integer(a.len() as i64),
2240            HeapObject::Hash(h) => {
2241                if h.is_empty() {
2242                    PerlValue::integer(0)
2243                } else {
2244                    PerlValue::string(format!("{}/{}", h.len(), h.capacity()))
2245                }
2246            }
2247            HeapObject::Set(s) => PerlValue::integer(s.len() as i64),
2248            HeapObject::Deque(d) => PerlValue::integer(d.lock().len() as i64),
2249            HeapObject::Heap(h) => PerlValue::integer(h.lock().items.len() as i64),
2250            HeapObject::Pipeline(p) => PerlValue::integer(p.lock().source.len() as i64),
2251            HeapObject::Capture(_)
2252            | HeapObject::Ppool(_)
2253            | HeapObject::RemoteCluster(_)
2254            | HeapObject::Barrier(_) => PerlValue::integer(1),
2255            HeapObject::Generator(_) => PerlValue::integer(1),
2256            _ => self.clone(),
2257        }
2258    }
2259}
2260
2261impl fmt::Display for PerlValue {
2262    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2263        if nanbox::is_imm_undef(self.0) {
2264            return Ok(());
2265        }
2266        if let Some(n) = nanbox::as_imm_int32(self.0) {
2267            return write!(f, "{n}");
2268        }
2269        if nanbox::is_raw_float_bits(self.0) {
2270            return write!(f, "{}", format_float(f64::from_bits(self.0)));
2271        }
2272        match unsafe { self.heap_ref() } {
2273            HeapObject::Integer(n) => write!(f, "{n}"),
2274            HeapObject::Float(val) => write!(f, "{}", format_float(*val)),
2275            HeapObject::ErrnoDual { msg, .. } => f.write_str(msg),
2276            HeapObject::String(s) => f.write_str(s),
2277            HeapObject::Bytes(b) => f.write_str(&decode_utf8_or_latin1(b)),
2278            HeapObject::Array(a) => {
2279                for v in a {
2280                    write!(f, "{v}")?;
2281                }
2282                Ok(())
2283            }
2284            HeapObject::Hash(h) => write!(f, "{}/{}", h.len(), h.capacity()),
2285            HeapObject::ArrayRef(_) | HeapObject::ArrayBindingRef(_) => f.write_str("ARRAY(0x...)"),
2286            HeapObject::HashRef(_) | HeapObject::HashBindingRef(_) => f.write_str("HASH(0x...)"),
2287            HeapObject::ScalarRef(_) | HeapObject::ScalarBindingRef(_) | HeapObject::CaptureCell(_) => {
2288                f.write_str("SCALAR(0x...)")
2289            }
2290            HeapObject::CodeRef(sub) => write!(f, "CODE({})", sub.name),
2291            HeapObject::Regex(_, src, _) => write!(f, "(?:{src})"),
2292            HeapObject::Blessed(b) => write!(f, "{}=HASH(0x...)", b.class),
2293            HeapObject::IOHandle(name) => f.write_str(name),
2294            HeapObject::Atomic(arc) => write!(f, "{}", arc.lock()),
2295            HeapObject::Set(s) => {
2296                f.write_str("{")?;
2297                if !s.is_empty() {
2298                    let mut iter = s.values();
2299                    if let Some(v) = iter.next() {
2300                        write!(f, "{v}")?;
2301                    }
2302                    for v in iter {
2303                        write!(f, ",{v}")?;
2304                    }
2305                }
2306                f.write_str("}")
2307            }
2308            HeapObject::ChannelTx(_) => f.write_str("PCHANNEL::Tx"),
2309            HeapObject::ChannelRx(_) => f.write_str("PCHANNEL::Rx"),
2310            HeapObject::AsyncTask(_) => f.write_str("AsyncTask"),
2311            HeapObject::Generator(g) => write!(f, "Generator({} stmts)", g.block.len()),
2312            HeapObject::Deque(d) => write!(f, "Deque({})", d.lock().len()),
2313            HeapObject::Heap(h) => write!(f, "Heap({})", h.lock().items.len()),
2314            HeapObject::Pipeline(p) => {
2315                let g = p.lock();
2316                write!(f, "Pipeline({} ops)", g.ops.len())
2317            }
2318            HeapObject::Capture(c) => write!(f, "Capture(exit={})", c.exitcode),
2319            HeapObject::Ppool(_) => f.write_str("Ppool"),
2320            HeapObject::RemoteCluster(c) => write!(f, "Cluster({} slots)", c.slots.len()),
2321            HeapObject::Barrier(_) => f.write_str("Barrier"),
2322            HeapObject::SqliteConn(_) => f.write_str("SqliteConn"),
2323            HeapObject::StructInst(s) => {
2324                // Smart stringify: Point(x => 1.5, y => 2.0)
2325                write!(f, "{}(", s.def.name)?;
2326                let values = s.values.read();
2327                for (i, field) in s.def.fields.iter().enumerate() {
2328                    if i > 0 {
2329                        f.write_str(", ")?;
2330                    }
2331                    write!(
2332                        f,
2333                        "{} => {}",
2334                        field.name,
2335                        values.get(i).cloned().unwrap_or(PerlValue::UNDEF)
2336                    )?;
2337                }
2338                f.write_str(")")
2339            }
2340            HeapObject::EnumInst(e) => {
2341                // Smart stringify: Color::Red or Maybe::Some(value)
2342                write!(f, "{}::{}", e.def.name, e.variant_name())?;
2343                if e.def.variants[e.variant_idx].ty.is_some() {
2344                    write!(f, "({})", e.data)?;
2345                }
2346                Ok(())
2347            }
2348            HeapObject::ClassInst(c) => {
2349                // Smart stringify: Dog(name => "Rex", age => 5)
2350                write!(f, "{}(", c.def.name)?;
2351                let values = c.values.read();
2352                for (i, field) in c.def.fields.iter().enumerate() {
2353                    if i > 0 {
2354                        f.write_str(", ")?;
2355                    }
2356                    write!(
2357                        f,
2358                        "{} => {}",
2359                        field.name,
2360                        values.get(i).cloned().unwrap_or(PerlValue::UNDEF)
2361                    )?;
2362                }
2363                f.write_str(")")
2364            }
2365            HeapObject::DataFrame(d) => {
2366                let g = d.lock();
2367                write!(f, "DataFrame({} rows)", g.nrows())
2368            }
2369            HeapObject::Iterator(_) => f.write_str("Iterator"),
2370        }
2371    }
2372}
2373
2374/// Stable key for set membership (dedup of `PerlValue` in this runtime).
2375pub fn set_member_key(v: &PerlValue) -> String {
2376    if nanbox::is_imm_undef(v.0) {
2377        return "u:".to_string();
2378    }
2379    if let Some(n) = nanbox::as_imm_int32(v.0) {
2380        return format!("i:{n}");
2381    }
2382    if nanbox::is_raw_float_bits(v.0) {
2383        return format!("f:{}", f64::from_bits(v.0).to_bits());
2384    }
2385    match unsafe { v.heap_ref() } {
2386        HeapObject::String(s) => format!("s:{s}"),
2387        HeapObject::Bytes(b) => {
2388            use std::fmt::Write as _;
2389            let mut h = String::with_capacity(b.len() * 2);
2390            for &x in b.iter() {
2391                let _ = write!(&mut h, "{:02x}", x);
2392            }
2393            format!("by:{h}")
2394        }
2395        HeapObject::Array(a) => {
2396            let parts: Vec<_> = a.iter().map(set_member_key).collect();
2397            format!("a:{}", parts.join(","))
2398        }
2399        HeapObject::Hash(h) => {
2400            let mut keys: Vec<_> = h.keys().cloned().collect();
2401            keys.sort();
2402            let parts: Vec<_> = keys
2403                .iter()
2404                .map(|k| format!("{}={}", k, set_member_key(h.get(k).unwrap())))
2405                .collect();
2406            format!("h:{}", parts.join(","))
2407        }
2408        HeapObject::Set(inner) => {
2409            let mut keys: Vec<_> = inner.keys().cloned().collect();
2410            keys.sort();
2411            format!("S:{}", keys.join(","))
2412        }
2413        HeapObject::ArrayRef(a) => {
2414            let g = a.read();
2415            let parts: Vec<_> = g.iter().map(set_member_key).collect();
2416            format!("ar:{}", parts.join(","))
2417        }
2418        HeapObject::HashRef(h) => {
2419            let g = h.read();
2420            let mut keys: Vec<_> = g.keys().cloned().collect();
2421            keys.sort();
2422            let parts: Vec<_> = keys
2423                .iter()
2424                .map(|k| format!("{}={}", k, set_member_key(g.get(k).unwrap())))
2425                .collect();
2426            format!("hr:{}", parts.join(","))
2427        }
2428        HeapObject::Blessed(b) => {
2429            let d = b.data.read();
2430            format!("b:{}:{}", b.class, set_member_key(&d))
2431        }
2432        HeapObject::ScalarRef(_) | HeapObject::ScalarBindingRef(_) | HeapObject::CaptureCell(_) => format!("sr:{v}"),
2433        HeapObject::ArrayBindingRef(n) => format!("abind:{n}"),
2434        HeapObject::HashBindingRef(n) => format!("hbind:{n}"),
2435        HeapObject::CodeRef(_) => format!("c:{v}"),
2436        HeapObject::Regex(_, src, _) => format!("r:{src}"),
2437        HeapObject::IOHandle(s) => format!("io:{s}"),
2438        HeapObject::Atomic(arc) => format!("at:{}", set_member_key(&arc.lock())),
2439        HeapObject::ChannelTx(tx) => format!("chtx:{:p}", Arc::as_ptr(tx)),
2440        HeapObject::ChannelRx(rx) => format!("chrx:{:p}", Arc::as_ptr(rx)),
2441        HeapObject::AsyncTask(t) => format!("async:{:p}", Arc::as_ptr(t)),
2442        HeapObject::Generator(g) => format!("gen:{:p}", Arc::as_ptr(g)),
2443        HeapObject::Deque(d) => format!("dq:{:p}", Arc::as_ptr(d)),
2444        HeapObject::Heap(h) => format!("hp:{:p}", Arc::as_ptr(h)),
2445        HeapObject::Pipeline(p) => format!("pl:{:p}", Arc::as_ptr(p)),
2446        HeapObject::Capture(c) => format!("cap:{:p}", Arc::as_ptr(c)),
2447        HeapObject::Ppool(p) => format!("pp:{:p}", Arc::as_ptr(&p.0)),
2448        HeapObject::RemoteCluster(c) => format!("rcl:{:p}", Arc::as_ptr(c)),
2449        HeapObject::Barrier(b) => format!("br:{:p}", Arc::as_ptr(&b.0)),
2450        HeapObject::SqliteConn(c) => format!("sql:{:p}", Arc::as_ptr(c)),
2451        HeapObject::StructInst(s) => format!("st:{}:{:?}", s.def.name, s.values),
2452        HeapObject::EnumInst(e) => {
2453            format!("en:{}::{}:{}", e.def.name, e.variant_name(), e.data)
2454        }
2455        HeapObject::ClassInst(c) => format!("cl:{}:{:?}", c.def.name, c.values),
2456        HeapObject::DataFrame(d) => format!("df:{:p}", Arc::as_ptr(d)),
2457        HeapObject::Iterator(_) => "iter".to_string(),
2458        HeapObject::ErrnoDual { code, msg } => format!("e:{code}:{msg}"),
2459        HeapObject::Integer(n) => format!("i:{n}"),
2460        HeapObject::Float(fl) => format!("f:{}", fl.to_bits()),
2461    }
2462}
2463
2464pub fn set_from_elements<I: IntoIterator<Item = PerlValue>>(items: I) -> PerlValue {
2465    let mut map = PerlSet::new();
2466    for v in items {
2467        let k = set_member_key(&v);
2468        map.insert(k, v);
2469    }
2470    PerlValue::set(Arc::new(map))
2471}
2472
2473/// Underlying set for union/intersection, including `mysync $s` (`Atomic` wrapping `Set`).
2474#[inline]
2475pub fn set_payload(v: &PerlValue) -> Option<Arc<PerlSet>> {
2476    if !nanbox::is_heap(v.0) {
2477        return None;
2478    }
2479    match unsafe { v.heap_ref() } {
2480        HeapObject::Set(s) => Some(Arc::clone(s)),
2481        HeapObject::Atomic(a) => set_payload(&a.lock()),
2482        _ => None,
2483    }
2484}
2485
2486pub fn set_union(a: &PerlValue, b: &PerlValue) -> Option<PerlValue> {
2487    let ia = set_payload(a)?;
2488    let ib = set_payload(b)?;
2489    let mut m = (*ia).clone();
2490    for (k, v) in ib.iter() {
2491        m.entry(k.clone()).or_insert_with(|| v.clone());
2492    }
2493    Some(PerlValue::set(Arc::new(m)))
2494}
2495
2496pub fn set_intersection(a: &PerlValue, b: &PerlValue) -> Option<PerlValue> {
2497    let ia = set_payload(a)?;
2498    let ib = set_payload(b)?;
2499    let mut m = PerlSet::new();
2500    for (k, v) in ia.iter() {
2501        if ib.contains_key(k) {
2502            m.insert(k.clone(), v.clone());
2503        }
2504    }
2505    Some(PerlValue::set(Arc::new(m)))
2506}
2507fn parse_number(s: &str) -> f64 {
2508    let s = s.trim();
2509    if s.is_empty() {
2510        return 0.0;
2511    }
2512    // Perl extracts leading numeric portion
2513    let mut end = 0;
2514    let bytes = s.as_bytes();
2515    if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
2516        end += 1;
2517    }
2518    while end < bytes.len() && bytes[end].is_ascii_digit() {
2519        end += 1;
2520    }
2521    if end < bytes.len() && bytes[end] == b'.' {
2522        end += 1;
2523        while end < bytes.len() && bytes[end].is_ascii_digit() {
2524            end += 1;
2525        }
2526    }
2527    if end < bytes.len() && (bytes[end] == b'e' || bytes[end] == b'E') {
2528        end += 1;
2529        if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
2530            end += 1;
2531        }
2532        while end < bytes.len() && bytes[end].is_ascii_digit() {
2533            end += 1;
2534        }
2535    }
2536    if end == 0 {
2537        return 0.0;
2538    }
2539    s[..end].parse::<f64>().unwrap_or(0.0)
2540}
2541
2542fn format_float(f: f64) -> String {
2543    if f.fract() == 0.0 && f.abs() < 1e16 {
2544        format!("{}", f as i64)
2545    } else {
2546        // Perl uses Gconvert which is sprintf("%.15g", f) on most platforms.
2547        let mut buf = [0u8; 64];
2548        unsafe {
2549            libc::snprintf(
2550                buf.as_mut_ptr() as *mut libc::c_char,
2551                buf.len(),
2552                c"%.15g".as_ptr(),
2553                f,
2554            );
2555            std::ffi::CStr::from_ptr(buf.as_ptr() as *const libc::c_char)
2556                .to_string_lossy()
2557                .into_owned()
2558        }
2559    }
2560}
2561
2562/// Result of one magical string increment step in a list-context `..` range (Perl `sv_inc`).
2563#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2564pub(crate) enum PerlListRangeIncOutcome {
2565    Continue,
2566    /// Perl upgraded the scalar to a numeric form (`SvNIOKp`); list range stops after this step.
2567    BecameNumeric,
2568}
2569
2570/// Perl `looks_like_number` / `grok_number` subset: `s` must be **entirely** a numeric string
2571/// (after trim), with no trailing garbage. Used for `RANGE_IS_NUMERIC` in `pp_flop`.
2572fn perl_str_looks_like_number_for_range(s: &str) -> bool {
2573    let t = s.trim();
2574    if t.is_empty() {
2575        return s.is_empty();
2576    }
2577    let b = t.as_bytes();
2578    let mut i = 0usize;
2579    if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2580        i += 1;
2581    }
2582    if i >= b.len() {
2583        return false;
2584    }
2585    let mut saw_digit = false;
2586    while i < b.len() && b[i].is_ascii_digit() {
2587        saw_digit = true;
2588        i += 1;
2589    }
2590    if i < b.len() && b[i] == b'.' {
2591        i += 1;
2592        while i < b.len() && b[i].is_ascii_digit() {
2593            saw_digit = true;
2594            i += 1;
2595        }
2596    }
2597    if !saw_digit {
2598        return false;
2599    }
2600    if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
2601        i += 1;
2602        if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2603            i += 1;
2604        }
2605        let exp0 = i;
2606        while i < b.len() && b[i].is_ascii_digit() {
2607            i += 1;
2608        }
2609        if i == exp0 {
2610            return false;
2611        }
2612    }
2613    i == b.len()
2614}
2615
2616/// Whether list-context `..` uses Perl's **numeric** counting (`pp_flop` `RANGE_IS_NUMERIC`).
2617pub(crate) fn perl_list_range_pair_is_numeric(left: &PerlValue, right: &PerlValue) -> bool {
2618    if left.is_integer_like() || left.is_float_like() {
2619        return true;
2620    }
2621    if !left.is_undef() && !left.is_string_like() {
2622        return true;
2623    }
2624    if right.is_integer_like() || right.is_float_like() {
2625        return true;
2626    }
2627    if !right.is_undef() && !right.is_string_like() {
2628        return true;
2629    }
2630
2631    let left_ok = !left.is_undef();
2632    let right_ok = !right.is_undef();
2633    let left_pok = left.is_string_like();
2634    let left_pv = left.as_str_or_empty();
2635    let right_pv = right.as_str_or_empty();
2636
2637    let left_n = perl_str_looks_like_number_for_range(&left_pv);
2638    let right_n = perl_str_looks_like_number_for_range(&right_pv);
2639
2640    let left_zero_prefix =
2641        left_pok && left_pv.len() > 1 && left_pv.as_bytes().first() == Some(&b'0');
2642
2643    let clause5_left =
2644        (!left_ok && right_ok) || ((!left_ok || left_n) && left_pok && !left_zero_prefix);
2645    clause5_left && (!right_ok || right_n)
2646}
2647
2648/// Magical string `++` for ASCII letter/digit runs (Perl `sv_inc_nomg`, non-EBCDIC).
2649pub(crate) fn perl_magic_string_increment_for_range(s: &mut String) -> PerlListRangeIncOutcome {
2650    if s.is_empty() {
2651        return PerlListRangeIncOutcome::BecameNumeric;
2652    }
2653    let b = s.as_bytes();
2654    let mut i = 0usize;
2655    while i < b.len() && b[i].is_ascii_alphabetic() {
2656        i += 1;
2657    }
2658    while i < b.len() && b[i].is_ascii_digit() {
2659        i += 1;
2660    }
2661    if i < b.len() {
2662        let n = parse_number(s) + 1.0;
2663        *s = format_float(n);
2664        return PerlListRangeIncOutcome::BecameNumeric;
2665    }
2666
2667    let bytes = unsafe { s.as_mut_vec() };
2668    let mut idx = bytes.len() - 1;
2669    loop {
2670        if bytes[idx].is_ascii_digit() {
2671            bytes[idx] += 1;
2672            if bytes[idx] <= b'9' {
2673                return PerlListRangeIncOutcome::Continue;
2674            }
2675            bytes[idx] = b'0';
2676            if idx == 0 {
2677                bytes.insert(0, b'1');
2678                return PerlListRangeIncOutcome::Continue;
2679            }
2680            idx -= 1;
2681        } else {
2682            bytes[idx] = bytes[idx].wrapping_add(1);
2683            if bytes[idx].is_ascii_alphabetic() {
2684                return PerlListRangeIncOutcome::Continue;
2685            }
2686            bytes[idx] = bytes[idx].wrapping_sub(b'z' - b'a' + 1);
2687            if idx == 0 {
2688                let c = bytes[0];
2689                bytes.insert(0, if c.is_ascii_digit() { b'1' } else { c });
2690                return PerlListRangeIncOutcome::Continue;
2691            }
2692            idx -= 1;
2693        }
2694    }
2695}
2696
2697fn perl_list_range_max_bound(right: &str) -> usize {
2698    if right.is_ascii() {
2699        right.len()
2700    } else {
2701        right.chars().count()
2702    }
2703}
2704
2705fn perl_list_range_cur_bound(cur: &str, right_is_ascii: bool) -> usize {
2706    if right_is_ascii {
2707        cur.len()
2708    } else {
2709        cur.chars().count()
2710    }
2711}
2712
2713fn perl_list_range_expand_string_magic(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
2714    let mut cur = from.into_string();
2715    let right = to.into_string();
2716    let right_ascii = right.is_ascii();
2717    let max_bound = perl_list_range_max_bound(&right);
2718    let mut out = Vec::new();
2719    let mut guard = 0usize;
2720    loop {
2721        guard += 1;
2722        if guard > 50_000_000 {
2723            break;
2724        }
2725        let cur_bound = perl_list_range_cur_bound(&cur, right_ascii);
2726        if cur_bound > max_bound {
2727            break;
2728        }
2729        out.push(PerlValue::string(cur.clone()));
2730        if cur == right {
2731            break;
2732        }
2733        match perl_magic_string_increment_for_range(&mut cur) {
2734            PerlListRangeIncOutcome::Continue => {}
2735            PerlListRangeIncOutcome::BecameNumeric => break,
2736        }
2737    }
2738    out
2739}
2740
2741/// Perl list-context `..` (`pp_flop`): numeric counting or magical string sequence.
2742pub(crate) fn perl_list_range_expand(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
2743    if perl_list_range_pair_is_numeric(&from, &to) {
2744        let i = from.to_int();
2745        let j = to.to_int();
2746        if j >= i {
2747            (i..=j).map(PerlValue::integer).collect()
2748        } else {
2749            Vec::new()
2750        }
2751    } else {
2752        perl_list_range_expand_string_magic(from, to)
2753    }
2754}
2755
2756impl PerlDataFrame {
2757    /// One row as a hashref (`$_` in `filter`).
2758    pub fn row_hashref(&self, row: usize) -> PerlValue {
2759        let mut m = IndexMap::new();
2760        for (i, col) in self.columns.iter().enumerate() {
2761            m.insert(
2762                col.clone(),
2763                self.cols[i].get(row).cloned().unwrap_or(PerlValue::UNDEF),
2764            );
2765        }
2766        PerlValue::hash_ref(Arc::new(RwLock::new(m)))
2767    }
2768}
2769
2770#[cfg(test)]
2771mod tests {
2772    use super::PerlValue;
2773    use crate::perl_regex::PerlCompiledRegex;
2774    use indexmap::IndexMap;
2775    use parking_lot::RwLock;
2776    use std::cmp::Ordering;
2777    use std::sync::Arc;
2778
2779    #[test]
2780    fn undef_is_false() {
2781        assert!(!PerlValue::UNDEF.is_true());
2782    }
2783
2784    #[test]
2785    fn string_zero_is_false() {
2786        assert!(!PerlValue::string("0".into()).is_true());
2787        assert!(PerlValue::string("00".into()).is_true());
2788    }
2789
2790    #[test]
2791    fn empty_string_is_false() {
2792        assert!(!PerlValue::string(String::new()).is_true());
2793    }
2794
2795    #[test]
2796    fn integer_zero_is_false_nonzero_true() {
2797        assert!(!PerlValue::integer(0).is_true());
2798        assert!(PerlValue::integer(-1).is_true());
2799    }
2800
2801    #[test]
2802    fn float_zero_is_false_nonzero_true() {
2803        assert!(!PerlValue::float(0.0).is_true());
2804        assert!(PerlValue::float(0.1).is_true());
2805    }
2806
2807    #[test]
2808    fn num_cmp_orders_float_against_integer() {
2809        assert_eq!(
2810            PerlValue::float(2.5).num_cmp(&PerlValue::integer(3)),
2811            Ordering::Less
2812        );
2813    }
2814
2815    #[test]
2816    fn to_int_parses_leading_number_from_string() {
2817        assert_eq!(PerlValue::string("42xyz".into()).to_int(), 42);
2818        assert_eq!(PerlValue::string("  -3.7foo".into()).to_int(), -3);
2819    }
2820
2821    #[test]
2822    fn num_cmp_orders_as_numeric() {
2823        assert_eq!(
2824            PerlValue::integer(2).num_cmp(&PerlValue::integer(11)),
2825            Ordering::Less
2826        );
2827        assert_eq!(
2828            PerlValue::string("2foo".into()).num_cmp(&PerlValue::string("11".into())),
2829            Ordering::Less
2830        );
2831    }
2832
2833    #[test]
2834    fn str_cmp_orders_as_strings() {
2835        assert_eq!(
2836            PerlValue::string("2".into()).str_cmp(&PerlValue::string("11".into())),
2837            Ordering::Greater
2838        );
2839    }
2840
2841    #[test]
2842    fn str_eq_heap_strings_fast_path() {
2843        let a = PerlValue::string("hello".into());
2844        let b = PerlValue::string("hello".into());
2845        assert!(a.str_eq(&b));
2846        assert!(!a.str_eq(&PerlValue::string("hell".into())));
2847    }
2848
2849    #[test]
2850    fn str_eq_fallback_matches_stringified_equality() {
2851        let n = PerlValue::integer(42);
2852        let s = PerlValue::string("42".into());
2853        assert!(n.str_eq(&s));
2854        assert!(!PerlValue::integer(1).str_eq(&PerlValue::string("2".into())));
2855    }
2856
2857    #[test]
2858    fn str_cmp_heap_strings_fast_path() {
2859        assert_eq!(
2860            PerlValue::string("a".into()).str_cmp(&PerlValue::string("b".into())),
2861            Ordering::Less
2862        );
2863    }
2864
2865    #[test]
2866    fn scalar_context_array_and_hash() {
2867        let a =
2868            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).scalar_context();
2869        assert_eq!(a.to_int(), 2);
2870        let mut h = IndexMap::new();
2871        h.insert("a".into(), PerlValue::integer(1));
2872        let sc = PerlValue::hash(h).scalar_context();
2873        assert!(sc.is_string_like());
2874    }
2875
2876    #[test]
2877    fn to_list_array_hash_and_scalar() {
2878        assert_eq!(
2879            PerlValue::array(vec![PerlValue::integer(7)])
2880                .to_list()
2881                .len(),
2882            1
2883        );
2884        let mut h = IndexMap::new();
2885        h.insert("k".into(), PerlValue::integer(1));
2886        let list = PerlValue::hash(h).to_list();
2887        assert_eq!(list.len(), 2);
2888        let one = PerlValue::integer(99).to_list();
2889        assert_eq!(one.len(), 1);
2890        assert_eq!(one[0].to_int(), 99);
2891    }
2892
2893    #[test]
2894    fn type_name_and_ref_type_for_core_kinds() {
2895        assert_eq!(PerlValue::integer(0).type_name(), "INTEGER");
2896        assert_eq!(PerlValue::UNDEF.ref_type().to_string(), "");
2897        assert_eq!(
2898            PerlValue::array_ref(Arc::new(RwLock::new(vec![])))
2899                .ref_type()
2900                .to_string(),
2901            "ARRAY"
2902        );
2903    }
2904
2905    #[test]
2906    fn display_undef_is_empty_integer_is_decimal() {
2907        assert_eq!(PerlValue::UNDEF.to_string(), "");
2908        assert_eq!(PerlValue::integer(-7).to_string(), "-7");
2909    }
2910
2911    #[test]
2912    fn empty_array_is_false_nonempty_is_true() {
2913        assert!(!PerlValue::array(vec![]).is_true());
2914        assert!(PerlValue::array(vec![PerlValue::integer(0)]).is_true());
2915    }
2916
2917    #[test]
2918    fn to_number_undef_and_non_numeric_refs_are_zero() {
2919        use super::PerlSub;
2920
2921        assert_eq!(PerlValue::UNDEF.to_number(), 0.0);
2922        assert_eq!(
2923            PerlValue::code_ref(Arc::new(PerlSub {
2924                name: "f".into(),
2925                params: vec![],
2926                body: vec![],
2927                closure_env: None,
2928                prototype: None,
2929                fib_like: None,
2930            }))
2931            .to_number(),
2932            0.0
2933        );
2934    }
2935
2936    #[test]
2937    fn append_to_builds_string_without_extra_alloc_for_int_and_string() {
2938        let mut buf = String::new();
2939        PerlValue::integer(-12).append_to(&mut buf);
2940        PerlValue::string("ab".into()).append_to(&mut buf);
2941        assert_eq!(buf, "-12ab");
2942        let mut u = String::new();
2943        PerlValue::UNDEF.append_to(&mut u);
2944        assert!(u.is_empty());
2945    }
2946
2947    #[test]
2948    fn append_to_atomic_delegates_to_inner() {
2949        use parking_lot::Mutex;
2950        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::string("z".into()))));
2951        let mut buf = String::new();
2952        a.append_to(&mut buf);
2953        assert_eq!(buf, "z");
2954    }
2955
2956    #[test]
2957    fn unwrap_atomic_reads_inner_other_variants_clone() {
2958        use parking_lot::Mutex;
2959        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(9))));
2960        assert_eq!(a.unwrap_atomic().to_int(), 9);
2961        assert_eq!(PerlValue::integer(3).unwrap_atomic().to_int(), 3);
2962    }
2963
2964    #[test]
2965    fn is_atomic_only_true_for_atomic_variant() {
2966        use parking_lot::Mutex;
2967        assert!(PerlValue::atomic(Arc::new(Mutex::new(PerlValue::UNDEF))).is_atomic());
2968        assert!(!PerlValue::integer(0).is_atomic());
2969    }
2970
2971    #[test]
2972    fn as_str_only_on_string_variant() {
2973        assert_eq!(
2974            PerlValue::string("x".into()).as_str(),
2975            Some("x".to_string())
2976        );
2977        assert_eq!(PerlValue::integer(1).as_str(), None);
2978    }
2979
2980    #[test]
2981    fn as_str_or_empty_defaults_non_string() {
2982        assert_eq!(PerlValue::string("z".into()).as_str_or_empty(), "z");
2983        assert_eq!(PerlValue::integer(1).as_str_or_empty(), "");
2984    }
2985
2986    #[test]
2987    fn to_int_truncates_float_toward_zero() {
2988        assert_eq!(PerlValue::float(3.9).to_int(), 3);
2989        assert_eq!(PerlValue::float(-2.1).to_int(), -2);
2990    }
2991
2992    #[test]
2993    fn to_number_array_is_length() {
2994        assert_eq!(
2995            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).to_number(),
2996            2.0
2997        );
2998    }
2999
3000    #[test]
3001    fn scalar_context_empty_hash_is_zero() {
3002        let h = IndexMap::new();
3003        assert_eq!(PerlValue::hash(h).scalar_context().to_int(), 0);
3004    }
3005
3006    #[test]
3007    fn scalar_context_nonhash_nonarray_clones() {
3008        let v = PerlValue::integer(8);
3009        assert_eq!(v.scalar_context().to_int(), 8);
3010    }
3011
3012    #[test]
3013    fn display_float_integer_like_omits_decimal() {
3014        assert_eq!(PerlValue::float(4.0).to_string(), "4");
3015    }
3016
3017    #[test]
3018    fn display_array_concatenates_element_displays() {
3019        let a = PerlValue::array(vec![PerlValue::integer(1), PerlValue::string("b".into())]);
3020        assert_eq!(a.to_string(), "1b");
3021    }
3022
3023    #[test]
3024    fn display_code_ref_includes_sub_name() {
3025        use super::PerlSub;
3026        let c = PerlValue::code_ref(Arc::new(PerlSub {
3027            name: "foo".into(),
3028            params: vec![],
3029            body: vec![],
3030            closure_env: None,
3031            prototype: None,
3032            fib_like: None,
3033        }));
3034        assert!(c.to_string().contains("foo"));
3035    }
3036
3037    #[test]
3038    fn display_regex_shows_non_capturing_prefix() {
3039        let r = PerlValue::regex(
3040            PerlCompiledRegex::compile("x+").unwrap(),
3041            "x+".into(),
3042            "".into(),
3043        );
3044        assert_eq!(r.to_string(), "(?:x+)");
3045    }
3046
3047    #[test]
3048    fn display_iohandle_is_name() {
3049        assert_eq!(PerlValue::io_handle("STDOUT".into()).to_string(), "STDOUT");
3050    }
3051
3052    #[test]
3053    fn ref_type_blessed_uses_class_name() {
3054        let b = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
3055            "Pkg".into(),
3056            PerlValue::UNDEF,
3057        )));
3058        assert_eq!(b.ref_type().to_string(), "Pkg");
3059    }
3060
3061    #[test]
3062    fn blessed_drop_enqueues_pending_destroy() {
3063        let v = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
3064            "Z".into(),
3065            PerlValue::integer(7),
3066        )));
3067        drop(v);
3068        let q = crate::pending_destroy::take_queue();
3069        assert_eq!(q.len(), 1);
3070        assert_eq!(q[0].0, "Z");
3071        assert_eq!(q[0].1.to_int(), 7);
3072    }
3073
3074    #[test]
3075    fn type_name_iohandle_is_glob() {
3076        assert_eq!(PerlValue::io_handle("FH".into()).type_name(), "GLOB");
3077    }
3078
3079    #[test]
3080    fn empty_hash_is_false() {
3081        assert!(!PerlValue::hash(IndexMap::new()).is_true());
3082    }
3083
3084    #[test]
3085    fn hash_nonempty_is_true() {
3086        let mut h = IndexMap::new();
3087        h.insert("k".into(), PerlValue::UNDEF);
3088        assert!(PerlValue::hash(h).is_true());
3089    }
3090
3091    #[test]
3092    fn num_cmp_equal_integers() {
3093        assert_eq!(
3094            PerlValue::integer(5).num_cmp(&PerlValue::integer(5)),
3095            Ordering::Equal
3096        );
3097    }
3098
3099    #[test]
3100    fn str_cmp_compares_lexicographic_string_forms() {
3101        // Display forms "2" and "10" — string order differs from numeric order.
3102        assert_eq!(
3103            PerlValue::integer(2).str_cmp(&PerlValue::integer(10)),
3104            Ordering::Greater
3105        );
3106    }
3107
3108    #[test]
3109    fn to_list_undef_empty() {
3110        assert!(PerlValue::UNDEF.to_list().is_empty());
3111    }
3112
3113    #[test]
3114    fn unwrap_atomic_nested_atomic() {
3115        use parking_lot::Mutex;
3116        let inner = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(2))));
3117        let outer = PerlValue::atomic(Arc::new(Mutex::new(inner)));
3118        assert_eq!(outer.unwrap_atomic().to_int(), 2);
3119    }
3120
3121    #[test]
3122    fn errno_dual_parts_extracts_code_and_message() {
3123        let v = PerlValue::errno_dual(-2, "oops".into());
3124        assert_eq!(v.errno_dual_parts(), Some((-2, "oops".into())));
3125    }
3126
3127    #[test]
3128    fn errno_dual_parts_none_for_plain_string() {
3129        assert!(PerlValue::string("hi".into()).errno_dual_parts().is_none());
3130    }
3131
3132    #[test]
3133    fn errno_dual_parts_none_for_integer() {
3134        assert!(PerlValue::integer(1).errno_dual_parts().is_none());
3135    }
3136
3137    #[test]
3138    fn errno_dual_numeric_context_uses_code_string_uses_msg() {
3139        let v = PerlValue::errno_dual(5, "five".into());
3140        assert_eq!(v.to_int(), 5);
3141        assert_eq!(v.to_string(), "five");
3142    }
3143
3144    #[test]
3145    fn list_range_alpha_joins_like_perl() {
3146        use super::perl_list_range_expand;
3147        let v =
3148            perl_list_range_expand(PerlValue::string("a".into()), PerlValue::string("z".into()));
3149        let s: String = v.iter().map(|x| x.to_string()).collect();
3150        assert_eq!(s, "abcdefghijklmnopqrstuvwxyz");
3151    }
3152
3153    #[test]
3154    fn list_range_numeric_string_endpoints() {
3155        use super::perl_list_range_expand;
3156        let v = perl_list_range_expand(
3157            PerlValue::string("9".into()),
3158            PerlValue::string("11".into()),
3159        );
3160        assert_eq!(v.len(), 3);
3161        assert_eq!(
3162            v.iter().map(|x| x.to_int()).collect::<Vec<_>>(),
3163            vec![9, 10, 11]
3164        );
3165    }
3166
3167    #[test]
3168    fn list_range_leading_zero_is_string_mode() {
3169        use super::perl_list_range_expand;
3170        let v = perl_list_range_expand(
3171            PerlValue::string("01".into()),
3172            PerlValue::string("05".into()),
3173        );
3174        assert_eq!(v.len(), 5);
3175        assert_eq!(
3176            v.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
3177            vec!["01", "02", "03", "04", "05"]
3178        );
3179    }
3180
3181    #[test]
3182    fn list_range_empty_to_letter_one_element() {
3183        use super::perl_list_range_expand;
3184        let v = perl_list_range_expand(
3185            PerlValue::string(String::new()),
3186            PerlValue::string("c".into()),
3187        );
3188        assert_eq!(v.len(), 1);
3189        assert_eq!(v[0].to_string(), "");
3190    }
3191
3192    #[test]
3193    fn magic_string_inc_z_wraps_aa() {
3194        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
3195        let mut s = "z".to_string();
3196        assert_eq!(
3197            perl_magic_string_increment_for_range(&mut s),
3198            PerlListRangeIncOutcome::Continue
3199        );
3200        assert_eq!(s, "aa");
3201    }
3202
3203    #[test]
3204    fn test_boxed_numeric_stringification() {
3205        // Large integer outside i32 range
3206        let large_int = 10_000_000_000i64;
3207        let v_int = PerlValue::integer(large_int);
3208        assert_eq!(v_int.to_string(), "10000000000");
3209
3210        // Float that needs boxing (e.g. Infinity)
3211        let v_inf = PerlValue::float(f64::INFINITY);
3212        assert_eq!(v_inf.to_string(), "inf");
3213    }
3214
3215    #[test]
3216    fn magic_string_inc_nine_to_ten() {
3217        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
3218        let mut s = "9".to_string();
3219        assert_eq!(
3220            perl_magic_string_increment_for_range(&mut s),
3221            PerlListRangeIncOutcome::Continue
3222        );
3223        assert_eq!(s, "10");
3224    }
3225}