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::Bytes(_) => PerlValue::string("BYTES".into()),
2155            HeapObject::Blessed(b) => PerlValue::string(b.class.clone()),
2156            _ => PerlValue::string(String::new()),
2157        }
2158    }
2159
2160    pub fn num_cmp(&self, other: &PerlValue) -> Ordering {
2161        let a = self.to_number();
2162        let b = other.to_number();
2163        a.partial_cmp(&b).unwrap_or(Ordering::Equal)
2164    }
2165
2166    /// String equality for `eq` / `cmp` without allocating when both sides are heap strings.
2167    #[inline]
2168    pub fn str_eq(&self, other: &PerlValue) -> bool {
2169        if nanbox::is_heap(self.0) && nanbox::is_heap(other.0) {
2170            if let (HeapObject::String(a), HeapObject::String(b)) =
2171                unsafe { (self.heap_ref(), other.heap_ref()) }
2172            {
2173                return a == b;
2174            }
2175        }
2176        self.to_string() == other.to_string()
2177    }
2178
2179    pub fn str_cmp(&self, other: &PerlValue) -> Ordering {
2180        if nanbox::is_heap(self.0) && nanbox::is_heap(other.0) {
2181            if let (HeapObject::String(a), HeapObject::String(b)) =
2182                unsafe { (self.heap_ref(), other.heap_ref()) }
2183            {
2184                return a.cmp(b);
2185            }
2186        }
2187        self.to_string().cmp(&other.to_string())
2188    }
2189
2190    /// Deep equality for struct fields (recursive).
2191    pub fn struct_field_eq(&self, other: &PerlValue) -> bool {
2192        if nanbox::is_imm_undef(self.0) && nanbox::is_imm_undef(other.0) {
2193            return true;
2194        }
2195        if let (Some(a), Some(b)) = (nanbox::as_imm_int32(self.0), nanbox::as_imm_int32(other.0)) {
2196            return a == b;
2197        }
2198        if nanbox::is_raw_float_bits(self.0) && nanbox::is_raw_float_bits(other.0) {
2199            return f64::from_bits(self.0) == f64::from_bits(other.0);
2200        }
2201        if !nanbox::is_heap(self.0) || !nanbox::is_heap(other.0) {
2202            return self.to_number() == other.to_number();
2203        }
2204        match (unsafe { self.heap_ref() }, unsafe { other.heap_ref() }) {
2205            (HeapObject::String(a), HeapObject::String(b)) => a == b,
2206            (HeapObject::Integer(a), HeapObject::Integer(b)) => a == b,
2207            (HeapObject::BigInt(a), HeapObject::BigInt(b)) => a == b,
2208            (HeapObject::BigInt(a), HeapObject::Integer(b))
2209            | (HeapObject::Integer(b), HeapObject::BigInt(a)) => a.as_ref() == &BigInt::from(*b),
2210            (HeapObject::Float(a), HeapObject::Float(b)) => a == b,
2211            (HeapObject::Array(a), HeapObject::Array(b)) => {
2212                a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| x.struct_field_eq(y))
2213            }
2214            (HeapObject::ArrayRef(a), HeapObject::ArrayRef(b)) => {
2215                let ag = a.read();
2216                let bg = b.read();
2217                ag.len() == bg.len() && ag.iter().zip(bg.iter()).all(|(x, y)| x.struct_field_eq(y))
2218            }
2219            (HeapObject::Hash(a), HeapObject::Hash(b)) => {
2220                a.len() == b.len()
2221                    && a.iter()
2222                        .all(|(k, v)| b.get(k).is_some_and(|bv| v.struct_field_eq(bv)))
2223            }
2224            (HeapObject::HashRef(a), HeapObject::HashRef(b)) => {
2225                let ag = a.read();
2226                let bg = b.read();
2227                ag.len() == bg.len()
2228                    && ag
2229                        .iter()
2230                        .all(|(k, v)| bg.get(k).is_some_and(|bv| v.struct_field_eq(bv)))
2231            }
2232            (HeapObject::StructInst(a), HeapObject::StructInst(b)) => {
2233                if a.def.name != b.def.name {
2234                    false
2235                } else {
2236                    let av = a.get_values();
2237                    let bv = b.get_values();
2238                    av.len() == bv.len()
2239                        && av.iter().zip(bv.iter()).all(|(x, y)| x.struct_field_eq(y))
2240                }
2241            }
2242            _ => self.to_string() == other.to_string(),
2243        }
2244    }
2245
2246    /// Deep clone a value (used for struct clone).
2247    pub fn deep_clone(&self) -> PerlValue {
2248        if !nanbox::is_heap(self.0) {
2249            return self.clone();
2250        }
2251        match unsafe { self.heap_ref() } {
2252            HeapObject::Array(a) => PerlValue::array(a.iter().map(|v| v.deep_clone()).collect()),
2253            HeapObject::ArrayRef(a) => {
2254                let cloned: Vec<PerlValue> = a.read().iter().map(|v| v.deep_clone()).collect();
2255                PerlValue::array_ref(Arc::new(RwLock::new(cloned)))
2256            }
2257            HeapObject::Hash(h) => {
2258                let mut cloned = IndexMap::new();
2259                for (k, v) in h.iter() {
2260                    cloned.insert(k.clone(), v.deep_clone());
2261                }
2262                PerlValue::hash(cloned)
2263            }
2264            HeapObject::HashRef(h) => {
2265                let mut cloned = IndexMap::new();
2266                for (k, v) in h.read().iter() {
2267                    cloned.insert(k.clone(), v.deep_clone());
2268                }
2269                PerlValue::hash_ref(Arc::new(RwLock::new(cloned)))
2270            }
2271            HeapObject::StructInst(s) => {
2272                let new_values = s.get_values().iter().map(|v| v.deep_clone()).collect();
2273                PerlValue::struct_inst(Arc::new(StructInstance::new(
2274                    Arc::clone(&s.def),
2275                    new_values,
2276                )))
2277            }
2278            _ => self.clone(),
2279        }
2280    }
2281
2282    pub fn to_list(&self) -> Vec<PerlValue> {
2283        if nanbox::is_imm_undef(self.0) {
2284            return vec![];
2285        }
2286        if !nanbox::is_heap(self.0) {
2287            return vec![self.clone()];
2288        }
2289        match unsafe { self.heap_ref() } {
2290            HeapObject::Array(a) => a.clone(),
2291            HeapObject::Hash(h) => h
2292                .iter()
2293                .flat_map(|(k, v)| vec![PerlValue::string(k.clone()), v.clone()])
2294                .collect(),
2295            HeapObject::Atomic(arc) => arc.lock().to_list(),
2296            HeapObject::Set(s) => s.values().cloned().collect(),
2297            HeapObject::Deque(d) => d.lock().iter().cloned().collect(),
2298            HeapObject::Iterator(it) => {
2299                let mut out = Vec::new();
2300                while let Some(v) = it.next_item() {
2301                    out.push(v);
2302                }
2303                out
2304            }
2305            _ => vec![self.clone()],
2306        }
2307    }
2308
2309    pub fn scalar_context(&self) -> PerlValue {
2310        if !nanbox::is_heap(self.0) {
2311            return self.clone();
2312        }
2313        if let Some(arc) = self.as_atomic_arc() {
2314            return arc.lock().scalar_context();
2315        }
2316        match unsafe { self.heap_ref() } {
2317            HeapObject::Array(a) => PerlValue::integer(a.len() as i64),
2318            HeapObject::Hash(h) => {
2319                if h.is_empty() {
2320                    PerlValue::integer(0)
2321                } else {
2322                    PerlValue::string(format!("{}/{}", h.len(), h.capacity()))
2323                }
2324            }
2325            HeapObject::Set(s) => PerlValue::integer(s.len() as i64),
2326            HeapObject::Deque(d) => PerlValue::integer(d.lock().len() as i64),
2327            HeapObject::Heap(h) => PerlValue::integer(h.lock().items.len() as i64),
2328            HeapObject::Pipeline(p) => PerlValue::integer(p.lock().source.len() as i64),
2329            HeapObject::Capture(_)
2330            | HeapObject::Ppool(_)
2331            | HeapObject::RemoteCluster(_)
2332            | HeapObject::Barrier(_) => PerlValue::integer(1),
2333            HeapObject::Generator(_) => PerlValue::integer(1),
2334            _ => self.clone(),
2335        }
2336    }
2337}
2338
2339impl fmt::Display for PerlValue {
2340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
2341        if nanbox::is_imm_undef(self.0) {
2342            return Ok(());
2343        }
2344        if let Some(n) = nanbox::as_imm_int32(self.0) {
2345            return write!(f, "{n}");
2346        }
2347        if nanbox::is_raw_float_bits(self.0) {
2348            return write!(f, "{}", format_float(f64::from_bits(self.0)));
2349        }
2350        match unsafe { self.heap_ref() } {
2351            HeapObject::Integer(n) => write!(f, "{n}"),
2352            HeapObject::BigInt(b) => write!(f, "{b}"),
2353            HeapObject::Float(val) => write!(f, "{}", format_float(*val)),
2354            HeapObject::ErrnoDual { msg, .. } => f.write_str(msg),
2355            HeapObject::String(s) => f.write_str(s),
2356            HeapObject::Bytes(b) => f.write_str(&decode_utf8_or_latin1(b)),
2357            HeapObject::Array(a) => {
2358                for v in a {
2359                    write!(f, "{v}")?;
2360                }
2361                Ok(())
2362            }
2363            HeapObject::Hash(h) => write!(f, "{}/{}", h.len(), h.capacity()),
2364            HeapObject::ArrayRef(_) | HeapObject::ArrayBindingRef(_) => f.write_str("ARRAY(0x...)"),
2365            HeapObject::HashRef(_) | HeapObject::HashBindingRef(_) => f.write_str("HASH(0x...)"),
2366            HeapObject::ScalarRef(_)
2367            | HeapObject::ScalarBindingRef(_)
2368            | HeapObject::CaptureCell(_) => f.write_str("SCALAR(0x...)"),
2369            HeapObject::CodeRef(sub) => write!(f, "CODE({})", sub.name),
2370            HeapObject::Regex(_, src, _) => write!(f, "(?:{src})"),
2371            HeapObject::Blessed(b) => write!(f, "{}=HASH(0x...)", b.class),
2372            HeapObject::IOHandle(name) => f.write_str(name),
2373            HeapObject::Atomic(arc) => write!(f, "{}", arc.lock()),
2374            HeapObject::Set(s) => {
2375                f.write_str("{")?;
2376                if !s.is_empty() {
2377                    let mut iter = s.values();
2378                    if let Some(v) = iter.next() {
2379                        write!(f, "{v}")?;
2380                    }
2381                    for v in iter {
2382                        write!(f, ",{v}")?;
2383                    }
2384                }
2385                f.write_str("}")
2386            }
2387            HeapObject::ChannelTx(_) => f.write_str("PCHANNEL::Tx"),
2388            HeapObject::ChannelRx(_) => f.write_str("PCHANNEL::Rx"),
2389            HeapObject::AsyncTask(_) => f.write_str("AsyncTask"),
2390            HeapObject::Generator(g) => write!(f, "Generator({} stmts)", g.block.len()),
2391            HeapObject::Deque(d) => write!(f, "Deque({})", d.lock().len()),
2392            HeapObject::Heap(h) => write!(f, "Heap({})", h.lock().items.len()),
2393            HeapObject::Pipeline(p) => {
2394                let g = p.lock();
2395                write!(f, "Pipeline({} ops)", g.ops.len())
2396            }
2397            HeapObject::Capture(c) => write!(f, "Capture(exit={})", c.exitcode),
2398            HeapObject::Ppool(_) => f.write_str("Ppool"),
2399            HeapObject::RemoteCluster(c) => write!(f, "Cluster({} slots)", c.slots.len()),
2400            HeapObject::Barrier(_) => f.write_str("Barrier"),
2401            HeapObject::SqliteConn(_) => f.write_str("SqliteConn"),
2402            HeapObject::StructInst(s) => {
2403                // Smart stringify: Point(x => 1.5, y => 2.0)
2404                write!(f, "{}(", s.def.name)?;
2405                let values = s.values.read();
2406                for (i, field) in s.def.fields.iter().enumerate() {
2407                    if i > 0 {
2408                        f.write_str(", ")?;
2409                    }
2410                    write!(
2411                        f,
2412                        "{} => {}",
2413                        field.name,
2414                        values.get(i).cloned().unwrap_or(PerlValue::UNDEF)
2415                    )?;
2416                }
2417                f.write_str(")")
2418            }
2419            HeapObject::EnumInst(e) => {
2420                // Smart stringify: Color::Red or Maybe::Some(value)
2421                write!(f, "{}::{}", e.def.name, e.variant_name())?;
2422                if e.def.variants[e.variant_idx].ty.is_some() {
2423                    write!(f, "({})", e.data)?;
2424                }
2425                Ok(())
2426            }
2427            HeapObject::ClassInst(c) => {
2428                // Smart stringify: Dog(name => "Rex", age => 5)
2429                write!(f, "{}(", c.def.name)?;
2430                let values = c.values.read();
2431                for (i, field) in c.def.fields.iter().enumerate() {
2432                    if i > 0 {
2433                        f.write_str(", ")?;
2434                    }
2435                    write!(
2436                        f,
2437                        "{} => {}",
2438                        field.name,
2439                        values.get(i).cloned().unwrap_or(PerlValue::UNDEF)
2440                    )?;
2441                }
2442                f.write_str(")")
2443            }
2444            HeapObject::DataFrame(d) => {
2445                let g = d.lock();
2446                write!(f, "DataFrame({} rows)", g.nrows())
2447            }
2448            HeapObject::Iterator(_) => f.write_str("Iterator"),
2449        }
2450    }
2451}
2452
2453/// Stable key for set membership (dedup of `PerlValue` in this runtime).
2454pub fn set_member_key(v: &PerlValue) -> String {
2455    if nanbox::is_imm_undef(v.0) {
2456        return "u:".to_string();
2457    }
2458    if let Some(n) = nanbox::as_imm_int32(v.0) {
2459        return format!("i:{n}");
2460    }
2461    if nanbox::is_raw_float_bits(v.0) {
2462        return format!("f:{}", f64::from_bits(v.0).to_bits());
2463    }
2464    match unsafe { v.heap_ref() } {
2465        HeapObject::String(s) => format!("s:{s}"),
2466        HeapObject::Bytes(b) => {
2467            use std::fmt::Write as _;
2468            let mut h = String::with_capacity(b.len() * 2);
2469            for &x in b.iter() {
2470                let _ = write!(&mut h, "{:02x}", x);
2471            }
2472            format!("by:{h}")
2473        }
2474        HeapObject::Array(a) => {
2475            let parts: Vec<_> = a.iter().map(set_member_key).collect();
2476            format!("a:{}", parts.join(","))
2477        }
2478        HeapObject::Hash(h) => {
2479            let mut keys: Vec<_> = h.keys().cloned().collect();
2480            keys.sort();
2481            let parts: Vec<_> = keys
2482                .iter()
2483                .map(|k| format!("{}={}", k, set_member_key(h.get(k).unwrap())))
2484                .collect();
2485            format!("h:{}", parts.join(","))
2486        }
2487        HeapObject::Set(inner) => {
2488            let mut keys: Vec<_> = inner.keys().cloned().collect();
2489            keys.sort();
2490            format!("S:{}", keys.join(","))
2491        }
2492        HeapObject::ArrayRef(a) => {
2493            let g = a.read();
2494            let parts: Vec<_> = g.iter().map(set_member_key).collect();
2495            format!("ar:{}", parts.join(","))
2496        }
2497        HeapObject::HashRef(h) => {
2498            let g = h.read();
2499            let mut keys: Vec<_> = g.keys().cloned().collect();
2500            keys.sort();
2501            let parts: Vec<_> = keys
2502                .iter()
2503                .map(|k| format!("{}={}", k, set_member_key(g.get(k).unwrap())))
2504                .collect();
2505            format!("hr:{}", parts.join(","))
2506        }
2507        HeapObject::Blessed(b) => {
2508            let d = b.data.read();
2509            format!("b:{}:{}", b.class, set_member_key(&d))
2510        }
2511        HeapObject::ScalarRef(_) | HeapObject::ScalarBindingRef(_) | HeapObject::CaptureCell(_) => {
2512            format!("sr:{v}")
2513        }
2514        HeapObject::ArrayBindingRef(n) => format!("abind:{n}"),
2515        HeapObject::HashBindingRef(n) => format!("hbind:{n}"),
2516        HeapObject::CodeRef(_) => format!("c:{v}"),
2517        HeapObject::Regex(_, src, _) => format!("r:{src}"),
2518        HeapObject::IOHandle(s) => format!("io:{s}"),
2519        HeapObject::Atomic(arc) => format!("at:{}", set_member_key(&arc.lock())),
2520        HeapObject::ChannelTx(tx) => format!("chtx:{:p}", Arc::as_ptr(tx)),
2521        HeapObject::ChannelRx(rx) => format!("chrx:{:p}", Arc::as_ptr(rx)),
2522        HeapObject::AsyncTask(t) => format!("async:{:p}", Arc::as_ptr(t)),
2523        HeapObject::Generator(g) => format!("gen:{:p}", Arc::as_ptr(g)),
2524        HeapObject::Deque(d) => format!("dq:{:p}", Arc::as_ptr(d)),
2525        HeapObject::Heap(h) => format!("hp:{:p}", Arc::as_ptr(h)),
2526        HeapObject::Pipeline(p) => format!("pl:{:p}", Arc::as_ptr(p)),
2527        HeapObject::Capture(c) => format!("cap:{:p}", Arc::as_ptr(c)),
2528        HeapObject::Ppool(p) => format!("pp:{:p}", Arc::as_ptr(&p.0)),
2529        HeapObject::RemoteCluster(c) => format!("rcl:{:p}", Arc::as_ptr(c)),
2530        HeapObject::Barrier(b) => format!("br:{:p}", Arc::as_ptr(&b.0)),
2531        HeapObject::SqliteConn(c) => format!("sql:{:p}", Arc::as_ptr(c)),
2532        HeapObject::StructInst(s) => format!("st:{}:{:?}", s.def.name, s.values),
2533        HeapObject::EnumInst(e) => {
2534            format!("en:{}::{}:{}", e.def.name, e.variant_name(), e.data)
2535        }
2536        HeapObject::ClassInst(c) => format!("cl:{}:{:?}", c.def.name, c.values),
2537        HeapObject::DataFrame(d) => format!("df:{:p}", Arc::as_ptr(d)),
2538        HeapObject::Iterator(_) => "iter".to_string(),
2539        HeapObject::ErrnoDual { code, msg } => format!("e:{code}:{msg}"),
2540        HeapObject::Integer(n) => format!("i:{n}"),
2541        HeapObject::BigInt(b) => format!("bi:{b}"),
2542        HeapObject::Float(fl) => format!("f:{}", fl.to_bits()),
2543    }
2544}
2545
2546/// Perl-style integer modulo: floored division, so the result has the
2547/// sign of the divisor (or is zero). Defined for all `b != 0`. Rust's
2548/// `%` operator returns the sign of the dividend, which differs whenever
2549/// the operands have opposite signs.
2550///
2551/// Examples (matching Perl 5.42):
2552///   `perl_mod_i64(-7, 3) =  2`
2553///   `perl_mod_i64( 7,-3) = -2`
2554///   `perl_mod_i64(-7,-3) = -1`
2555///   `perl_mod_i64( 7, 3) =  1`
2556#[inline]
2557pub fn perl_mod_i64(a: i64, b: i64) -> i64 {
2558    debug_assert_ne!(b, 0);
2559    let r = a.wrapping_rem(b);
2560    // Sign mismatch between r and b, and r is non-zero → snap toward
2561    // the divisor's sign by adding b (won't overflow since |r| < |b|).
2562    if r != 0 && (r ^ b) < 0 {
2563        r + b
2564    } else {
2565        r
2566    }
2567}
2568
2569/// `--compat`-aware integer multiply. In compat mode, promotes to `BigInt` on
2570/// overflow. In native mode, wraps (preserves current behavior). Either side
2571/// already being a `BigInt` forces the BigInt path.
2572#[inline]
2573pub fn compat_mul(a: &PerlValue, b: &PerlValue) -> PerlValue {
2574    if a.as_bigint().is_some() || b.as_bigint().is_some() {
2575        return PerlValue::bigint(a.to_bigint() * b.to_bigint());
2576    }
2577    let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) else {
2578        return PerlValue::float(a.to_number() * b.to_number());
2579    };
2580    if crate::compat_mode() || crate::bigint_pragma() {
2581        match x.checked_mul(y) {
2582            Some(r) => PerlValue::integer(r),
2583            None => PerlValue::bigint(BigInt::from(x) * BigInt::from(y)),
2584        }
2585    } else {
2586        PerlValue::integer(x.wrapping_mul(y))
2587    }
2588}
2589
2590#[inline]
2591pub fn compat_add(a: &PerlValue, b: &PerlValue) -> PerlValue {
2592    if a.as_bigint().is_some() || b.as_bigint().is_some() {
2593        return PerlValue::bigint(a.to_bigint() + b.to_bigint());
2594    }
2595    let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) else {
2596        return PerlValue::float(a.to_number() + b.to_number());
2597    };
2598    if crate::compat_mode() || crate::bigint_pragma() {
2599        match x.checked_add(y) {
2600            Some(r) => PerlValue::integer(r),
2601            None => PerlValue::bigint(BigInt::from(x) + BigInt::from(y)),
2602        }
2603    } else {
2604        PerlValue::integer(x.wrapping_add(y))
2605    }
2606}
2607
2608#[inline]
2609pub fn compat_sub(a: &PerlValue, b: &PerlValue) -> PerlValue {
2610    if a.as_bigint().is_some() || b.as_bigint().is_some() {
2611        return PerlValue::bigint(a.to_bigint() - b.to_bigint());
2612    }
2613    let (Some(x), Some(y)) = (a.as_integer(), b.as_integer()) else {
2614        return PerlValue::float(a.to_number() - b.to_number());
2615    };
2616    if crate::compat_mode() || crate::bigint_pragma() {
2617        match x.checked_sub(y) {
2618            Some(r) => PerlValue::integer(r),
2619            None => PerlValue::bigint(BigInt::from(x) - BigInt::from(y)),
2620        }
2621    } else {
2622        PerlValue::integer(x.wrapping_sub(y))
2623    }
2624}
2625
2626/// `**` (exponentiation) — under `--compat` or `use bigint;`, uses `BigInt`
2627/// directly when the exponent is a non-negative integer so `2 ** 100`
2628/// works. Falls through to `f64::powf` for negative or non-integer
2629/// exponents (matches Perl's behavior).
2630#[inline]
2631pub fn compat_pow(a: &PerlValue, b: &PerlValue) -> PerlValue {
2632    let (Some(base), Some(exp)) = (a.as_integer(), b.as_integer()) else {
2633        return PerlValue::float(a.to_number().powf(b.to_number()));
2634    };
2635    let bigint_active = crate::compat_mode() || crate::bigint_pragma();
2636    if !bigint_active {
2637        // Native: do whatever the existing path does — fall back to float
2638        // (matches Perl's default i64-overflow-to-NV behavior).
2639        return PerlValue::float((base as f64).powf(exp as f64));
2640    }
2641    if exp < 0 {
2642        return PerlValue::float((base as f64).powf(exp as f64));
2643    }
2644    use num_traits::Pow;
2645    let result = BigInt::from(base).pow(exp as u32);
2646    PerlValue::bigint(result)
2647}
2648
2649pub fn set_from_elements<I: IntoIterator<Item = PerlValue>>(items: I) -> PerlValue {
2650    let mut map = PerlSet::new();
2651    for v in items {
2652        let k = set_member_key(&v);
2653        map.insert(k, v);
2654    }
2655    PerlValue::set(Arc::new(map))
2656}
2657
2658/// Underlying set for union/intersection, including `mysync $s` (`Atomic` wrapping `Set`).
2659#[inline]
2660pub fn set_payload(v: &PerlValue) -> Option<Arc<PerlSet>> {
2661    if !nanbox::is_heap(v.0) {
2662        return None;
2663    }
2664    match unsafe { v.heap_ref() } {
2665        HeapObject::Set(s) => Some(Arc::clone(s)),
2666        HeapObject::Atomic(a) => set_payload(&a.lock()),
2667        _ => None,
2668    }
2669}
2670
2671pub fn set_union(a: &PerlValue, b: &PerlValue) -> Option<PerlValue> {
2672    let ia = set_payload(a)?;
2673    let ib = set_payload(b)?;
2674    let mut m = (*ia).clone();
2675    for (k, v) in ib.iter() {
2676        m.entry(k.clone()).or_insert_with(|| v.clone());
2677    }
2678    Some(PerlValue::set(Arc::new(m)))
2679}
2680
2681pub fn set_intersection(a: &PerlValue, b: &PerlValue) -> Option<PerlValue> {
2682    let ia = set_payload(a)?;
2683    let ib = set_payload(b)?;
2684    let mut m = PerlSet::new();
2685    for (k, v) in ia.iter() {
2686        if ib.contains_key(k) {
2687            m.insert(k.clone(), v.clone());
2688        }
2689    }
2690    Some(PerlValue::set(Arc::new(m)))
2691}
2692fn parse_number(s: &str) -> f64 {
2693    let s = s.trim();
2694    if s.is_empty() {
2695        return 0.0;
2696    }
2697    // Perl 5.22+ recognizes "Inf" / "Infinity" / "NaN" (case-insensitive,
2698    // optional leading sign) as float specials. We accept the same forms.
2699    {
2700        let bytes = s.as_bytes();
2701        let (sign, rest) = match bytes.first() {
2702            Some(b'+') => (1.0_f64, &s[1..]),
2703            Some(b'-') => (-1.0_f64, &s[1..]),
2704            _ => (1.0_f64, s),
2705        };
2706        if rest.eq_ignore_ascii_case("inf") || rest.eq_ignore_ascii_case("infinity") {
2707            return sign * f64::INFINITY;
2708        }
2709        if rest.eq_ignore_ascii_case("nan") {
2710            // Perl's sign on NaN is preserved through arithmetic; here we
2711            // just return the canonical NaN bit pattern. Sign on NaN is
2712            // not observable via `==` anyway.
2713            return f64::NAN;
2714        }
2715    }
2716    // Perl extracts leading numeric portion
2717    let mut end = 0;
2718    let bytes = s.as_bytes();
2719    if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
2720        end += 1;
2721    }
2722    while end < bytes.len() && bytes[end].is_ascii_digit() {
2723        end += 1;
2724    }
2725    if end < bytes.len() && bytes[end] == b'.' {
2726        end += 1;
2727        while end < bytes.len() && bytes[end].is_ascii_digit() {
2728            end += 1;
2729        }
2730    }
2731    if end < bytes.len() && (bytes[end] == b'e' || bytes[end] == b'E') {
2732        end += 1;
2733        if end < bytes.len() && (bytes[end] == b'+' || bytes[end] == b'-') {
2734            end += 1;
2735        }
2736        while end < bytes.len() && bytes[end].is_ascii_digit() {
2737            end += 1;
2738        }
2739    }
2740    if end == 0 {
2741        return 0.0;
2742    }
2743    s[..end].parse::<f64>().unwrap_or(0.0)
2744}
2745
2746fn format_float(f: f64) -> String {
2747    // Perl prints float specials as "Inf" / "-Inf" / "NaN".
2748    if f.is_nan() {
2749        return "NaN".to_string();
2750    }
2751    if f.is_infinite() {
2752        return if f.is_sign_negative() {
2753            "-Inf".to_string()
2754        } else {
2755            "Inf".to_string()
2756        };
2757    }
2758    if f.fract() == 0.0 && f.abs() < 1e16 {
2759        format!("{}", f as i64)
2760    } else {
2761        // Perl uses Gconvert which is sprintf("%.15g", f) on most platforms.
2762        let mut buf = [0u8; 64];
2763        unsafe {
2764            libc::snprintf(
2765                buf.as_mut_ptr() as *mut libc::c_char,
2766                buf.len(),
2767                c"%.15g".as_ptr(),
2768                f,
2769            );
2770            std::ffi::CStr::from_ptr(buf.as_ptr() as *const libc::c_char)
2771                .to_string_lossy()
2772                .into_owned()
2773        }
2774    }
2775}
2776
2777/// Result of one magical string increment step in a list-context `..` range (Perl `sv_inc`).
2778#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2779pub(crate) enum PerlListRangeIncOutcome {
2780    Continue,
2781    /// Perl upgraded the scalar to a numeric form (`SvNIOKp`); list range stops after this step.
2782    BecameNumeric,
2783}
2784
2785/// Perl `looks_like_number` / `grok_number` subset: `s` must be **entirely** a numeric string
2786/// (after trim), with no trailing garbage. Used for `RANGE_IS_NUMERIC` in `pp_flop`.
2787fn perl_str_looks_like_number_for_range(s: &str) -> bool {
2788    let t = s.trim();
2789    if t.is_empty() {
2790        return s.is_empty();
2791    }
2792    let b = t.as_bytes();
2793    let mut i = 0usize;
2794    if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2795        i += 1;
2796    }
2797    if i >= b.len() {
2798        return false;
2799    }
2800    let mut saw_digit = false;
2801    while i < b.len() && b[i].is_ascii_digit() {
2802        saw_digit = true;
2803        i += 1;
2804    }
2805    if i < b.len() && b[i] == b'.' {
2806        i += 1;
2807        while i < b.len() && b[i].is_ascii_digit() {
2808            saw_digit = true;
2809            i += 1;
2810        }
2811    }
2812    if !saw_digit {
2813        return false;
2814    }
2815    if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
2816        i += 1;
2817        if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2818            i += 1;
2819        }
2820        let exp0 = i;
2821        while i < b.len() && b[i].is_ascii_digit() {
2822            i += 1;
2823        }
2824        if i == exp0 {
2825            return false;
2826        }
2827    }
2828    i == b.len()
2829}
2830
2831/// Whether list-context `..` uses Perl's **numeric** counting (`pp_flop` `RANGE_IS_NUMERIC`).
2832pub(crate) fn perl_list_range_pair_is_numeric(left: &PerlValue, right: &PerlValue) -> bool {
2833    if left.is_integer_like() || left.is_float_like() {
2834        return true;
2835    }
2836    if !left.is_undef() && !left.is_string_like() {
2837        return true;
2838    }
2839    if right.is_integer_like() || right.is_float_like() {
2840        return true;
2841    }
2842    if !right.is_undef() && !right.is_string_like() {
2843        return true;
2844    }
2845
2846    let left_ok = !left.is_undef();
2847    let right_ok = !right.is_undef();
2848    let left_pok = left.is_string_like();
2849    let left_pv = left.as_str_or_empty();
2850    let right_pv = right.as_str_or_empty();
2851
2852    let left_n = perl_str_looks_like_number_for_range(&left_pv);
2853    let right_n = perl_str_looks_like_number_for_range(&right_pv);
2854
2855    let left_zero_prefix =
2856        left_pok && left_pv.len() > 1 && left_pv.as_bytes().first() == Some(&b'0');
2857
2858    let clause5_left =
2859        (!left_ok && right_ok) || ((!left_ok || left_n) && left_pok && !left_zero_prefix);
2860    clause5_left && (!right_ok || right_n)
2861}
2862
2863/// Magical string `++` for ASCII letter/digit runs (Perl `sv_inc_nomg`, non-EBCDIC).
2864pub(crate) fn perl_magic_string_increment_for_range(s: &mut String) -> PerlListRangeIncOutcome {
2865    if s.is_empty() {
2866        return PerlListRangeIncOutcome::BecameNumeric;
2867    }
2868    let b = s.as_bytes();
2869    let mut i = 0usize;
2870    while i < b.len() && b[i].is_ascii_alphabetic() {
2871        i += 1;
2872    }
2873    while i < b.len() && b[i].is_ascii_digit() {
2874        i += 1;
2875    }
2876    if i < b.len() {
2877        let n = parse_number(s) + 1.0;
2878        *s = format_float(n);
2879        return PerlListRangeIncOutcome::BecameNumeric;
2880    }
2881
2882    let bytes = unsafe { s.as_mut_vec() };
2883    let mut idx = bytes.len() - 1;
2884    loop {
2885        if bytes[idx].is_ascii_digit() {
2886            bytes[idx] += 1;
2887            if bytes[idx] <= b'9' {
2888                return PerlListRangeIncOutcome::Continue;
2889            }
2890            bytes[idx] = b'0';
2891            if idx == 0 {
2892                bytes.insert(0, b'1');
2893                return PerlListRangeIncOutcome::Continue;
2894            }
2895            idx -= 1;
2896        } else {
2897            bytes[idx] = bytes[idx].wrapping_add(1);
2898            if bytes[idx].is_ascii_alphabetic() {
2899                return PerlListRangeIncOutcome::Continue;
2900            }
2901            bytes[idx] = bytes[idx].wrapping_sub(b'z' - b'a' + 1);
2902            if idx == 0 {
2903                let c = bytes[0];
2904                bytes.insert(0, if c.is_ascii_digit() { b'1' } else { c });
2905                return PerlListRangeIncOutcome::Continue;
2906            }
2907            idx -= 1;
2908        }
2909    }
2910}
2911
2912/// Magical string `--` for ASCII letter/digit runs (stryke extension — Perl doesn't have this).
2913/// Returns `None` if we've hit the floor (e.g., "a" can't decrement, "aa" → "z").
2914pub(crate) fn perl_magic_string_decrement_for_range(s: &mut String) -> Option<()> {
2915    if s.is_empty() {
2916        return None;
2917    }
2918    // Validate: must be all alpha then all digit (like increment)
2919    let b = s.as_bytes();
2920    let mut i = 0usize;
2921    while i < b.len() && b[i].is_ascii_alphabetic() {
2922        i += 1;
2923    }
2924    while i < b.len() && b[i].is_ascii_digit() {
2925        i += 1;
2926    }
2927    if i < b.len() {
2928        return None; // Not a pure alpha/digit string
2929    }
2930
2931    let bytes = unsafe { s.as_mut_vec() };
2932    let mut idx = bytes.len() - 1;
2933    loop {
2934        if bytes[idx].is_ascii_digit() {
2935            if bytes[idx] > b'0' {
2936                bytes[idx] -= 1;
2937                return Some(());
2938            }
2939            // Borrow: '0' becomes '9', continue to next position
2940            bytes[idx] = b'9';
2941            if idx == 0 {
2942                // "0" → can't go lower, or "00" → "9" (shrink)
2943                if bytes.len() == 1 {
2944                    bytes[0] = b'0'; // restore, signal floor
2945                    return None;
2946                }
2947                bytes.remove(0);
2948                return Some(());
2949            }
2950            idx -= 1;
2951        } else if bytes[idx].is_ascii_lowercase() {
2952            if bytes[idx] > b'a' {
2953                bytes[idx] -= 1;
2954                return Some(());
2955            }
2956            // Borrow: 'a' becomes 'z', continue to next position
2957            bytes[idx] = b'z';
2958            if idx == 0 {
2959                // "a" can't decrement, "aa" → "z"
2960                if bytes.len() == 1 {
2961                    bytes[0] = b'a'; // restore
2962                    return None;
2963                }
2964                bytes.remove(0);
2965                return Some(());
2966            }
2967            idx -= 1;
2968        } else if bytes[idx].is_ascii_uppercase() {
2969            if bytes[idx] > b'A' {
2970                bytes[idx] -= 1;
2971                return Some(());
2972            }
2973            // Borrow: 'A' becomes 'Z', continue to next position
2974            bytes[idx] = b'Z';
2975            if idx == 0 {
2976                if bytes.len() == 1 {
2977                    bytes[0] = b'A'; // restore
2978                    return None;
2979                }
2980                bytes.remove(0);
2981                return Some(());
2982            }
2983            idx -= 1;
2984        } else {
2985            return None;
2986        }
2987    }
2988}
2989
2990fn perl_list_range_max_bound(right: &str) -> usize {
2991    if right.is_ascii() {
2992        right.len()
2993    } else {
2994        right.chars().count()
2995    }
2996}
2997
2998fn perl_list_range_cur_bound(cur: &str, right_is_ascii: bool) -> usize {
2999    if right_is_ascii {
3000        cur.len()
3001    } else {
3002        cur.chars().count()
3003    }
3004}
3005
3006fn perl_list_range_expand_string_magic(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
3007    let mut cur = from.into_string();
3008    let right = to.into_string();
3009    let right_ascii = right.is_ascii();
3010    let max_bound = perl_list_range_max_bound(&right);
3011    let mut out = Vec::new();
3012    let mut guard = 0usize;
3013    loop {
3014        guard += 1;
3015        if guard > 50_000_000 {
3016            break;
3017        }
3018        let cur_bound = perl_list_range_cur_bound(&cur, right_ascii);
3019        if cur_bound > max_bound {
3020            break;
3021        }
3022        out.push(PerlValue::string(cur.clone()));
3023        if cur == right {
3024            break;
3025        }
3026        match perl_magic_string_increment_for_range(&mut cur) {
3027            PerlListRangeIncOutcome::Continue => {}
3028            PerlListRangeIncOutcome::BecameNumeric => break,
3029        }
3030    }
3031    out
3032}
3033
3034/// Perl list-context `..` (`pp_flop`): numeric counting or magical string sequence.
3035pub(crate) fn perl_list_range_expand(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
3036    if perl_list_range_pair_is_numeric(&from, &to) {
3037        let i = from.to_int();
3038        let j = to.to_int();
3039        if j >= i {
3040            (i..=j).map(PerlValue::integer).collect()
3041        } else {
3042            Vec::new()
3043        }
3044    } else {
3045        perl_list_range_expand_string_magic(from, to)
3046    }
3047}
3048
3049// ═══════════════════════════════════════════════════════════════════════════════
3050// Polymorphic range types — stryke extension (world first!)
3051// ═══════════════════════════════════════════════════════════════════════════════
3052
3053/// Check if string is a valid Roman numeral.
3054fn is_roman_numeral(s: &str) -> bool {
3055    if s.is_empty() {
3056        return false;
3057    }
3058    let upper = s.to_ascii_uppercase();
3059    upper
3060        .chars()
3061        .all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))
3062}
3063
3064/// Check if string is an IPv4 address.
3065fn is_ipv4(s: &str) -> bool {
3066    let parts: Vec<&str> = s.split('.').collect();
3067    parts.len() == 4 && parts.iter().all(|p| p.parse::<u8>().is_ok())
3068}
3069
3070/// Parse IPv4 to u32.
3071fn ipv4_to_u32(s: &str) -> Option<u32> {
3072    let parts: Vec<u8> = s.split('.').filter_map(|p| p.parse().ok()).collect();
3073    if parts.len() != 4 {
3074        return None;
3075    }
3076    Some(
3077        ((parts[0] as u32) << 24)
3078            | ((parts[1] as u32) << 16)
3079            | ((parts[2] as u32) << 8)
3080            | (parts[3] as u32),
3081    )
3082}
3083
3084/// Convert u32 to IPv4 string.
3085fn u32_to_ipv4(n: u32) -> String {
3086    format!(
3087        "{}.{}.{}.{}",
3088        (n >> 24) & 0xFF,
3089        (n >> 16) & 0xFF,
3090        (n >> 8) & 0xFF,
3091        n & 0xFF
3092    )
3093}
3094
3095/// IPv4 range with step.
3096fn ipv4_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3097    let Some(start) = ipv4_to_u32(from) else {
3098        return vec![];
3099    };
3100    let Some(end) = ipv4_to_u32(to) else {
3101        return vec![];
3102    };
3103    let mut out = Vec::new();
3104    if step > 0 {
3105        let mut cur = start as i64;
3106        while cur <= end as i64 {
3107            out.push(PerlValue::string(u32_to_ipv4(cur as u32)));
3108            cur += step;
3109        }
3110    } else {
3111        let mut cur = start as i64;
3112        while cur >= end as i64 {
3113            out.push(PerlValue::string(u32_to_ipv4(cur as u32)));
3114            cur += step;
3115        }
3116    }
3117    out
3118}
3119
3120/// Check if string is a valid IPv6 address. Uses Rust's parser so all
3121/// compressed (`::`), full (8-group), and IPv4-mapped forms are accepted.
3122fn is_ipv6(s: &str) -> bool {
3123    s.parse::<std::net::Ipv6Addr>().is_ok()
3124}
3125
3126/// Check if string is a `0x…` / `0X…` hex literal in source-form. Used by
3127/// the range op to keep `0x00:0xFF:1` iterating as hex strings instead of
3128/// decimal. Returns true only when the prefix is present and the body is
3129/// non-empty hex digits.
3130fn is_hex_source_literal(s: &str) -> bool {
3131    let bytes = s.as_bytes();
3132    bytes.len() > 2
3133        && bytes[0] == b'0'
3134        && (bytes[1] == b'x' || bytes[1] == b'X')
3135        && bytes[2..].iter().all(|b| b.is_ascii_hexdigit())
3136}
3137
3138/// Iterate a hex range with step. Output values preserve:
3139/// - The `0x` / `0X` prefix from the FROM endpoint.
3140/// - The minimum digit width to fit either endpoint (zero-padded to that).
3141/// - Uppercase iff EITHER endpoint had any uppercase letter — once the user
3142///   types `0xFF` we keep the case for every value in the range, even when
3143///   the FROM endpoint (`0x00`) had no letters of its own to disambiguate.
3144fn hex_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3145    let from_body = &from[2..];
3146    let to_body = &to[2..];
3147    let Ok(start) = i64::from_str_radix(from_body, 16) else {
3148        return vec![];
3149    };
3150    let Ok(end) = i64::from_str_radix(to_body, 16) else {
3151        return vec![];
3152    };
3153    let prefix = &from[..2];
3154    let width = from_body.len().max(to_body.len());
3155    let upper = from_body.bytes().any(|b| b.is_ascii_uppercase())
3156        || to_body.bytes().any(|b| b.is_ascii_uppercase());
3157    let mut out = Vec::new();
3158    let format_one = |n: i64, width: usize, upper: bool, prefix: &str| -> String {
3159        if upper {
3160            format!("{}{:0>w$X}", prefix, n, w = width)
3161        } else {
3162            format!("{}{:0>w$x}", prefix, n, w = width)
3163        }
3164    };
3165    if step > 0 {
3166        if start > end {
3167            return out;
3168        }
3169        let mut cur = start;
3170        while cur <= end {
3171            out.push(PerlValue::string(format_one(cur, width, upper, prefix)));
3172            if (end - cur) < step {
3173                break;
3174            }
3175            cur += step;
3176        }
3177    } else if step < 0 {
3178        if start < end {
3179            return out;
3180        }
3181        let mut cur = start;
3182        while cur >= end {
3183            out.push(PerlValue::string(format_one(cur, width, upper, prefix)));
3184            if (cur - end) < (-step) {
3185                break;
3186            }
3187            cur += step;
3188        }
3189    }
3190    out
3191}
3192
3193fn ipv6_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3194    let Ok(start) = from.parse::<std::net::Ipv6Addr>() else {
3195        return vec![];
3196    };
3197    let Ok(end) = to.parse::<std::net::Ipv6Addr>() else {
3198        return vec![];
3199    };
3200    let s = u128::from(start);
3201    let e = u128::from(end);
3202    let mut out = Vec::new();
3203    if step > 0 {
3204        if s > e {
3205            return out; // start past end with positive step → empty
3206        }
3207        let step = step as u128;
3208        let mut cur = s;
3209        loop {
3210            out.push(PerlValue::string(std::net::Ipv6Addr::from(cur).to_string()));
3211            if cur == e || e.saturating_sub(cur) < step {
3212                break;
3213            }
3214            cur += step;
3215        }
3216    } else if step < 0 {
3217        if s < e {
3218            return out; // start before end with negative step → empty
3219        }
3220        let step = (-step) as u128;
3221        let mut cur = s;
3222        loop {
3223            out.push(PerlValue::string(std::net::Ipv6Addr::from(cur).to_string()));
3224            if cur == e || cur.saturating_sub(e) < step {
3225                break;
3226            }
3227            cur -= step;
3228        }
3229    }
3230    out
3231}
3232
3233/// Check if string is ISO date YYYY-MM-DD.
3234fn is_iso_date(s: &str) -> bool {
3235    if s.len() != 10 {
3236        return false;
3237    }
3238    let parts: Vec<&str> = s.split('-').collect();
3239    parts.len() == 3
3240        && parts[0].len() == 4
3241        && parts[0].parse::<u16>().is_ok()
3242        && parts[1].len() == 2
3243        && parts[1]
3244            .parse::<u8>()
3245            .map(|m| (1..=12).contains(&m))
3246            .unwrap_or(false)
3247        && parts[2].len() == 2
3248        && parts[2]
3249            .parse::<u8>()
3250            .map(|d| (1..=31).contains(&d))
3251            .unwrap_or(false)
3252}
3253
3254/// Check if string is YYYY-MM (month range).
3255fn is_year_month(s: &str) -> bool {
3256    if s.len() != 7 {
3257        return false;
3258    }
3259    let parts: Vec<&str> = s.split('-').collect();
3260    parts.len() == 2
3261        && parts[0].len() == 4
3262        && parts[0].parse::<u16>().is_ok()
3263        && parts[1].len() == 2
3264        && parts[1]
3265            .parse::<u8>()
3266            .map(|m| (1..=12).contains(&m))
3267            .unwrap_or(false)
3268}
3269
3270/// Parse ISO date to (year, month, day).
3271fn parse_iso_date(s: &str) -> Option<(i32, u32, u32)> {
3272    let parts: Vec<&str> = s.split('-').collect();
3273    if parts.len() != 3 {
3274        return None;
3275    }
3276    Some((
3277        parts[0].parse().ok()?,
3278        parts[1].parse().ok()?,
3279        parts[2].parse().ok()?,
3280    ))
3281}
3282
3283/// Parse YYYY-MM to (year, month).
3284fn parse_year_month(s: &str) -> Option<(i32, u32)> {
3285    let parts: Vec<&str> = s.split('-').collect();
3286    if parts.len() != 2 {
3287        return None;
3288    }
3289    Some((parts[0].parse().ok()?, parts[1].parse().ok()?))
3290}
3291
3292/// Days in month (handles leap years).
3293fn days_in_month(year: i32, month: u32) -> u32 {
3294    match month {
3295        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3296        4 | 6 | 9 | 11 => 30,
3297        2 => {
3298            if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
3299                29
3300            } else {
3301                28
3302            }
3303        }
3304        _ => 30,
3305    }
3306}
3307
3308/// Add days to a date, returning new (year, month, day).
3309fn add_days(mut year: i32, mut month: u32, mut day: u32, mut delta: i64) -> (i32, u32, u32) {
3310    if delta > 0 {
3311        while delta > 0 {
3312            let dim = days_in_month(year, month);
3313            let remaining = dim - day;
3314            if delta <= remaining as i64 {
3315                day += delta as u32;
3316                break;
3317            }
3318            delta -= (remaining + 1) as i64;
3319            day = 1;
3320            month += 1;
3321            if month > 12 {
3322                month = 1;
3323                year += 1;
3324            }
3325        }
3326    } else {
3327        while delta < 0 {
3328            if (-delta) < day as i64 {
3329                day = (day as i64 + delta) as u32;
3330                break;
3331            }
3332            delta += day as i64;
3333            month -= 1;
3334            if month == 0 {
3335                month = 12;
3336                year -= 1;
3337            }
3338            day = days_in_month(year, month);
3339        }
3340    }
3341    (year, month, day)
3342}
3343
3344/// ISO date range with step (step = days).
3345fn iso_date_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3346    let Some((mut y, mut m, mut d)) = parse_iso_date(from) else {
3347        return vec![];
3348    };
3349    let Some((ey, em, ed)) = parse_iso_date(to) else {
3350        return vec![];
3351    };
3352    let mut out = Vec::new();
3353    let mut guard = 0;
3354    if step > 0 {
3355        while (y, m, d) <= (ey, em, ed) && guard < 50_000 {
3356            out.push(PerlValue::string(format!("{:04}-{:02}-{:02}", y, m, d)));
3357            (y, m, d) = add_days(y, m, d, step);
3358            guard += 1;
3359        }
3360    } else {
3361        while (y, m, d) >= (ey, em, ed) && guard < 50_000 {
3362            out.push(PerlValue::string(format!("{:04}-{:02}-{:02}", y, m, d)));
3363            (y, m, d) = add_days(y, m, d, step);
3364            guard += 1;
3365        }
3366    }
3367    out
3368}
3369
3370/// Add months to (year, month).
3371fn add_months(mut year: i32, mut month: u32, delta: i64) -> (i32, u32) {
3372    let total = (year as i64 * 12 + month as i64 - 1) + delta;
3373    year = (total / 12) as i32;
3374    month = ((total % 12) + 1) as u32;
3375    if month == 0 {
3376        month = 12;
3377        year -= 1;
3378    }
3379    (year, month)
3380}
3381
3382/// YYYY-MM range with step (step = months).
3383fn year_month_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3384    let Some((mut y, mut m)) = parse_year_month(from) else {
3385        return vec![];
3386    };
3387    let Some((ey, em)) = parse_year_month(to) else {
3388        return vec![];
3389    };
3390    let mut out = Vec::new();
3391    let mut guard = 0;
3392    if step > 0 {
3393        while (y, m) <= (ey, em) && guard < 50_000 {
3394            out.push(PerlValue::string(format!("{:04}-{:02}", y, m)));
3395            (y, m) = add_months(y, m, step);
3396            guard += 1;
3397        }
3398    } else {
3399        while (y, m) >= (ey, em) && guard < 50_000 {
3400            out.push(PerlValue::string(format!("{:04}-{:02}", y, m)));
3401            (y, m) = add_months(y, m, step);
3402            guard += 1;
3403        }
3404    }
3405    out
3406}
3407
3408/// Check if string looks like HH:MM time.
3409fn is_time_hhmm(s: &str) -> bool {
3410    if s.len() != 5 {
3411        return false;
3412    }
3413    let parts: Vec<&str> = s.split(':').collect();
3414    parts.len() == 2
3415        && parts[0].len() == 2
3416        && parts[0].parse::<u8>().map(|h| h < 24).unwrap_or(false)
3417        && parts[1].len() == 2
3418        && parts[1].parse::<u8>().map(|m| m < 60).unwrap_or(false)
3419}
3420
3421/// Parse HH:MM to minutes since midnight.
3422fn parse_time_hhmm(s: &str) -> Option<i32> {
3423    let parts: Vec<&str> = s.split(':').collect();
3424    if parts.len() != 2 {
3425        return None;
3426    }
3427    let h: i32 = parts[0].parse().ok()?;
3428    let m: i32 = parts[1].parse().ok()?;
3429    Some(h * 60 + m)
3430}
3431
3432/// Minutes to HH:MM string.
3433fn minutes_to_hhmm(mins: i32) -> String {
3434    let h = (mins / 60) % 24;
3435    let m = mins % 60;
3436    format!("{:02}:{:02}", h, m)
3437}
3438
3439/// HH:MM time range with step (step = minutes).
3440fn time_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3441    let Some(start) = parse_time_hhmm(from) else {
3442        return vec![];
3443    };
3444    let Some(end) = parse_time_hhmm(to) else {
3445        return vec![];
3446    };
3447    let mut out = Vec::new();
3448    let mut guard = 0;
3449    if step > 0 {
3450        let mut cur = start;
3451        while cur <= end && guard < 50_000 {
3452            out.push(PerlValue::string(minutes_to_hhmm(cur)));
3453            cur += step as i32;
3454            guard += 1;
3455        }
3456    } else {
3457        let mut cur = start;
3458        while cur >= end && guard < 50_000 {
3459            out.push(PerlValue::string(minutes_to_hhmm(cur)));
3460            cur += step as i32;
3461            guard += 1;
3462        }
3463    }
3464    out
3465}
3466
3467const WEEKDAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
3468const WEEKDAYS_FULL: [&str; 7] = [
3469    "Monday",
3470    "Tuesday",
3471    "Wednesday",
3472    "Thursday",
3473    "Friday",
3474    "Saturday",
3475    "Sunday",
3476];
3477
3478/// Check if string is a weekday name.
3479fn weekday_index(s: &str) -> Option<usize> {
3480    let lower = s.to_ascii_lowercase();
3481    for (i, &d) in WEEKDAYS.iter().enumerate() {
3482        if d.to_ascii_lowercase() == lower {
3483            return Some(i);
3484        }
3485    }
3486    for (i, &d) in WEEKDAYS_FULL.iter().enumerate() {
3487        if d.to_ascii_lowercase() == lower {
3488            return Some(i);
3489        }
3490    }
3491    None
3492}
3493
3494/// Weekday range with step.
3495fn weekday_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3496    let Some(start) = weekday_index(from) else {
3497        return vec![];
3498    };
3499    let Some(end) = weekday_index(to) else {
3500        return vec![];
3501    };
3502    let full = from.len() > 3;
3503    let names = if full { &WEEKDAYS_FULL } else { &WEEKDAYS };
3504    let mut out = Vec::new();
3505    if step > 0 {
3506        let mut cur = start as i64;
3507        let target = if end >= start {
3508            end as i64
3509        } else {
3510            end as i64 + 7
3511        };
3512        while cur <= target {
3513            out.push(PerlValue::string(names[(cur % 7) as usize].to_string()));
3514            cur += step;
3515        }
3516    } else {
3517        let mut cur = start as i64;
3518        let target = if end <= start {
3519            end as i64
3520        } else {
3521            end as i64 - 7
3522        };
3523        while cur >= target {
3524            out.push(PerlValue::string(
3525                names[((cur % 7 + 7) % 7) as usize].to_string(),
3526            ));
3527            cur += step;
3528        }
3529    }
3530    out
3531}
3532
3533const MONTHS: [&str; 12] = [
3534    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
3535];
3536const MONTHS_FULL: [&str; 12] = [
3537    "January",
3538    "February",
3539    "March",
3540    "April",
3541    "May",
3542    "June",
3543    "July",
3544    "August",
3545    "September",
3546    "October",
3547    "November",
3548    "December",
3549];
3550
3551/// Check if string is a month name.
3552fn month_name_index(s: &str) -> Option<usize> {
3553    let lower = s.to_ascii_lowercase();
3554    for (i, &m) in MONTHS.iter().enumerate() {
3555        if m.to_ascii_lowercase() == lower {
3556            return Some(i);
3557        }
3558    }
3559    for (i, &m) in MONTHS_FULL.iter().enumerate() {
3560        if m.to_ascii_lowercase() == lower {
3561            return Some(i);
3562        }
3563    }
3564    None
3565}
3566
3567/// Month name range with step.
3568fn month_name_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3569    let Some(start) = month_name_index(from) else {
3570        return vec![];
3571    };
3572    let Some(end) = month_name_index(to) else {
3573        return vec![];
3574    };
3575    let full = from.len() > 3;
3576    let names = if full { &MONTHS_FULL } else { &MONTHS };
3577    let mut out = Vec::new();
3578    if step > 0 {
3579        let mut cur = start as i64;
3580        let target = if end >= start {
3581            end as i64
3582        } else {
3583            end as i64 + 12
3584        };
3585        while cur <= target {
3586            out.push(PerlValue::string(names[(cur % 12) as usize].to_string()));
3587            cur += step;
3588        }
3589    } else {
3590        let mut cur = start as i64;
3591        let target = if end <= start {
3592            end as i64
3593        } else {
3594            end as i64 - 12
3595        };
3596        while cur >= target {
3597            out.push(PerlValue::string(
3598                names[((cur % 12 + 12) % 12) as usize].to_string(),
3599            ));
3600            cur += step;
3601        }
3602    }
3603    out
3604}
3605
3606/// Check if both operands are float-like (contain decimal point, not date/time/IP).
3607fn is_float_pair(from: &str, to: &str) -> bool {
3608    fn is_float(s: &str) -> bool {
3609        s.contains('.')
3610            && !s.contains(':')
3611            && s.matches('.').count() == 1
3612            && s.parse::<f64>().is_ok()
3613    }
3614    is_float(from) && is_float(to)
3615}
3616
3617/// Float range with step.
3618fn float_range_stepped(from: &str, to: &str, step: f64) -> Vec<PerlValue> {
3619    let Ok(start) = from.parse::<f64>() else {
3620        return vec![];
3621    };
3622    let Ok(end) = to.parse::<f64>() else {
3623        return vec![];
3624    };
3625    let mut out = Vec::new();
3626    let mut guard = 0;
3627    // Use integer counting to avoid floating point accumulation errors
3628    if step > 0.0 {
3629        let mut i = 0i64;
3630        loop {
3631            let cur = start + (i as f64) * step;
3632            if cur > end + step.abs() * f64::EPSILON * 10.0 || guard >= 50_000 {
3633                break;
3634            }
3635            // Round to avoid floating point noise
3636            let rounded = (cur * 1e12).round() / 1e12;
3637            out.push(PerlValue::float(rounded));
3638            i += 1;
3639            guard += 1;
3640        }
3641    } else if step < 0.0 {
3642        let mut i = 0i64;
3643        loop {
3644            let cur = start + (i as f64) * step;
3645            if cur < end - step.abs() * f64::EPSILON * 10.0 || guard >= 50_000 {
3646                break;
3647            }
3648            let rounded = (cur * 1e12).round() / 1e12;
3649            out.push(PerlValue::float(rounded));
3650            i += 1;
3651            guard += 1;
3652        }
3653    }
3654    out
3655}
3656
3657/// Convert Roman numeral string to integer.
3658fn roman_to_int(s: &str) -> Option<i64> {
3659    let upper = s.to_ascii_uppercase();
3660    let mut result = 0i64;
3661    let mut prev = 0i64;
3662    for c in upper.chars().rev() {
3663        let val = match c {
3664            'I' => 1,
3665            'V' => 5,
3666            'X' => 10,
3667            'L' => 50,
3668            'C' => 100,
3669            'D' => 500,
3670            'M' => 1000,
3671            _ => return None,
3672        };
3673        if val < prev {
3674            result -= val;
3675        } else {
3676            result += val;
3677        }
3678        prev = val;
3679    }
3680    if result > 0 {
3681        Some(result)
3682    } else {
3683        None
3684    }
3685}
3686
3687/// Convert integer to Roman numeral string.
3688fn int_to_roman(mut n: i64, lowercase: bool) -> Option<String> {
3689    if n <= 0 || n > 3999 {
3690        return None;
3691    }
3692    let numerals = [
3693        (1000, "M"),
3694        (900, "CM"),
3695        (500, "D"),
3696        (400, "CD"),
3697        (100, "C"),
3698        (90, "XC"),
3699        (50, "L"),
3700        (40, "XL"),
3701        (10, "X"),
3702        (9, "IX"),
3703        (5, "V"),
3704        (4, "IV"),
3705        (1, "I"),
3706    ];
3707    let mut result = String::new();
3708    for (val, sym) in numerals {
3709        while n >= val {
3710            result.push_str(sym);
3711            n -= val;
3712        }
3713    }
3714    if lowercase {
3715        Some(result.to_ascii_lowercase())
3716    } else {
3717        Some(result)
3718    }
3719}
3720
3721/// Expand a Roman numeral range with step.
3722fn roman_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3723    let Some(start) = roman_to_int(from) else {
3724        return vec![];
3725    };
3726    let Some(end) = roman_to_int(to) else {
3727        return vec![];
3728    };
3729    let lowercase = from
3730        .chars()
3731        .next()
3732        .map(|c| c.is_ascii_lowercase())
3733        .unwrap_or(false);
3734
3735    let mut out = Vec::new();
3736    if step > 0 {
3737        let mut cur = start;
3738        while cur <= end {
3739            if let Some(r) = int_to_roman(cur, lowercase) {
3740                out.push(PerlValue::string(r));
3741            }
3742            cur += step;
3743        }
3744    } else {
3745        let mut cur = start;
3746        while cur >= end {
3747            if let Some(r) = int_to_roman(cur, lowercase) {
3748                out.push(PerlValue::string(r));
3749            }
3750            cur += step; // step is negative
3751        }
3752    }
3753    out
3754}
3755
3756/// Stepped range expansion — polymorphic across many types (stryke world first!).
3757/// Supports: integers, floats, strings, Roman numerals, dates, times, weekdays, months, IPv4.
3758pub(crate) fn perl_list_range_expand_stepped(
3759    from: PerlValue,
3760    to: PerlValue,
3761    step_val: PerlValue,
3762) -> Vec<PerlValue> {
3763    let from_str = from.to_string();
3764    let to_str = to.to_string();
3765
3766    // Check if this is a float range (operands have decimal points)
3767    let is_float_range = is_float_pair(&from_str, &to_str);
3768
3769    // Get step as float or int depending on context
3770    let step_float = step_val.as_float().unwrap_or(step_val.to_int() as f64);
3771    let step_int = step_val.to_int();
3772
3773    if step_int == 0 && step_float == 0.0 {
3774        return vec![];
3775    }
3776
3777    // Float ranges use float step
3778    if is_float_range {
3779        return float_range_stepped(&from_str, &to_str, step_float);
3780    }
3781
3782    // Pure numeric integers
3783    if perl_list_range_pair_is_numeric(&from, &to) {
3784        let i = from.to_int();
3785        let j = to.to_int();
3786        if step_int > 0 {
3787            (i..=j)
3788                .step_by(step_int as usize)
3789                .map(PerlValue::integer)
3790                .collect()
3791        } else {
3792            std::iter::successors(Some(i), |&x| {
3793                let next = x + step_int;
3794                if next >= j {
3795                    Some(next)
3796                } else {
3797                    None
3798                }
3799            })
3800            .map(PerlValue::integer)
3801            .collect()
3802        }
3803    } else {
3804        // Check special types in order of specificity
3805
3806        // Hex literals — must check before IPv4 because `0xFF` chars include
3807        // hex digits that aren't dotted-quad anyway, but keeping ordering
3808        // tight prevents future ambiguity. Preserves `0x` prefix, width,
3809        // and case from the source form.
3810        if is_hex_source_literal(&from_str) && is_hex_source_literal(&to_str) {
3811            return hex_range_stepped(&from_str, &to_str, step_int);
3812        }
3813
3814        // IPv4 addresses (must check before floats due to dots)
3815        if is_ipv4(&from_str) && is_ipv4(&to_str) {
3816            return ipv4_range_stepped(&from_str, &to_str, step_int);
3817        }
3818
3819        // IPv6 addresses — full or `::`-compressed. Uses the dedicated `!!!`
3820        // range separator so the IPv6's own colons don't collide with the
3821        // standard `:` range op.
3822        if is_ipv6(&from_str) && is_ipv6(&to_str) {
3823            return ipv6_range_stepped(&from_str, &to_str, step_int);
3824        }
3825
3826        // ISO dates YYYY-MM-DD (step = days)
3827        if is_iso_date(&from_str) && is_iso_date(&to_str) {
3828            return iso_date_range_stepped(&from_str, &to_str, step_int);
3829        }
3830
3831        // Year-month YYYY-MM (step = months)
3832        if is_year_month(&from_str) && is_year_month(&to_str) {
3833            return year_month_range_stepped(&from_str, &to_str, step_int);
3834        }
3835
3836        // Time HH:MM (step = minutes)
3837        if is_time_hhmm(&from_str) && is_time_hhmm(&to_str) {
3838            return time_range_stepped(&from_str, &to_str, step_int);
3839        }
3840
3841        // Weekday names
3842        if weekday_index(&from_str).is_some() && weekday_index(&to_str).is_some() {
3843            return weekday_range_stepped(&from_str, &to_str, step_int);
3844        }
3845
3846        // Month names
3847        if month_name_index(&from_str).is_some() && month_name_index(&to_str).is_some() {
3848            return month_name_range_stepped(&from_str, &to_str, step_int);
3849        }
3850
3851        // Roman numerals
3852        if is_roman_numeral(&from_str) && is_roman_numeral(&to_str) {
3853            return roman_range_stepped(&from_str, &to_str, step_int);
3854        }
3855
3856        // Fall back to magic string increment/decrement
3857        perl_list_range_expand_string_magic_stepped(from, to, step_int)
3858    }
3859}
3860
3861/// Coerce a slice endpoint to a strict integer. Used by [`Op::ArraySliceRange`] —
3862/// non-numeric strings, fractional floats, refs, and other non-integer types die.
3863/// `where_` is the diagnostic context (`"start"`, `"stop"`, `"step"`).
3864pub(crate) fn perl_slice_endpoint_to_strict_int(
3865    v: &PerlValue,
3866    where_: &str,
3867) -> Result<i64, String> {
3868    if let Some(n) = v.as_integer() {
3869        return Ok(n);
3870    }
3871    if let Some(f) = v.as_float() {
3872        if f.is_finite() && f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
3873            return Ok(f as i64);
3874        }
3875        return Err(format!(
3876            "array slice {}: non-integer float endpoint {}",
3877            where_, f
3878        ));
3879    }
3880    let s = v.as_str_or_empty();
3881    if !s.is_empty() {
3882        if let Ok(n) = s.trim().parse::<i64>() {
3883            return Ok(n);
3884        }
3885        return Err(format!(
3886            "array slice {}: non-integer string endpoint {:?}",
3887            where_, s
3888        ));
3889    }
3890    Err(format!(
3891        "array slice {}: endpoint must be an integer (got non-numeric value)",
3892        where_
3893    ))
3894}
3895
3896/// Resolve `from`/`to`/`step` for `@arr[FROM:TO:STEP]` (and open-ended forms) into the
3897/// concrete list of array indices. Closed inclusive on both ends. `Undef` endpoints
3898/// (the omitted-endpoint sentinel emitted by the compiler) default to:
3899/// - `step` → `1`
3900/// - `from` → `0` (positive step) or `arr_len-1` (negative step)
3901/// - `to`   → `arr_len-1` (positive step) or `0` (negative step)
3902///
3903/// Negative explicit indices count from the end (Perl semantics: `-1` = last element).
3904/// Returns `Err(msg)` for non-integer endpoints or zero step — caller dies with that.
3905pub(crate) fn compute_array_slice_indices(
3906    arr_len: i64,
3907    from: &PerlValue,
3908    to: &PerlValue,
3909    step: &PerlValue,
3910) -> Result<Vec<i64>, String> {
3911    let step_i = if step.is_undef() {
3912        1i64
3913    } else {
3914        perl_slice_endpoint_to_strict_int(step, "step")?
3915    };
3916    if step_i == 0 {
3917        return Err("array slice step cannot be 0".into());
3918    }
3919
3920    let normalize = |i: i64| -> i64 {
3921        if i < 0 {
3922            i + arr_len
3923        } else {
3924            i
3925        }
3926    };
3927
3928    // Open-ended slice (`@a[..3]`, `@a[-3..]`) is a stryke extension where each
3929    // explicit endpoint wraps once from the end. Closed `Range` slices
3930    // (`@a[0..-1]`, `@a[3..-1]`, `@a[-3..-1]`) follow Perl's raw-integer range
3931    // semantics: `0..-1` is empty, `-3..-1` is `(-3, -2, -1)`, and each
3932    // generated integer wraps individually when looked up.
3933    let any_undef = from.is_undef() || to.is_undef();
3934
3935    let from_raw = if from.is_undef() {
3936        if step_i > 0 {
3937            0
3938        } else {
3939            arr_len - 1
3940        }
3941    } else {
3942        perl_slice_endpoint_to_strict_int(from, "start")?
3943    };
3944
3945    let to_raw = if to.is_undef() {
3946        if step_i > 0 {
3947            arr_len - 1
3948        } else {
3949            0
3950        }
3951    } else {
3952        perl_slice_endpoint_to_strict_int(to, "stop")?
3953    };
3954
3955    let mut out = Vec::new();
3956    if arr_len == 0 {
3957        return Ok(out);
3958    }
3959
3960    let (from_i, to_i) = if any_undef {
3961        (normalize(from_raw), normalize(to_raw))
3962    } else {
3963        (from_raw, to_raw)
3964    };
3965
3966    if step_i > 0 {
3967        let mut i = from_i;
3968        while i <= to_i {
3969            out.push(if any_undef { i } else { normalize(i) });
3970            i += step_i;
3971        }
3972    } else {
3973        let mut i = from_i;
3974        while i >= to_i {
3975            out.push(if any_undef { i } else { normalize(i) });
3976            i += step_i; // step_i is negative
3977        }
3978    }
3979    Ok(out)
3980}
3981
3982/// Resolve `from`/`to`/`step` for `@h{FROM:TO:STEP}` into the concrete list of hash keys.
3983/// Both endpoints must be present (open-ended forms are nonsense for unordered hashes
3984/// and die). Endpoints stringify to keys; expansion uses the polymorphic stepped-range
3985/// machinery (numeric, magic-string-increment, Roman, etc.).
3986pub(crate) fn compute_hash_slice_keys(
3987    from: &PerlValue,
3988    to: &PerlValue,
3989    step: &PerlValue,
3990) -> Result<Vec<String>, String> {
3991    if from.is_undef() || to.is_undef() {
3992        return Err(
3993            "hash slice range requires both endpoints (open-ended forms not allowed)".into(),
3994        );
3995    }
3996    let step_val = if step.is_undef() {
3997        PerlValue::integer(1)
3998    } else {
3999        step.clone()
4000    };
4001    let expanded = perl_list_range_expand_stepped(from.clone(), to.clone(), step_val);
4002    Ok(expanded.into_iter().map(|v| v.to_string()).collect())
4003}
4004
4005fn perl_list_range_expand_string_magic_stepped(
4006    from: PerlValue,
4007    to: PerlValue,
4008    step: i64,
4009) -> Vec<PerlValue> {
4010    if step == 0 {
4011        return vec![];
4012    }
4013    let mut cur = from.into_string();
4014    let right = to.into_string();
4015
4016    if step > 0 {
4017        // Forward iteration
4018        let step = step as usize;
4019        let right_ascii = right.is_ascii();
4020        let max_bound = perl_list_range_max_bound(&right);
4021        let mut out = Vec::new();
4022        let mut guard = 0usize;
4023        let mut idx = 0usize;
4024        loop {
4025            guard += 1;
4026            if guard > 50_000_000 {
4027                break;
4028            }
4029            let cur_bound = perl_list_range_cur_bound(&cur, right_ascii);
4030            if cur_bound > max_bound {
4031                break;
4032            }
4033            if idx.is_multiple_of(step) {
4034                out.push(PerlValue::string(cur.clone()));
4035            }
4036            if cur == right {
4037                break;
4038            }
4039            match perl_magic_string_increment_for_range(&mut cur) {
4040                PerlListRangeIncOutcome::Continue => {}
4041                PerlListRangeIncOutcome::BecameNumeric => break,
4042            }
4043            idx += 1;
4044        }
4045        out
4046    } else {
4047        // Reverse iteration (stryke extension)
4048        let step = (-step) as usize;
4049        let mut out = Vec::new();
4050        let mut guard = 0usize;
4051        let mut idx = 0usize;
4052        loop {
4053            guard += 1;
4054            if guard > 50_000_000 {
4055                break;
4056            }
4057            if idx.is_multiple_of(step) {
4058                out.push(PerlValue::string(cur.clone()));
4059            }
4060            if cur == right {
4061                break;
4062            }
4063            // Check if we've gone past the target (cur < right lexicographically)
4064            if cur < right {
4065                break;
4066            }
4067            match perl_magic_string_decrement_for_range(&mut cur) {
4068                Some(()) => {}
4069                None => break, // Hit floor
4070            }
4071            idx += 1;
4072        }
4073        out
4074    }
4075}
4076
4077impl PerlDataFrame {
4078    /// One row as a hashref (`$_` in `filter`).
4079    pub fn row_hashref(&self, row: usize) -> PerlValue {
4080        let mut m = IndexMap::new();
4081        for (i, col) in self.columns.iter().enumerate() {
4082            m.insert(
4083                col.clone(),
4084                self.cols[i].get(row).cloned().unwrap_or(PerlValue::UNDEF),
4085            );
4086        }
4087        PerlValue::hash_ref(Arc::new(RwLock::new(m)))
4088    }
4089}
4090
4091#[cfg(test)]
4092mod tests {
4093    use super::PerlValue;
4094    use crate::perl_regex::PerlCompiledRegex;
4095    use indexmap::IndexMap;
4096    use parking_lot::RwLock;
4097    use std::cmp::Ordering;
4098    use std::sync::Arc;
4099
4100    #[test]
4101    fn undef_is_false() {
4102        assert!(!PerlValue::UNDEF.is_true());
4103    }
4104
4105    #[test]
4106    fn string_zero_is_false() {
4107        assert!(!PerlValue::string("0".into()).is_true());
4108        assert!(PerlValue::string("00".into()).is_true());
4109    }
4110
4111    #[test]
4112    fn empty_string_is_false() {
4113        assert!(!PerlValue::string(String::new()).is_true());
4114    }
4115
4116    #[test]
4117    fn integer_zero_is_false_nonzero_true() {
4118        assert!(!PerlValue::integer(0).is_true());
4119        assert!(PerlValue::integer(-1).is_true());
4120    }
4121
4122    #[test]
4123    fn float_zero_is_false_nonzero_true() {
4124        assert!(!PerlValue::float(0.0).is_true());
4125        assert!(PerlValue::float(0.1).is_true());
4126    }
4127
4128    #[test]
4129    fn num_cmp_orders_float_against_integer() {
4130        assert_eq!(
4131            PerlValue::float(2.5).num_cmp(&PerlValue::integer(3)),
4132            Ordering::Less
4133        );
4134    }
4135
4136    #[test]
4137    fn to_int_parses_leading_number_from_string() {
4138        assert_eq!(PerlValue::string("42xyz".into()).to_int(), 42);
4139        assert_eq!(PerlValue::string("  -3.7foo".into()).to_int(), -3);
4140    }
4141
4142    #[test]
4143    fn num_cmp_orders_as_numeric() {
4144        assert_eq!(
4145            PerlValue::integer(2).num_cmp(&PerlValue::integer(11)),
4146            Ordering::Less
4147        );
4148        assert_eq!(
4149            PerlValue::string("2foo".into()).num_cmp(&PerlValue::string("11".into())),
4150            Ordering::Less
4151        );
4152    }
4153
4154    #[test]
4155    fn str_cmp_orders_as_strings() {
4156        assert_eq!(
4157            PerlValue::string("2".into()).str_cmp(&PerlValue::string("11".into())),
4158            Ordering::Greater
4159        );
4160    }
4161
4162    #[test]
4163    fn str_eq_heap_strings_fast_path() {
4164        let a = PerlValue::string("hello".into());
4165        let b = PerlValue::string("hello".into());
4166        assert!(a.str_eq(&b));
4167        assert!(!a.str_eq(&PerlValue::string("hell".into())));
4168    }
4169
4170    #[test]
4171    fn str_eq_fallback_matches_stringified_equality() {
4172        let n = PerlValue::integer(42);
4173        let s = PerlValue::string("42".into());
4174        assert!(n.str_eq(&s));
4175        assert!(!PerlValue::integer(1).str_eq(&PerlValue::string("2".into())));
4176    }
4177
4178    #[test]
4179    fn str_cmp_heap_strings_fast_path() {
4180        assert_eq!(
4181            PerlValue::string("a".into()).str_cmp(&PerlValue::string("b".into())),
4182            Ordering::Less
4183        );
4184    }
4185
4186    #[test]
4187    fn scalar_context_array_and_hash() {
4188        let a =
4189            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).scalar_context();
4190        assert_eq!(a.to_int(), 2);
4191        let mut h = IndexMap::new();
4192        h.insert("a".into(), PerlValue::integer(1));
4193        let sc = PerlValue::hash(h).scalar_context();
4194        assert!(sc.is_string_like());
4195    }
4196
4197    #[test]
4198    fn to_list_array_hash_and_scalar() {
4199        assert_eq!(
4200            PerlValue::array(vec![PerlValue::integer(7)])
4201                .to_list()
4202                .len(),
4203            1
4204        );
4205        let mut h = IndexMap::new();
4206        h.insert("k".into(), PerlValue::integer(1));
4207        let list = PerlValue::hash(h).to_list();
4208        assert_eq!(list.len(), 2);
4209        let one = PerlValue::integer(99).to_list();
4210        assert_eq!(one.len(), 1);
4211        assert_eq!(one[0].to_int(), 99);
4212    }
4213
4214    #[test]
4215    fn type_name_and_ref_type_for_core_kinds() {
4216        assert_eq!(PerlValue::integer(0).type_name(), "INTEGER");
4217        assert_eq!(PerlValue::UNDEF.ref_type().to_string(), "");
4218        assert_eq!(
4219            PerlValue::array_ref(Arc::new(RwLock::new(vec![])))
4220                .ref_type()
4221                .to_string(),
4222            "ARRAY"
4223        );
4224    }
4225
4226    #[test]
4227    fn display_undef_is_empty_integer_is_decimal() {
4228        assert_eq!(PerlValue::UNDEF.to_string(), "");
4229        assert_eq!(PerlValue::integer(-7).to_string(), "-7");
4230    }
4231
4232    #[test]
4233    fn empty_array_is_false_nonempty_is_true() {
4234        assert!(!PerlValue::array(vec![]).is_true());
4235        assert!(PerlValue::array(vec![PerlValue::integer(0)]).is_true());
4236    }
4237
4238    #[test]
4239    fn to_number_undef_and_non_numeric_refs_are_zero() {
4240        use super::PerlSub;
4241
4242        assert_eq!(PerlValue::UNDEF.to_number(), 0.0);
4243        assert_eq!(
4244            PerlValue::code_ref(Arc::new(PerlSub {
4245                name: "f".into(),
4246                params: vec![],
4247                body: vec![],
4248                closure_env: None,
4249                prototype: None,
4250                fib_like: None,
4251            }))
4252            .to_number(),
4253            0.0
4254        );
4255    }
4256
4257    #[test]
4258    fn append_to_builds_string_without_extra_alloc_for_int_and_string() {
4259        let mut buf = String::new();
4260        PerlValue::integer(-12).append_to(&mut buf);
4261        PerlValue::string("ab".into()).append_to(&mut buf);
4262        assert_eq!(buf, "-12ab");
4263        let mut u = String::new();
4264        PerlValue::UNDEF.append_to(&mut u);
4265        assert!(u.is_empty());
4266    }
4267
4268    #[test]
4269    fn append_to_atomic_delegates_to_inner() {
4270        use parking_lot::Mutex;
4271        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::string("z".into()))));
4272        let mut buf = String::new();
4273        a.append_to(&mut buf);
4274        assert_eq!(buf, "z");
4275    }
4276
4277    #[test]
4278    fn unwrap_atomic_reads_inner_other_variants_clone() {
4279        use parking_lot::Mutex;
4280        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(9))));
4281        assert_eq!(a.unwrap_atomic().to_int(), 9);
4282        assert_eq!(PerlValue::integer(3).unwrap_atomic().to_int(), 3);
4283    }
4284
4285    #[test]
4286    fn is_atomic_only_true_for_atomic_variant() {
4287        use parking_lot::Mutex;
4288        assert!(PerlValue::atomic(Arc::new(Mutex::new(PerlValue::UNDEF))).is_atomic());
4289        assert!(!PerlValue::integer(0).is_atomic());
4290    }
4291
4292    #[test]
4293    fn as_str_only_on_string_variant() {
4294        assert_eq!(
4295            PerlValue::string("x".into()).as_str(),
4296            Some("x".to_string())
4297        );
4298        assert_eq!(PerlValue::integer(1).as_str(), None);
4299    }
4300
4301    #[test]
4302    fn as_str_or_empty_defaults_non_string() {
4303        assert_eq!(PerlValue::string("z".into()).as_str_or_empty(), "z");
4304        assert_eq!(PerlValue::integer(1).as_str_or_empty(), "");
4305    }
4306
4307    #[test]
4308    fn to_int_truncates_float_toward_zero() {
4309        assert_eq!(PerlValue::float(3.9).to_int(), 3);
4310        assert_eq!(PerlValue::float(-2.1).to_int(), -2);
4311    }
4312
4313    #[test]
4314    fn to_number_array_is_length() {
4315        assert_eq!(
4316            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).to_number(),
4317            2.0
4318        );
4319    }
4320
4321    #[test]
4322    fn scalar_context_empty_hash_is_zero() {
4323        let h = IndexMap::new();
4324        assert_eq!(PerlValue::hash(h).scalar_context().to_int(), 0);
4325    }
4326
4327    #[test]
4328    fn scalar_context_nonhash_nonarray_clones() {
4329        let v = PerlValue::integer(8);
4330        assert_eq!(v.scalar_context().to_int(), 8);
4331    }
4332
4333    #[test]
4334    fn display_float_integer_like_omits_decimal() {
4335        assert_eq!(PerlValue::float(4.0).to_string(), "4");
4336    }
4337
4338    #[test]
4339    fn display_array_concatenates_element_displays() {
4340        let a = PerlValue::array(vec![PerlValue::integer(1), PerlValue::string("b".into())]);
4341        assert_eq!(a.to_string(), "1b");
4342    }
4343
4344    #[test]
4345    fn display_code_ref_includes_sub_name() {
4346        use super::PerlSub;
4347        let c = PerlValue::code_ref(Arc::new(PerlSub {
4348            name: "foo".into(),
4349            params: vec![],
4350            body: vec![],
4351            closure_env: None,
4352            prototype: None,
4353            fib_like: None,
4354        }));
4355        assert!(c.to_string().contains("foo"));
4356    }
4357
4358    #[test]
4359    fn display_regex_shows_non_capturing_prefix() {
4360        let r = PerlValue::regex(
4361            PerlCompiledRegex::compile("x+").unwrap(),
4362            "x+".into(),
4363            "".into(),
4364        );
4365        assert_eq!(r.to_string(), "(?:x+)");
4366    }
4367
4368    #[test]
4369    fn display_iohandle_is_name() {
4370        assert_eq!(PerlValue::io_handle("STDOUT".into()).to_string(), "STDOUT");
4371    }
4372
4373    #[test]
4374    fn ref_type_blessed_uses_class_name() {
4375        let b = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
4376            "Pkg".into(),
4377            PerlValue::UNDEF,
4378        )));
4379        assert_eq!(b.ref_type().to_string(), "Pkg");
4380    }
4381
4382    #[test]
4383    fn blessed_drop_enqueues_pending_destroy() {
4384        let v = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
4385            "Z".into(),
4386            PerlValue::integer(7),
4387        )));
4388        drop(v);
4389        let q = crate::pending_destroy::take_queue();
4390        assert_eq!(q.len(), 1);
4391        assert_eq!(q[0].0, "Z");
4392        assert_eq!(q[0].1.to_int(), 7);
4393    }
4394
4395    #[test]
4396    fn type_name_iohandle_is_glob() {
4397        assert_eq!(PerlValue::io_handle("FH".into()).type_name(), "GLOB");
4398    }
4399
4400    #[test]
4401    fn empty_hash_is_false() {
4402        assert!(!PerlValue::hash(IndexMap::new()).is_true());
4403    }
4404
4405    #[test]
4406    fn hash_nonempty_is_true() {
4407        let mut h = IndexMap::new();
4408        h.insert("k".into(), PerlValue::UNDEF);
4409        assert!(PerlValue::hash(h).is_true());
4410    }
4411
4412    #[test]
4413    fn num_cmp_equal_integers() {
4414        assert_eq!(
4415            PerlValue::integer(5).num_cmp(&PerlValue::integer(5)),
4416            Ordering::Equal
4417        );
4418    }
4419
4420    #[test]
4421    fn str_cmp_compares_lexicographic_string_forms() {
4422        // Display forms "2" and "10" — string order differs from numeric order.
4423        assert_eq!(
4424            PerlValue::integer(2).str_cmp(&PerlValue::integer(10)),
4425            Ordering::Greater
4426        );
4427    }
4428
4429    #[test]
4430    fn to_list_undef_empty() {
4431        assert!(PerlValue::UNDEF.to_list().is_empty());
4432    }
4433
4434    #[test]
4435    fn unwrap_atomic_nested_atomic() {
4436        use parking_lot::Mutex;
4437        let inner = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(2))));
4438        let outer = PerlValue::atomic(Arc::new(Mutex::new(inner)));
4439        assert_eq!(outer.unwrap_atomic().to_int(), 2);
4440    }
4441
4442    #[test]
4443    fn errno_dual_parts_extracts_code_and_message() {
4444        let v = PerlValue::errno_dual(-2, "oops".into());
4445        assert_eq!(v.errno_dual_parts(), Some((-2, "oops".into())));
4446    }
4447
4448    #[test]
4449    fn errno_dual_parts_none_for_plain_string() {
4450        assert!(PerlValue::string("hi".into()).errno_dual_parts().is_none());
4451    }
4452
4453    #[test]
4454    fn errno_dual_parts_none_for_integer() {
4455        assert!(PerlValue::integer(1).errno_dual_parts().is_none());
4456    }
4457
4458    #[test]
4459    fn errno_dual_numeric_context_uses_code_string_uses_msg() {
4460        let v = PerlValue::errno_dual(5, "five".into());
4461        assert_eq!(v.to_int(), 5);
4462        assert_eq!(v.to_string(), "five");
4463    }
4464
4465    #[test]
4466    fn list_range_alpha_joins_like_perl() {
4467        use super::perl_list_range_expand;
4468        let v =
4469            perl_list_range_expand(PerlValue::string("a".into()), PerlValue::string("z".into()));
4470        let s: String = v.iter().map(|x| x.to_string()).collect();
4471        assert_eq!(s, "abcdefghijklmnopqrstuvwxyz");
4472    }
4473
4474    #[test]
4475    fn list_range_numeric_string_endpoints() {
4476        use super::perl_list_range_expand;
4477        let v = perl_list_range_expand(
4478            PerlValue::string("9".into()),
4479            PerlValue::string("11".into()),
4480        );
4481        assert_eq!(v.len(), 3);
4482        assert_eq!(
4483            v.iter().map(|x| x.to_int()).collect::<Vec<_>>(),
4484            vec![9, 10, 11]
4485        );
4486    }
4487
4488    #[test]
4489    fn list_range_leading_zero_is_string_mode() {
4490        use super::perl_list_range_expand;
4491        let v = perl_list_range_expand(
4492            PerlValue::string("01".into()),
4493            PerlValue::string("05".into()),
4494        );
4495        assert_eq!(v.len(), 5);
4496        assert_eq!(
4497            v.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
4498            vec!["01", "02", "03", "04", "05"]
4499        );
4500    }
4501
4502    #[test]
4503    fn list_range_empty_to_letter_one_element() {
4504        use super::perl_list_range_expand;
4505        let v = perl_list_range_expand(
4506            PerlValue::string(String::new()),
4507            PerlValue::string("c".into()),
4508        );
4509        assert_eq!(v.len(), 1);
4510        assert_eq!(v[0].to_string(), "");
4511    }
4512
4513    #[test]
4514    fn magic_string_inc_z_wraps_aa() {
4515        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
4516        let mut s = "z".to_string();
4517        assert_eq!(
4518            perl_magic_string_increment_for_range(&mut s),
4519            PerlListRangeIncOutcome::Continue
4520        );
4521        assert_eq!(s, "aa");
4522    }
4523
4524    #[test]
4525    fn test_boxed_numeric_stringification() {
4526        // Large integer outside i32 range
4527        let large_int = 10_000_000_000i64;
4528        let v_int = PerlValue::integer(large_int);
4529        assert_eq!(v_int.to_string(), "10000000000");
4530
4531        // Float that needs boxing (e.g. Infinity); Perl prints "Inf".
4532        let v_inf = PerlValue::float(f64::INFINITY);
4533        assert_eq!(v_inf.to_string(), "Inf");
4534    }
4535
4536    #[test]
4537    fn magic_string_inc_nine_to_ten() {
4538        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
4539        let mut s = "9".to_string();
4540        assert_eq!(
4541            perl_magic_string_increment_for_range(&mut s),
4542            PerlListRangeIncOutcome::Continue
4543        );
4544        assert_eq!(s, "10");
4545    }
4546}