Skip to main content

stryke/
value.rs

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