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() { "-Inf".to_string() } else { "Inf".to_string() };
2753    }
2754    if f.fract() == 0.0 && f.abs() < 1e16 {
2755        format!("{}", f as i64)
2756    } else {
2757        // Perl uses Gconvert which is sprintf("%.15g", f) on most platforms.
2758        let mut buf = [0u8; 64];
2759        unsafe {
2760            libc::snprintf(
2761                buf.as_mut_ptr() as *mut libc::c_char,
2762                buf.len(),
2763                c"%.15g".as_ptr(),
2764                f,
2765            );
2766            std::ffi::CStr::from_ptr(buf.as_ptr() as *const libc::c_char)
2767                .to_string_lossy()
2768                .into_owned()
2769        }
2770    }
2771}
2772
2773/// Result of one magical string increment step in a list-context `..` range (Perl `sv_inc`).
2774#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2775pub(crate) enum PerlListRangeIncOutcome {
2776    Continue,
2777    /// Perl upgraded the scalar to a numeric form (`SvNIOKp`); list range stops after this step.
2778    BecameNumeric,
2779}
2780
2781/// Perl `looks_like_number` / `grok_number` subset: `s` must be **entirely** a numeric string
2782/// (after trim), with no trailing garbage. Used for `RANGE_IS_NUMERIC` in `pp_flop`.
2783fn perl_str_looks_like_number_for_range(s: &str) -> bool {
2784    let t = s.trim();
2785    if t.is_empty() {
2786        return s.is_empty();
2787    }
2788    let b = t.as_bytes();
2789    let mut i = 0usize;
2790    if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2791        i += 1;
2792    }
2793    if i >= b.len() {
2794        return false;
2795    }
2796    let mut saw_digit = false;
2797    while i < b.len() && b[i].is_ascii_digit() {
2798        saw_digit = true;
2799        i += 1;
2800    }
2801    if i < b.len() && b[i] == b'.' {
2802        i += 1;
2803        while i < b.len() && b[i].is_ascii_digit() {
2804            saw_digit = true;
2805            i += 1;
2806        }
2807    }
2808    if !saw_digit {
2809        return false;
2810    }
2811    if i < b.len() && (b[i] == b'e' || b[i] == b'E') {
2812        i += 1;
2813        if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
2814            i += 1;
2815        }
2816        let exp0 = i;
2817        while i < b.len() && b[i].is_ascii_digit() {
2818            i += 1;
2819        }
2820        if i == exp0 {
2821            return false;
2822        }
2823    }
2824    i == b.len()
2825}
2826
2827/// Whether list-context `..` uses Perl's **numeric** counting (`pp_flop` `RANGE_IS_NUMERIC`).
2828pub(crate) fn perl_list_range_pair_is_numeric(left: &PerlValue, right: &PerlValue) -> bool {
2829    if left.is_integer_like() || left.is_float_like() {
2830        return true;
2831    }
2832    if !left.is_undef() && !left.is_string_like() {
2833        return true;
2834    }
2835    if right.is_integer_like() || right.is_float_like() {
2836        return true;
2837    }
2838    if !right.is_undef() && !right.is_string_like() {
2839        return true;
2840    }
2841
2842    let left_ok = !left.is_undef();
2843    let right_ok = !right.is_undef();
2844    let left_pok = left.is_string_like();
2845    let left_pv = left.as_str_or_empty();
2846    let right_pv = right.as_str_or_empty();
2847
2848    let left_n = perl_str_looks_like_number_for_range(&left_pv);
2849    let right_n = perl_str_looks_like_number_for_range(&right_pv);
2850
2851    let left_zero_prefix =
2852        left_pok && left_pv.len() > 1 && left_pv.as_bytes().first() == Some(&b'0');
2853
2854    let clause5_left =
2855        (!left_ok && right_ok) || ((!left_ok || left_n) && left_pok && !left_zero_prefix);
2856    clause5_left && (!right_ok || right_n)
2857}
2858
2859/// Magical string `++` for ASCII letter/digit runs (Perl `sv_inc_nomg`, non-EBCDIC).
2860pub(crate) fn perl_magic_string_increment_for_range(s: &mut String) -> PerlListRangeIncOutcome {
2861    if s.is_empty() {
2862        return PerlListRangeIncOutcome::BecameNumeric;
2863    }
2864    let b = s.as_bytes();
2865    let mut i = 0usize;
2866    while i < b.len() && b[i].is_ascii_alphabetic() {
2867        i += 1;
2868    }
2869    while i < b.len() && b[i].is_ascii_digit() {
2870        i += 1;
2871    }
2872    if i < b.len() {
2873        let n = parse_number(s) + 1.0;
2874        *s = format_float(n);
2875        return PerlListRangeIncOutcome::BecameNumeric;
2876    }
2877
2878    let bytes = unsafe { s.as_mut_vec() };
2879    let mut idx = bytes.len() - 1;
2880    loop {
2881        if bytes[idx].is_ascii_digit() {
2882            bytes[idx] += 1;
2883            if bytes[idx] <= b'9' {
2884                return PerlListRangeIncOutcome::Continue;
2885            }
2886            bytes[idx] = b'0';
2887            if idx == 0 {
2888                bytes.insert(0, b'1');
2889                return PerlListRangeIncOutcome::Continue;
2890            }
2891            idx -= 1;
2892        } else {
2893            bytes[idx] = bytes[idx].wrapping_add(1);
2894            if bytes[idx].is_ascii_alphabetic() {
2895                return PerlListRangeIncOutcome::Continue;
2896            }
2897            bytes[idx] = bytes[idx].wrapping_sub(b'z' - b'a' + 1);
2898            if idx == 0 {
2899                let c = bytes[0];
2900                bytes.insert(0, if c.is_ascii_digit() { b'1' } else { c });
2901                return PerlListRangeIncOutcome::Continue;
2902            }
2903            idx -= 1;
2904        }
2905    }
2906}
2907
2908/// Magical string `--` for ASCII letter/digit runs (stryke extension — Perl doesn't have this).
2909/// Returns `None` if we've hit the floor (e.g., "a" can't decrement, "aa" → "z").
2910pub(crate) fn perl_magic_string_decrement_for_range(s: &mut String) -> Option<()> {
2911    if s.is_empty() {
2912        return None;
2913    }
2914    // Validate: must be all alpha then all digit (like increment)
2915    let b = s.as_bytes();
2916    let mut i = 0usize;
2917    while i < b.len() && b[i].is_ascii_alphabetic() {
2918        i += 1;
2919    }
2920    while i < b.len() && b[i].is_ascii_digit() {
2921        i += 1;
2922    }
2923    if i < b.len() {
2924        return None; // Not a pure alpha/digit string
2925    }
2926
2927    let bytes = unsafe { s.as_mut_vec() };
2928    let mut idx = bytes.len() - 1;
2929    loop {
2930        if bytes[idx].is_ascii_digit() {
2931            if bytes[idx] > b'0' {
2932                bytes[idx] -= 1;
2933                return Some(());
2934            }
2935            // Borrow: '0' becomes '9', continue to next position
2936            bytes[idx] = b'9';
2937            if idx == 0 {
2938                // "0" → can't go lower, or "00" → "9" (shrink)
2939                if bytes.len() == 1 {
2940                    bytes[0] = b'0'; // restore, signal floor
2941                    return None;
2942                }
2943                bytes.remove(0);
2944                return Some(());
2945            }
2946            idx -= 1;
2947        } else if bytes[idx].is_ascii_lowercase() {
2948            if bytes[idx] > b'a' {
2949                bytes[idx] -= 1;
2950                return Some(());
2951            }
2952            // Borrow: 'a' becomes 'z', continue to next position
2953            bytes[idx] = b'z';
2954            if idx == 0 {
2955                // "a" can't decrement, "aa" → "z"
2956                if bytes.len() == 1 {
2957                    bytes[0] = b'a'; // restore
2958                    return None;
2959                }
2960                bytes.remove(0);
2961                return Some(());
2962            }
2963            idx -= 1;
2964        } else if bytes[idx].is_ascii_uppercase() {
2965            if bytes[idx] > b'A' {
2966                bytes[idx] -= 1;
2967                return Some(());
2968            }
2969            // Borrow: 'A' becomes 'Z', continue to next position
2970            bytes[idx] = b'Z';
2971            if idx == 0 {
2972                if bytes.len() == 1 {
2973                    bytes[0] = b'A'; // restore
2974                    return None;
2975                }
2976                bytes.remove(0);
2977                return Some(());
2978            }
2979            idx -= 1;
2980        } else {
2981            return None;
2982        }
2983    }
2984}
2985
2986fn perl_list_range_max_bound(right: &str) -> usize {
2987    if right.is_ascii() {
2988        right.len()
2989    } else {
2990        right.chars().count()
2991    }
2992}
2993
2994fn perl_list_range_cur_bound(cur: &str, right_is_ascii: bool) -> usize {
2995    if right_is_ascii {
2996        cur.len()
2997    } else {
2998        cur.chars().count()
2999    }
3000}
3001
3002fn perl_list_range_expand_string_magic(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
3003    let mut cur = from.into_string();
3004    let right = to.into_string();
3005    let right_ascii = right.is_ascii();
3006    let max_bound = perl_list_range_max_bound(&right);
3007    let mut out = Vec::new();
3008    let mut guard = 0usize;
3009    loop {
3010        guard += 1;
3011        if guard > 50_000_000 {
3012            break;
3013        }
3014        let cur_bound = perl_list_range_cur_bound(&cur, right_ascii);
3015        if cur_bound > max_bound {
3016            break;
3017        }
3018        out.push(PerlValue::string(cur.clone()));
3019        if cur == right {
3020            break;
3021        }
3022        match perl_magic_string_increment_for_range(&mut cur) {
3023            PerlListRangeIncOutcome::Continue => {}
3024            PerlListRangeIncOutcome::BecameNumeric => break,
3025        }
3026    }
3027    out
3028}
3029
3030/// Perl list-context `..` (`pp_flop`): numeric counting or magical string sequence.
3031pub(crate) fn perl_list_range_expand(from: PerlValue, to: PerlValue) -> Vec<PerlValue> {
3032    if perl_list_range_pair_is_numeric(&from, &to) {
3033        let i = from.to_int();
3034        let j = to.to_int();
3035        if j >= i {
3036            (i..=j).map(PerlValue::integer).collect()
3037        } else {
3038            Vec::new()
3039        }
3040    } else {
3041        perl_list_range_expand_string_magic(from, to)
3042    }
3043}
3044
3045// ═══════════════════════════════════════════════════════════════════════════════
3046// Polymorphic range types — stryke extension (world first!)
3047// ═══════════════════════════════════════════════════════════════════════════════
3048
3049/// Check if string is a valid Roman numeral.
3050fn is_roman_numeral(s: &str) -> bool {
3051    if s.is_empty() {
3052        return false;
3053    }
3054    let upper = s.to_ascii_uppercase();
3055    upper
3056        .chars()
3057        .all(|c| matches!(c, 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M'))
3058}
3059
3060/// Check if string is an IPv4 address.
3061fn is_ipv4(s: &str) -> bool {
3062    let parts: Vec<&str> = s.split('.').collect();
3063    parts.len() == 4 && parts.iter().all(|p| p.parse::<u8>().is_ok())
3064}
3065
3066/// Parse IPv4 to u32.
3067fn ipv4_to_u32(s: &str) -> Option<u32> {
3068    let parts: Vec<u8> = s.split('.').filter_map(|p| p.parse().ok()).collect();
3069    if parts.len() != 4 {
3070        return None;
3071    }
3072    Some(
3073        ((parts[0] as u32) << 24)
3074            | ((parts[1] as u32) << 16)
3075            | ((parts[2] as u32) << 8)
3076            | (parts[3] as u32),
3077    )
3078}
3079
3080/// Convert u32 to IPv4 string.
3081fn u32_to_ipv4(n: u32) -> String {
3082    format!(
3083        "{}.{}.{}.{}",
3084        (n >> 24) & 0xFF,
3085        (n >> 16) & 0xFF,
3086        (n >> 8) & 0xFF,
3087        n & 0xFF
3088    )
3089}
3090
3091/// IPv4 range with step.
3092fn ipv4_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3093    let Some(start) = ipv4_to_u32(from) else {
3094        return vec![];
3095    };
3096    let Some(end) = ipv4_to_u32(to) else {
3097        return vec![];
3098    };
3099    let mut out = Vec::new();
3100    if step > 0 {
3101        let mut cur = start as i64;
3102        while cur <= end as i64 {
3103            out.push(PerlValue::string(u32_to_ipv4(cur as u32)));
3104            cur += step;
3105        }
3106    } else {
3107        let mut cur = start as i64;
3108        while cur >= end as i64 {
3109            out.push(PerlValue::string(u32_to_ipv4(cur as u32)));
3110            cur += step;
3111        }
3112    }
3113    out
3114}
3115
3116/// Check if string is a valid IPv6 address. Uses Rust's parser so all
3117/// compressed (`::`), full (8-group), and IPv4-mapped forms are accepted.
3118fn is_ipv6(s: &str) -> bool {
3119    s.parse::<std::net::Ipv6Addr>().is_ok()
3120}
3121
3122/// Check if string is a `0x…` / `0X…` hex literal in source-form. Used by
3123/// the range op to keep `0x00:0xFF:1` iterating as hex strings instead of
3124/// decimal. Returns true only when the prefix is present and the body is
3125/// non-empty hex digits.
3126fn is_hex_source_literal(s: &str) -> bool {
3127    let bytes = s.as_bytes();
3128    bytes.len() > 2
3129        && bytes[0] == b'0'
3130        && (bytes[1] == b'x' || bytes[1] == b'X')
3131        && bytes[2..].iter().all(|b| b.is_ascii_hexdigit())
3132}
3133
3134/// Iterate a hex range with step. Output values preserve:
3135/// - The `0x` / `0X` prefix from the FROM endpoint.
3136/// - The minimum digit width to fit either endpoint (zero-padded to that).
3137/// - Uppercase iff EITHER endpoint had any uppercase letter — once the user
3138///   types `0xFF` we keep the case for every value in the range, even when
3139///   the FROM endpoint (`0x00`) had no letters of its own to disambiguate.
3140fn hex_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3141    let from_body = &from[2..];
3142    let to_body = &to[2..];
3143    let Ok(start) = i64::from_str_radix(from_body, 16) else {
3144        return vec![];
3145    };
3146    let Ok(end) = i64::from_str_radix(to_body, 16) else {
3147        return vec![];
3148    };
3149    let prefix = &from[..2];
3150    let width = from_body.len().max(to_body.len());
3151    let upper = from_body.bytes().any(|b| b.is_ascii_uppercase())
3152        || to_body.bytes().any(|b| b.is_ascii_uppercase());
3153    let mut out = Vec::new();
3154    let format_one = |n: i64, width: usize, upper: bool, prefix: &str| -> String {
3155        if upper {
3156            format!("{}{:0>w$X}", prefix, n, w = width)
3157        } else {
3158            format!("{}{:0>w$x}", prefix, n, w = width)
3159        }
3160    };
3161    if step > 0 {
3162        if start > end {
3163            return out;
3164        }
3165        let mut cur = start;
3166        while cur <= end {
3167            out.push(PerlValue::string(format_one(cur, width, upper, prefix)));
3168            if (end - cur) < step {
3169                break;
3170            }
3171            cur += step;
3172        }
3173    } else if step < 0 {
3174        if start < end {
3175            return out;
3176        }
3177        let mut cur = start;
3178        while cur >= end {
3179            out.push(PerlValue::string(format_one(cur, width, upper, prefix)));
3180            if (cur - end) < (-step) {
3181                break;
3182            }
3183            cur += step;
3184        }
3185    }
3186    out
3187}
3188
3189fn ipv6_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3190    let Ok(start) = from.parse::<std::net::Ipv6Addr>() else {
3191        return vec![];
3192    };
3193    let Ok(end) = to.parse::<std::net::Ipv6Addr>() else {
3194        return vec![];
3195    };
3196    let s = u128::from(start);
3197    let e = u128::from(end);
3198    let mut out = Vec::new();
3199    if step > 0 {
3200        if s > e {
3201            return out; // start past end with positive step → empty
3202        }
3203        let step = step as u128;
3204        let mut cur = s;
3205        loop {
3206            out.push(PerlValue::string(std::net::Ipv6Addr::from(cur).to_string()));
3207            if cur == e || e.saturating_sub(cur) < step {
3208                break;
3209            }
3210            cur += step;
3211        }
3212    } else if step < 0 {
3213        if s < e {
3214            return out; // start before end with negative step → empty
3215        }
3216        let step = (-step) as u128;
3217        let mut cur = s;
3218        loop {
3219            out.push(PerlValue::string(std::net::Ipv6Addr::from(cur).to_string()));
3220            if cur == e || cur.saturating_sub(e) < step {
3221                break;
3222            }
3223            cur -= step;
3224        }
3225    }
3226    out
3227}
3228
3229/// Check if string is ISO date YYYY-MM-DD.
3230fn is_iso_date(s: &str) -> bool {
3231    if s.len() != 10 {
3232        return false;
3233    }
3234    let parts: Vec<&str> = s.split('-').collect();
3235    parts.len() == 3
3236        && parts[0].len() == 4
3237        && parts[0].parse::<u16>().is_ok()
3238        && parts[1].len() == 2
3239        && parts[1]
3240            .parse::<u8>()
3241            .map(|m| (1..=12).contains(&m))
3242            .unwrap_or(false)
3243        && parts[2].len() == 2
3244        && parts[2]
3245            .parse::<u8>()
3246            .map(|d| (1..=31).contains(&d))
3247            .unwrap_or(false)
3248}
3249
3250/// Check if string is YYYY-MM (month range).
3251fn is_year_month(s: &str) -> bool {
3252    if s.len() != 7 {
3253        return false;
3254    }
3255    let parts: Vec<&str> = s.split('-').collect();
3256    parts.len() == 2
3257        && parts[0].len() == 4
3258        && parts[0].parse::<u16>().is_ok()
3259        && parts[1].len() == 2
3260        && parts[1]
3261            .parse::<u8>()
3262            .map(|m| (1..=12).contains(&m))
3263            .unwrap_or(false)
3264}
3265
3266/// Parse ISO date to (year, month, day).
3267fn parse_iso_date(s: &str) -> Option<(i32, u32, u32)> {
3268    let parts: Vec<&str> = s.split('-').collect();
3269    if parts.len() != 3 {
3270        return None;
3271    }
3272    Some((
3273        parts[0].parse().ok()?,
3274        parts[1].parse().ok()?,
3275        parts[2].parse().ok()?,
3276    ))
3277}
3278
3279/// Parse YYYY-MM to (year, month).
3280fn parse_year_month(s: &str) -> Option<(i32, u32)> {
3281    let parts: Vec<&str> = s.split('-').collect();
3282    if parts.len() != 2 {
3283        return None;
3284    }
3285    Some((parts[0].parse().ok()?, parts[1].parse().ok()?))
3286}
3287
3288/// Days in month (handles leap years).
3289fn days_in_month(year: i32, month: u32) -> u32 {
3290    match month {
3291        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
3292        4 | 6 | 9 | 11 => 30,
3293        2 => {
3294            if (year % 4 == 0 && year % 100 != 0) || year % 400 == 0 {
3295                29
3296            } else {
3297                28
3298            }
3299        }
3300        _ => 30,
3301    }
3302}
3303
3304/// Add days to a date, returning new (year, month, day).
3305fn add_days(mut year: i32, mut month: u32, mut day: u32, mut delta: i64) -> (i32, u32, u32) {
3306    if delta > 0 {
3307        while delta > 0 {
3308            let dim = days_in_month(year, month);
3309            let remaining = dim - day;
3310            if delta <= remaining as i64 {
3311                day += delta as u32;
3312                break;
3313            }
3314            delta -= (remaining + 1) as i64;
3315            day = 1;
3316            month += 1;
3317            if month > 12 {
3318                month = 1;
3319                year += 1;
3320            }
3321        }
3322    } else {
3323        while delta < 0 {
3324            if (-delta) < day as i64 {
3325                day = (day as i64 + delta) as u32;
3326                break;
3327            }
3328            delta += day as i64;
3329            month -= 1;
3330            if month == 0 {
3331                month = 12;
3332                year -= 1;
3333            }
3334            day = days_in_month(year, month);
3335        }
3336    }
3337    (year, month, day)
3338}
3339
3340/// ISO date range with step (step = days).
3341fn iso_date_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3342    let Some((mut y, mut m, mut d)) = parse_iso_date(from) else {
3343        return vec![];
3344    };
3345    let Some((ey, em, ed)) = parse_iso_date(to) else {
3346        return vec![];
3347    };
3348    let mut out = Vec::new();
3349    let mut guard = 0;
3350    if step > 0 {
3351        while (y, m, d) <= (ey, em, ed) && guard < 50_000 {
3352            out.push(PerlValue::string(format!("{:04}-{:02}-{:02}", y, m, d)));
3353            (y, m, d) = add_days(y, m, d, step);
3354            guard += 1;
3355        }
3356    } else {
3357        while (y, m, d) >= (ey, em, ed) && guard < 50_000 {
3358            out.push(PerlValue::string(format!("{:04}-{:02}-{:02}", y, m, d)));
3359            (y, m, d) = add_days(y, m, d, step);
3360            guard += 1;
3361        }
3362    }
3363    out
3364}
3365
3366/// Add months to (year, month).
3367fn add_months(mut year: i32, mut month: u32, delta: i64) -> (i32, u32) {
3368    let total = (year as i64 * 12 + month as i64 - 1) + delta;
3369    year = (total / 12) as i32;
3370    month = ((total % 12) + 1) as u32;
3371    if month == 0 {
3372        month = 12;
3373        year -= 1;
3374    }
3375    (year, month)
3376}
3377
3378/// YYYY-MM range with step (step = months).
3379fn year_month_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3380    let Some((mut y, mut m)) = parse_year_month(from) else {
3381        return vec![];
3382    };
3383    let Some((ey, em)) = parse_year_month(to) else {
3384        return vec![];
3385    };
3386    let mut out = Vec::new();
3387    let mut guard = 0;
3388    if step > 0 {
3389        while (y, m) <= (ey, em) && guard < 50_000 {
3390            out.push(PerlValue::string(format!("{:04}-{:02}", y, m)));
3391            (y, m) = add_months(y, m, step);
3392            guard += 1;
3393        }
3394    } else {
3395        while (y, m) >= (ey, em) && guard < 50_000 {
3396            out.push(PerlValue::string(format!("{:04}-{:02}", y, m)));
3397            (y, m) = add_months(y, m, step);
3398            guard += 1;
3399        }
3400    }
3401    out
3402}
3403
3404/// Check if string looks like HH:MM time.
3405fn is_time_hhmm(s: &str) -> bool {
3406    if s.len() != 5 {
3407        return false;
3408    }
3409    let parts: Vec<&str> = s.split(':').collect();
3410    parts.len() == 2
3411        && parts[0].len() == 2
3412        && parts[0].parse::<u8>().map(|h| h < 24).unwrap_or(false)
3413        && parts[1].len() == 2
3414        && parts[1].parse::<u8>().map(|m| m < 60).unwrap_or(false)
3415}
3416
3417/// Parse HH:MM to minutes since midnight.
3418fn parse_time_hhmm(s: &str) -> Option<i32> {
3419    let parts: Vec<&str> = s.split(':').collect();
3420    if parts.len() != 2 {
3421        return None;
3422    }
3423    let h: i32 = parts[0].parse().ok()?;
3424    let m: i32 = parts[1].parse().ok()?;
3425    Some(h * 60 + m)
3426}
3427
3428/// Minutes to HH:MM string.
3429fn minutes_to_hhmm(mins: i32) -> String {
3430    let h = (mins / 60) % 24;
3431    let m = mins % 60;
3432    format!("{:02}:{:02}", h, m)
3433}
3434
3435/// HH:MM time range with step (step = minutes).
3436fn time_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3437    let Some(start) = parse_time_hhmm(from) else {
3438        return vec![];
3439    };
3440    let Some(end) = parse_time_hhmm(to) else {
3441        return vec![];
3442    };
3443    let mut out = Vec::new();
3444    let mut guard = 0;
3445    if step > 0 {
3446        let mut cur = start;
3447        while cur <= end && guard < 50_000 {
3448            out.push(PerlValue::string(minutes_to_hhmm(cur)));
3449            cur += step as i32;
3450            guard += 1;
3451        }
3452    } else {
3453        let mut cur = start;
3454        while cur >= end && guard < 50_000 {
3455            out.push(PerlValue::string(minutes_to_hhmm(cur)));
3456            cur += step as i32;
3457            guard += 1;
3458        }
3459    }
3460    out
3461}
3462
3463const WEEKDAYS: [&str; 7] = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
3464const WEEKDAYS_FULL: [&str; 7] = [
3465    "Monday",
3466    "Tuesday",
3467    "Wednesday",
3468    "Thursday",
3469    "Friday",
3470    "Saturday",
3471    "Sunday",
3472];
3473
3474/// Check if string is a weekday name.
3475fn weekday_index(s: &str) -> Option<usize> {
3476    let lower = s.to_ascii_lowercase();
3477    for (i, &d) in WEEKDAYS.iter().enumerate() {
3478        if d.to_ascii_lowercase() == lower {
3479            return Some(i);
3480        }
3481    }
3482    for (i, &d) in WEEKDAYS_FULL.iter().enumerate() {
3483        if d.to_ascii_lowercase() == lower {
3484            return Some(i);
3485        }
3486    }
3487    None
3488}
3489
3490/// Weekday range with step.
3491fn weekday_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3492    let Some(start) = weekday_index(from) else {
3493        return vec![];
3494    };
3495    let Some(end) = weekday_index(to) else {
3496        return vec![];
3497    };
3498    let full = from.len() > 3;
3499    let names = if full { &WEEKDAYS_FULL } else { &WEEKDAYS };
3500    let mut out = Vec::new();
3501    if step > 0 {
3502        let mut cur = start as i64;
3503        let target = if end >= start {
3504            end as i64
3505        } else {
3506            end as i64 + 7
3507        };
3508        while cur <= target {
3509            out.push(PerlValue::string(names[(cur % 7) as usize].to_string()));
3510            cur += step;
3511        }
3512    } else {
3513        let mut cur = start as i64;
3514        let target = if end <= start {
3515            end as i64
3516        } else {
3517            end as i64 - 7
3518        };
3519        while cur >= target {
3520            out.push(PerlValue::string(
3521                names[((cur % 7 + 7) % 7) as usize].to_string(),
3522            ));
3523            cur += step;
3524        }
3525    }
3526    out
3527}
3528
3529const MONTHS: [&str; 12] = [
3530    "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
3531];
3532const MONTHS_FULL: [&str; 12] = [
3533    "January",
3534    "February",
3535    "March",
3536    "April",
3537    "May",
3538    "June",
3539    "July",
3540    "August",
3541    "September",
3542    "October",
3543    "November",
3544    "December",
3545];
3546
3547/// Check if string is a month name.
3548fn month_name_index(s: &str) -> Option<usize> {
3549    let lower = s.to_ascii_lowercase();
3550    for (i, &m) in MONTHS.iter().enumerate() {
3551        if m.to_ascii_lowercase() == lower {
3552            return Some(i);
3553        }
3554    }
3555    for (i, &m) in MONTHS_FULL.iter().enumerate() {
3556        if m.to_ascii_lowercase() == lower {
3557            return Some(i);
3558        }
3559    }
3560    None
3561}
3562
3563/// Month name range with step.
3564fn month_name_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3565    let Some(start) = month_name_index(from) else {
3566        return vec![];
3567    };
3568    let Some(end) = month_name_index(to) else {
3569        return vec![];
3570    };
3571    let full = from.len() > 3;
3572    let names = if full { &MONTHS_FULL } else { &MONTHS };
3573    let mut out = Vec::new();
3574    if step > 0 {
3575        let mut cur = start as i64;
3576        let target = if end >= start {
3577            end as i64
3578        } else {
3579            end as i64 + 12
3580        };
3581        while cur <= target {
3582            out.push(PerlValue::string(names[(cur % 12) as usize].to_string()));
3583            cur += step;
3584        }
3585    } else {
3586        let mut cur = start as i64;
3587        let target = if end <= start {
3588            end as i64
3589        } else {
3590            end as i64 - 12
3591        };
3592        while cur >= target {
3593            out.push(PerlValue::string(
3594                names[((cur % 12 + 12) % 12) as usize].to_string(),
3595            ));
3596            cur += step;
3597        }
3598    }
3599    out
3600}
3601
3602/// Check if both operands are float-like (contain decimal point, not date/time/IP).
3603fn is_float_pair(from: &str, to: &str) -> bool {
3604    fn is_float(s: &str) -> bool {
3605        s.contains('.')
3606            && !s.contains(':')
3607            && s.matches('.').count() == 1
3608            && s.parse::<f64>().is_ok()
3609    }
3610    is_float(from) && is_float(to)
3611}
3612
3613/// Float range with step.
3614fn float_range_stepped(from: &str, to: &str, step: f64) -> Vec<PerlValue> {
3615    let Ok(start) = from.parse::<f64>() else {
3616        return vec![];
3617    };
3618    let Ok(end) = to.parse::<f64>() else {
3619        return vec![];
3620    };
3621    let mut out = Vec::new();
3622    let mut guard = 0;
3623    // Use integer counting to avoid floating point accumulation errors
3624    if step > 0.0 {
3625        let mut i = 0i64;
3626        loop {
3627            let cur = start + (i as f64) * step;
3628            if cur > end + step.abs() * f64::EPSILON * 10.0 || guard >= 50_000 {
3629                break;
3630            }
3631            // Round to avoid floating point noise
3632            let rounded = (cur * 1e12).round() / 1e12;
3633            out.push(PerlValue::float(rounded));
3634            i += 1;
3635            guard += 1;
3636        }
3637    } else if step < 0.0 {
3638        let mut i = 0i64;
3639        loop {
3640            let cur = start + (i as f64) * step;
3641            if cur < end - step.abs() * f64::EPSILON * 10.0 || guard >= 50_000 {
3642                break;
3643            }
3644            let rounded = (cur * 1e12).round() / 1e12;
3645            out.push(PerlValue::float(rounded));
3646            i += 1;
3647            guard += 1;
3648        }
3649    }
3650    out
3651}
3652
3653/// Convert Roman numeral string to integer.
3654fn roman_to_int(s: &str) -> Option<i64> {
3655    let upper = s.to_ascii_uppercase();
3656    let mut result = 0i64;
3657    let mut prev = 0i64;
3658    for c in upper.chars().rev() {
3659        let val = match c {
3660            'I' => 1,
3661            'V' => 5,
3662            'X' => 10,
3663            'L' => 50,
3664            'C' => 100,
3665            'D' => 500,
3666            'M' => 1000,
3667            _ => return None,
3668        };
3669        if val < prev {
3670            result -= val;
3671        } else {
3672            result += val;
3673        }
3674        prev = val;
3675    }
3676    if result > 0 {
3677        Some(result)
3678    } else {
3679        None
3680    }
3681}
3682
3683/// Convert integer to Roman numeral string.
3684fn int_to_roman(mut n: i64, lowercase: bool) -> Option<String> {
3685    if n <= 0 || n > 3999 {
3686        return None;
3687    }
3688    let numerals = [
3689        (1000, "M"),
3690        (900, "CM"),
3691        (500, "D"),
3692        (400, "CD"),
3693        (100, "C"),
3694        (90, "XC"),
3695        (50, "L"),
3696        (40, "XL"),
3697        (10, "X"),
3698        (9, "IX"),
3699        (5, "V"),
3700        (4, "IV"),
3701        (1, "I"),
3702    ];
3703    let mut result = String::new();
3704    for (val, sym) in numerals {
3705        while n >= val {
3706            result.push_str(sym);
3707            n -= val;
3708        }
3709    }
3710    if lowercase {
3711        Some(result.to_ascii_lowercase())
3712    } else {
3713        Some(result)
3714    }
3715}
3716
3717/// Expand a Roman numeral range with step.
3718fn roman_range_stepped(from: &str, to: &str, step: i64) -> Vec<PerlValue> {
3719    let Some(start) = roman_to_int(from) else {
3720        return vec![];
3721    };
3722    let Some(end) = roman_to_int(to) else {
3723        return vec![];
3724    };
3725    let lowercase = from
3726        .chars()
3727        .next()
3728        .map(|c| c.is_ascii_lowercase())
3729        .unwrap_or(false);
3730
3731    let mut out = Vec::new();
3732    if step > 0 {
3733        let mut cur = start;
3734        while cur <= end {
3735            if let Some(r) = int_to_roman(cur, lowercase) {
3736                out.push(PerlValue::string(r));
3737            }
3738            cur += step;
3739        }
3740    } else {
3741        let mut cur = start;
3742        while cur >= end {
3743            if let Some(r) = int_to_roman(cur, lowercase) {
3744                out.push(PerlValue::string(r));
3745            }
3746            cur += step; // step is negative
3747        }
3748    }
3749    out
3750}
3751
3752/// Stepped range expansion — polymorphic across many types (stryke world first!).
3753/// Supports: integers, floats, strings, Roman numerals, dates, times, weekdays, months, IPv4.
3754pub(crate) fn perl_list_range_expand_stepped(
3755    from: PerlValue,
3756    to: PerlValue,
3757    step_val: PerlValue,
3758) -> Vec<PerlValue> {
3759    let from_str = from.to_string();
3760    let to_str = to.to_string();
3761
3762    // Check if this is a float range (operands have decimal points)
3763    let is_float_range = is_float_pair(&from_str, &to_str);
3764
3765    // Get step as float or int depending on context
3766    let step_float = step_val.as_float().unwrap_or(step_val.to_int() as f64);
3767    let step_int = step_val.to_int();
3768
3769    if step_int == 0 && step_float == 0.0 {
3770        return vec![];
3771    }
3772
3773    // Float ranges use float step
3774    if is_float_range {
3775        return float_range_stepped(&from_str, &to_str, step_float);
3776    }
3777
3778    // Pure numeric integers
3779    if perl_list_range_pair_is_numeric(&from, &to) {
3780        let i = from.to_int();
3781        let j = to.to_int();
3782        if step_int > 0 {
3783            (i..=j)
3784                .step_by(step_int as usize)
3785                .map(PerlValue::integer)
3786                .collect()
3787        } else {
3788            std::iter::successors(Some(i), |&x| {
3789                let next = x + step_int;
3790                if next >= j {
3791                    Some(next)
3792                } else {
3793                    None
3794                }
3795            })
3796            .map(PerlValue::integer)
3797            .collect()
3798        }
3799    } else {
3800        // Check special types in order of specificity
3801
3802        // Hex literals — must check before IPv4 because `0xFF` chars include
3803        // hex digits that aren't dotted-quad anyway, but keeping ordering
3804        // tight prevents future ambiguity. Preserves `0x` prefix, width,
3805        // and case from the source form.
3806        if is_hex_source_literal(&from_str) && is_hex_source_literal(&to_str) {
3807            return hex_range_stepped(&from_str, &to_str, step_int);
3808        }
3809
3810        // IPv4 addresses (must check before floats due to dots)
3811        if is_ipv4(&from_str) && is_ipv4(&to_str) {
3812            return ipv4_range_stepped(&from_str, &to_str, step_int);
3813        }
3814
3815        // IPv6 addresses — full or `::`-compressed. Uses the dedicated `!!!`
3816        // range separator so the IPv6's own colons don't collide with the
3817        // standard `:` range op.
3818        if is_ipv6(&from_str) && is_ipv6(&to_str) {
3819            return ipv6_range_stepped(&from_str, &to_str, step_int);
3820        }
3821
3822        // ISO dates YYYY-MM-DD (step = days)
3823        if is_iso_date(&from_str) && is_iso_date(&to_str) {
3824            return iso_date_range_stepped(&from_str, &to_str, step_int);
3825        }
3826
3827        // Year-month YYYY-MM (step = months)
3828        if is_year_month(&from_str) && is_year_month(&to_str) {
3829            return year_month_range_stepped(&from_str, &to_str, step_int);
3830        }
3831
3832        // Time HH:MM (step = minutes)
3833        if is_time_hhmm(&from_str) && is_time_hhmm(&to_str) {
3834            return time_range_stepped(&from_str, &to_str, step_int);
3835        }
3836
3837        // Weekday names
3838        if weekday_index(&from_str).is_some() && weekday_index(&to_str).is_some() {
3839            return weekday_range_stepped(&from_str, &to_str, step_int);
3840        }
3841
3842        // Month names
3843        if month_name_index(&from_str).is_some() && month_name_index(&to_str).is_some() {
3844            return month_name_range_stepped(&from_str, &to_str, step_int);
3845        }
3846
3847        // Roman numerals
3848        if is_roman_numeral(&from_str) && is_roman_numeral(&to_str) {
3849            return roman_range_stepped(&from_str, &to_str, step_int);
3850        }
3851
3852        // Fall back to magic string increment/decrement
3853        perl_list_range_expand_string_magic_stepped(from, to, step_int)
3854    }
3855}
3856
3857/// Coerce a slice endpoint to a strict integer. Used by [`Op::ArraySliceRange`] —
3858/// non-numeric strings, fractional floats, refs, and other non-integer types die.
3859/// `where_` is the diagnostic context (`"start"`, `"stop"`, `"step"`).
3860pub(crate) fn perl_slice_endpoint_to_strict_int(
3861    v: &PerlValue,
3862    where_: &str,
3863) -> Result<i64, String> {
3864    if let Some(n) = v.as_integer() {
3865        return Ok(n);
3866    }
3867    if let Some(f) = v.as_float() {
3868        if f.is_finite() && f.fract() == 0.0 && f >= i64::MIN as f64 && f <= i64::MAX as f64 {
3869            return Ok(f as i64);
3870        }
3871        return Err(format!(
3872            "array slice {}: non-integer float endpoint {}",
3873            where_, f
3874        ));
3875    }
3876    let s = v.as_str_or_empty();
3877    if !s.is_empty() {
3878        if let Ok(n) = s.trim().parse::<i64>() {
3879            return Ok(n);
3880        }
3881        return Err(format!(
3882            "array slice {}: non-integer string endpoint {:?}",
3883            where_, s
3884        ));
3885    }
3886    Err(format!(
3887        "array slice {}: endpoint must be an integer (got non-numeric value)",
3888        where_
3889    ))
3890}
3891
3892/// Resolve `from`/`to`/`step` for `@arr[FROM:TO:STEP]` (and open-ended forms) into the
3893/// concrete list of array indices. Closed inclusive on both ends. `Undef` endpoints
3894/// (the omitted-endpoint sentinel emitted by the compiler) default to:
3895/// - `step` → `1`
3896/// - `from` → `0` (positive step) or `arr_len-1` (negative step)
3897/// - `to`   → `arr_len-1` (positive step) or `0` (negative step)
3898///
3899/// Negative explicit indices count from the end (Perl semantics: `-1` = last element).
3900/// Returns `Err(msg)` for non-integer endpoints or zero step — caller dies with that.
3901pub(crate) fn compute_array_slice_indices(
3902    arr_len: i64,
3903    from: &PerlValue,
3904    to: &PerlValue,
3905    step: &PerlValue,
3906) -> Result<Vec<i64>, String> {
3907    let step_i = if step.is_undef() {
3908        1i64
3909    } else {
3910        perl_slice_endpoint_to_strict_int(step, "step")?
3911    };
3912    if step_i == 0 {
3913        return Err("array slice step cannot be 0".into());
3914    }
3915
3916    let normalize = |i: i64| -> i64 {
3917        if i < 0 {
3918            i + arr_len
3919        } else {
3920            i
3921        }
3922    };
3923
3924    // Open-ended slice (`@a[..3]`, `@a[-3..]`) is a stryke extension where each
3925    // explicit endpoint wraps once from the end. Closed `Range` slices
3926    // (`@a[0..-1]`, `@a[3..-1]`, `@a[-3..-1]`) follow Perl's raw-integer range
3927    // semantics: `0..-1` is empty, `-3..-1` is `(-3, -2, -1)`, and each
3928    // generated integer wraps individually when looked up.
3929    let any_undef = from.is_undef() || to.is_undef();
3930
3931    let from_raw = if from.is_undef() {
3932        if step_i > 0 {
3933            0
3934        } else {
3935            arr_len - 1
3936        }
3937    } else {
3938        perl_slice_endpoint_to_strict_int(from, "start")?
3939    };
3940
3941    let to_raw = if to.is_undef() {
3942        if step_i > 0 {
3943            arr_len - 1
3944        } else {
3945            0
3946        }
3947    } else {
3948        perl_slice_endpoint_to_strict_int(to, "stop")?
3949    };
3950
3951    let mut out = Vec::new();
3952    if arr_len == 0 {
3953        return Ok(out);
3954    }
3955
3956    let (from_i, to_i) = if any_undef {
3957        (normalize(from_raw), normalize(to_raw))
3958    } else {
3959        (from_raw, to_raw)
3960    };
3961
3962    if step_i > 0 {
3963        let mut i = from_i;
3964        while i <= to_i {
3965            out.push(if any_undef { i } else { normalize(i) });
3966            i += step_i;
3967        }
3968    } else {
3969        let mut i = from_i;
3970        while i >= to_i {
3971            out.push(if any_undef { i } else { normalize(i) });
3972            i += step_i; // step_i is negative
3973        }
3974    }
3975    Ok(out)
3976}
3977
3978/// Resolve `from`/`to`/`step` for `@h{FROM:TO:STEP}` into the concrete list of hash keys.
3979/// Both endpoints must be present (open-ended forms are nonsense for unordered hashes
3980/// and die). Endpoints stringify to keys; expansion uses the polymorphic stepped-range
3981/// machinery (numeric, magic-string-increment, Roman, etc.).
3982pub(crate) fn compute_hash_slice_keys(
3983    from: &PerlValue,
3984    to: &PerlValue,
3985    step: &PerlValue,
3986) -> Result<Vec<String>, String> {
3987    if from.is_undef() || to.is_undef() {
3988        return Err(
3989            "hash slice range requires both endpoints (open-ended forms not allowed)".into(),
3990        );
3991    }
3992    let step_val = if step.is_undef() {
3993        PerlValue::integer(1)
3994    } else {
3995        step.clone()
3996    };
3997    let expanded = perl_list_range_expand_stepped(from.clone(), to.clone(), step_val);
3998    Ok(expanded.into_iter().map(|v| v.to_string()).collect())
3999}
4000
4001fn perl_list_range_expand_string_magic_stepped(
4002    from: PerlValue,
4003    to: PerlValue,
4004    step: i64,
4005) -> Vec<PerlValue> {
4006    if step == 0 {
4007        return vec![];
4008    }
4009    let mut cur = from.into_string();
4010    let right = to.into_string();
4011
4012    if step > 0 {
4013        // Forward iteration
4014        let step = step as usize;
4015        let right_ascii = right.is_ascii();
4016        let max_bound = perl_list_range_max_bound(&right);
4017        let mut out = Vec::new();
4018        let mut guard = 0usize;
4019        let mut idx = 0usize;
4020        loop {
4021            guard += 1;
4022            if guard > 50_000_000 {
4023                break;
4024            }
4025            let cur_bound = perl_list_range_cur_bound(&cur, right_ascii);
4026            if cur_bound > max_bound {
4027                break;
4028            }
4029            if idx.is_multiple_of(step) {
4030                out.push(PerlValue::string(cur.clone()));
4031            }
4032            if cur == right {
4033                break;
4034            }
4035            match perl_magic_string_increment_for_range(&mut cur) {
4036                PerlListRangeIncOutcome::Continue => {}
4037                PerlListRangeIncOutcome::BecameNumeric => break,
4038            }
4039            idx += 1;
4040        }
4041        out
4042    } else {
4043        // Reverse iteration (stryke extension)
4044        let step = (-step) as usize;
4045        let mut out = Vec::new();
4046        let mut guard = 0usize;
4047        let mut idx = 0usize;
4048        loop {
4049            guard += 1;
4050            if guard > 50_000_000 {
4051                break;
4052            }
4053            if idx.is_multiple_of(step) {
4054                out.push(PerlValue::string(cur.clone()));
4055            }
4056            if cur == right {
4057                break;
4058            }
4059            // Check if we've gone past the target (cur < right lexicographically)
4060            if cur < right {
4061                break;
4062            }
4063            match perl_magic_string_decrement_for_range(&mut cur) {
4064                Some(()) => {}
4065                None => break, // Hit floor
4066            }
4067            idx += 1;
4068        }
4069        out
4070    }
4071}
4072
4073impl PerlDataFrame {
4074    /// One row as a hashref (`$_` in `filter`).
4075    pub fn row_hashref(&self, row: usize) -> PerlValue {
4076        let mut m = IndexMap::new();
4077        for (i, col) in self.columns.iter().enumerate() {
4078            m.insert(
4079                col.clone(),
4080                self.cols[i].get(row).cloned().unwrap_or(PerlValue::UNDEF),
4081            );
4082        }
4083        PerlValue::hash_ref(Arc::new(RwLock::new(m)))
4084    }
4085}
4086
4087#[cfg(test)]
4088mod tests {
4089    use super::PerlValue;
4090    use crate::perl_regex::PerlCompiledRegex;
4091    use indexmap::IndexMap;
4092    use parking_lot::RwLock;
4093    use std::cmp::Ordering;
4094    use std::sync::Arc;
4095
4096    #[test]
4097    fn undef_is_false() {
4098        assert!(!PerlValue::UNDEF.is_true());
4099    }
4100
4101    #[test]
4102    fn string_zero_is_false() {
4103        assert!(!PerlValue::string("0".into()).is_true());
4104        assert!(PerlValue::string("00".into()).is_true());
4105    }
4106
4107    #[test]
4108    fn empty_string_is_false() {
4109        assert!(!PerlValue::string(String::new()).is_true());
4110    }
4111
4112    #[test]
4113    fn integer_zero_is_false_nonzero_true() {
4114        assert!(!PerlValue::integer(0).is_true());
4115        assert!(PerlValue::integer(-1).is_true());
4116    }
4117
4118    #[test]
4119    fn float_zero_is_false_nonzero_true() {
4120        assert!(!PerlValue::float(0.0).is_true());
4121        assert!(PerlValue::float(0.1).is_true());
4122    }
4123
4124    #[test]
4125    fn num_cmp_orders_float_against_integer() {
4126        assert_eq!(
4127            PerlValue::float(2.5).num_cmp(&PerlValue::integer(3)),
4128            Ordering::Less
4129        );
4130    }
4131
4132    #[test]
4133    fn to_int_parses_leading_number_from_string() {
4134        assert_eq!(PerlValue::string("42xyz".into()).to_int(), 42);
4135        assert_eq!(PerlValue::string("  -3.7foo".into()).to_int(), -3);
4136    }
4137
4138    #[test]
4139    fn num_cmp_orders_as_numeric() {
4140        assert_eq!(
4141            PerlValue::integer(2).num_cmp(&PerlValue::integer(11)),
4142            Ordering::Less
4143        );
4144        assert_eq!(
4145            PerlValue::string("2foo".into()).num_cmp(&PerlValue::string("11".into())),
4146            Ordering::Less
4147        );
4148    }
4149
4150    #[test]
4151    fn str_cmp_orders_as_strings() {
4152        assert_eq!(
4153            PerlValue::string("2".into()).str_cmp(&PerlValue::string("11".into())),
4154            Ordering::Greater
4155        );
4156    }
4157
4158    #[test]
4159    fn str_eq_heap_strings_fast_path() {
4160        let a = PerlValue::string("hello".into());
4161        let b = PerlValue::string("hello".into());
4162        assert!(a.str_eq(&b));
4163        assert!(!a.str_eq(&PerlValue::string("hell".into())));
4164    }
4165
4166    #[test]
4167    fn str_eq_fallback_matches_stringified_equality() {
4168        let n = PerlValue::integer(42);
4169        let s = PerlValue::string("42".into());
4170        assert!(n.str_eq(&s));
4171        assert!(!PerlValue::integer(1).str_eq(&PerlValue::string("2".into())));
4172    }
4173
4174    #[test]
4175    fn str_cmp_heap_strings_fast_path() {
4176        assert_eq!(
4177            PerlValue::string("a".into()).str_cmp(&PerlValue::string("b".into())),
4178            Ordering::Less
4179        );
4180    }
4181
4182    #[test]
4183    fn scalar_context_array_and_hash() {
4184        let a =
4185            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).scalar_context();
4186        assert_eq!(a.to_int(), 2);
4187        let mut h = IndexMap::new();
4188        h.insert("a".into(), PerlValue::integer(1));
4189        let sc = PerlValue::hash(h).scalar_context();
4190        assert!(sc.is_string_like());
4191    }
4192
4193    #[test]
4194    fn to_list_array_hash_and_scalar() {
4195        assert_eq!(
4196            PerlValue::array(vec![PerlValue::integer(7)])
4197                .to_list()
4198                .len(),
4199            1
4200        );
4201        let mut h = IndexMap::new();
4202        h.insert("k".into(), PerlValue::integer(1));
4203        let list = PerlValue::hash(h).to_list();
4204        assert_eq!(list.len(), 2);
4205        let one = PerlValue::integer(99).to_list();
4206        assert_eq!(one.len(), 1);
4207        assert_eq!(one[0].to_int(), 99);
4208    }
4209
4210    #[test]
4211    fn type_name_and_ref_type_for_core_kinds() {
4212        assert_eq!(PerlValue::integer(0).type_name(), "INTEGER");
4213        assert_eq!(PerlValue::UNDEF.ref_type().to_string(), "");
4214        assert_eq!(
4215            PerlValue::array_ref(Arc::new(RwLock::new(vec![])))
4216                .ref_type()
4217                .to_string(),
4218            "ARRAY"
4219        );
4220    }
4221
4222    #[test]
4223    fn display_undef_is_empty_integer_is_decimal() {
4224        assert_eq!(PerlValue::UNDEF.to_string(), "");
4225        assert_eq!(PerlValue::integer(-7).to_string(), "-7");
4226    }
4227
4228    #[test]
4229    fn empty_array_is_false_nonempty_is_true() {
4230        assert!(!PerlValue::array(vec![]).is_true());
4231        assert!(PerlValue::array(vec![PerlValue::integer(0)]).is_true());
4232    }
4233
4234    #[test]
4235    fn to_number_undef_and_non_numeric_refs_are_zero() {
4236        use super::PerlSub;
4237
4238        assert_eq!(PerlValue::UNDEF.to_number(), 0.0);
4239        assert_eq!(
4240            PerlValue::code_ref(Arc::new(PerlSub {
4241                name: "f".into(),
4242                params: vec![],
4243                body: vec![],
4244                closure_env: None,
4245                prototype: None,
4246                fib_like: None,
4247            }))
4248            .to_number(),
4249            0.0
4250        );
4251    }
4252
4253    #[test]
4254    fn append_to_builds_string_without_extra_alloc_for_int_and_string() {
4255        let mut buf = String::new();
4256        PerlValue::integer(-12).append_to(&mut buf);
4257        PerlValue::string("ab".into()).append_to(&mut buf);
4258        assert_eq!(buf, "-12ab");
4259        let mut u = String::new();
4260        PerlValue::UNDEF.append_to(&mut u);
4261        assert!(u.is_empty());
4262    }
4263
4264    #[test]
4265    fn append_to_atomic_delegates_to_inner() {
4266        use parking_lot::Mutex;
4267        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::string("z".into()))));
4268        let mut buf = String::new();
4269        a.append_to(&mut buf);
4270        assert_eq!(buf, "z");
4271    }
4272
4273    #[test]
4274    fn unwrap_atomic_reads_inner_other_variants_clone() {
4275        use parking_lot::Mutex;
4276        let a = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(9))));
4277        assert_eq!(a.unwrap_atomic().to_int(), 9);
4278        assert_eq!(PerlValue::integer(3).unwrap_atomic().to_int(), 3);
4279    }
4280
4281    #[test]
4282    fn is_atomic_only_true_for_atomic_variant() {
4283        use parking_lot::Mutex;
4284        assert!(PerlValue::atomic(Arc::new(Mutex::new(PerlValue::UNDEF))).is_atomic());
4285        assert!(!PerlValue::integer(0).is_atomic());
4286    }
4287
4288    #[test]
4289    fn as_str_only_on_string_variant() {
4290        assert_eq!(
4291            PerlValue::string("x".into()).as_str(),
4292            Some("x".to_string())
4293        );
4294        assert_eq!(PerlValue::integer(1).as_str(), None);
4295    }
4296
4297    #[test]
4298    fn as_str_or_empty_defaults_non_string() {
4299        assert_eq!(PerlValue::string("z".into()).as_str_or_empty(), "z");
4300        assert_eq!(PerlValue::integer(1).as_str_or_empty(), "");
4301    }
4302
4303    #[test]
4304    fn to_int_truncates_float_toward_zero() {
4305        assert_eq!(PerlValue::float(3.9).to_int(), 3);
4306        assert_eq!(PerlValue::float(-2.1).to_int(), -2);
4307    }
4308
4309    #[test]
4310    fn to_number_array_is_length() {
4311        assert_eq!(
4312            PerlValue::array(vec![PerlValue::integer(1), PerlValue::integer(2)]).to_number(),
4313            2.0
4314        );
4315    }
4316
4317    #[test]
4318    fn scalar_context_empty_hash_is_zero() {
4319        let h = IndexMap::new();
4320        assert_eq!(PerlValue::hash(h).scalar_context().to_int(), 0);
4321    }
4322
4323    #[test]
4324    fn scalar_context_nonhash_nonarray_clones() {
4325        let v = PerlValue::integer(8);
4326        assert_eq!(v.scalar_context().to_int(), 8);
4327    }
4328
4329    #[test]
4330    fn display_float_integer_like_omits_decimal() {
4331        assert_eq!(PerlValue::float(4.0).to_string(), "4");
4332    }
4333
4334    #[test]
4335    fn display_array_concatenates_element_displays() {
4336        let a = PerlValue::array(vec![PerlValue::integer(1), PerlValue::string("b".into())]);
4337        assert_eq!(a.to_string(), "1b");
4338    }
4339
4340    #[test]
4341    fn display_code_ref_includes_sub_name() {
4342        use super::PerlSub;
4343        let c = PerlValue::code_ref(Arc::new(PerlSub {
4344            name: "foo".into(),
4345            params: vec![],
4346            body: vec![],
4347            closure_env: None,
4348            prototype: None,
4349            fib_like: None,
4350        }));
4351        assert!(c.to_string().contains("foo"));
4352    }
4353
4354    #[test]
4355    fn display_regex_shows_non_capturing_prefix() {
4356        let r = PerlValue::regex(
4357            PerlCompiledRegex::compile("x+").unwrap(),
4358            "x+".into(),
4359            "".into(),
4360        );
4361        assert_eq!(r.to_string(), "(?:x+)");
4362    }
4363
4364    #[test]
4365    fn display_iohandle_is_name() {
4366        assert_eq!(PerlValue::io_handle("STDOUT".into()).to_string(), "STDOUT");
4367    }
4368
4369    #[test]
4370    fn ref_type_blessed_uses_class_name() {
4371        let b = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
4372            "Pkg".into(),
4373            PerlValue::UNDEF,
4374        )));
4375        assert_eq!(b.ref_type().to_string(), "Pkg");
4376    }
4377
4378    #[test]
4379    fn blessed_drop_enqueues_pending_destroy() {
4380        let v = PerlValue::blessed(Arc::new(super::BlessedRef::new_blessed(
4381            "Z".into(),
4382            PerlValue::integer(7),
4383        )));
4384        drop(v);
4385        let q = crate::pending_destroy::take_queue();
4386        assert_eq!(q.len(), 1);
4387        assert_eq!(q[0].0, "Z");
4388        assert_eq!(q[0].1.to_int(), 7);
4389    }
4390
4391    #[test]
4392    fn type_name_iohandle_is_glob() {
4393        assert_eq!(PerlValue::io_handle("FH".into()).type_name(), "GLOB");
4394    }
4395
4396    #[test]
4397    fn empty_hash_is_false() {
4398        assert!(!PerlValue::hash(IndexMap::new()).is_true());
4399    }
4400
4401    #[test]
4402    fn hash_nonempty_is_true() {
4403        let mut h = IndexMap::new();
4404        h.insert("k".into(), PerlValue::UNDEF);
4405        assert!(PerlValue::hash(h).is_true());
4406    }
4407
4408    #[test]
4409    fn num_cmp_equal_integers() {
4410        assert_eq!(
4411            PerlValue::integer(5).num_cmp(&PerlValue::integer(5)),
4412            Ordering::Equal
4413        );
4414    }
4415
4416    #[test]
4417    fn str_cmp_compares_lexicographic_string_forms() {
4418        // Display forms "2" and "10" — string order differs from numeric order.
4419        assert_eq!(
4420            PerlValue::integer(2).str_cmp(&PerlValue::integer(10)),
4421            Ordering::Greater
4422        );
4423    }
4424
4425    #[test]
4426    fn to_list_undef_empty() {
4427        assert!(PerlValue::UNDEF.to_list().is_empty());
4428    }
4429
4430    #[test]
4431    fn unwrap_atomic_nested_atomic() {
4432        use parking_lot::Mutex;
4433        let inner = PerlValue::atomic(Arc::new(Mutex::new(PerlValue::integer(2))));
4434        let outer = PerlValue::atomic(Arc::new(Mutex::new(inner)));
4435        assert_eq!(outer.unwrap_atomic().to_int(), 2);
4436    }
4437
4438    #[test]
4439    fn errno_dual_parts_extracts_code_and_message() {
4440        let v = PerlValue::errno_dual(-2, "oops".into());
4441        assert_eq!(v.errno_dual_parts(), Some((-2, "oops".into())));
4442    }
4443
4444    #[test]
4445    fn errno_dual_parts_none_for_plain_string() {
4446        assert!(PerlValue::string("hi".into()).errno_dual_parts().is_none());
4447    }
4448
4449    #[test]
4450    fn errno_dual_parts_none_for_integer() {
4451        assert!(PerlValue::integer(1).errno_dual_parts().is_none());
4452    }
4453
4454    #[test]
4455    fn errno_dual_numeric_context_uses_code_string_uses_msg() {
4456        let v = PerlValue::errno_dual(5, "five".into());
4457        assert_eq!(v.to_int(), 5);
4458        assert_eq!(v.to_string(), "five");
4459    }
4460
4461    #[test]
4462    fn list_range_alpha_joins_like_perl() {
4463        use super::perl_list_range_expand;
4464        let v =
4465            perl_list_range_expand(PerlValue::string("a".into()), PerlValue::string("z".into()));
4466        let s: String = v.iter().map(|x| x.to_string()).collect();
4467        assert_eq!(s, "abcdefghijklmnopqrstuvwxyz");
4468    }
4469
4470    #[test]
4471    fn list_range_numeric_string_endpoints() {
4472        use super::perl_list_range_expand;
4473        let v = perl_list_range_expand(
4474            PerlValue::string("9".into()),
4475            PerlValue::string("11".into()),
4476        );
4477        assert_eq!(v.len(), 3);
4478        assert_eq!(
4479            v.iter().map(|x| x.to_int()).collect::<Vec<_>>(),
4480            vec![9, 10, 11]
4481        );
4482    }
4483
4484    #[test]
4485    fn list_range_leading_zero_is_string_mode() {
4486        use super::perl_list_range_expand;
4487        let v = perl_list_range_expand(
4488            PerlValue::string("01".into()),
4489            PerlValue::string("05".into()),
4490        );
4491        assert_eq!(v.len(), 5);
4492        assert_eq!(
4493            v.iter().map(|x| x.to_string()).collect::<Vec<_>>(),
4494            vec!["01", "02", "03", "04", "05"]
4495        );
4496    }
4497
4498    #[test]
4499    fn list_range_empty_to_letter_one_element() {
4500        use super::perl_list_range_expand;
4501        let v = perl_list_range_expand(
4502            PerlValue::string(String::new()),
4503            PerlValue::string("c".into()),
4504        );
4505        assert_eq!(v.len(), 1);
4506        assert_eq!(v[0].to_string(), "");
4507    }
4508
4509    #[test]
4510    fn magic_string_inc_z_wraps_aa() {
4511        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
4512        let mut s = "z".to_string();
4513        assert_eq!(
4514            perl_magic_string_increment_for_range(&mut s),
4515            PerlListRangeIncOutcome::Continue
4516        );
4517        assert_eq!(s, "aa");
4518    }
4519
4520    #[test]
4521    fn test_boxed_numeric_stringification() {
4522        // Large integer outside i32 range
4523        let large_int = 10_000_000_000i64;
4524        let v_int = PerlValue::integer(large_int);
4525        assert_eq!(v_int.to_string(), "10000000000");
4526
4527        // Float that needs boxing (e.g. Infinity); Perl prints "Inf".
4528        let v_inf = PerlValue::float(f64::INFINITY);
4529        assert_eq!(v_inf.to_string(), "Inf");
4530    }
4531
4532    #[test]
4533    fn magic_string_inc_nine_to_ten() {
4534        use super::{perl_magic_string_increment_for_range, PerlListRangeIncOutcome};
4535        let mut s = "9".to_string();
4536        assert_eq!(
4537            perl_magic_string_increment_for_range(&mut s),
4538            PerlListRangeIncOutcome::Continue
4539        );
4540        assert_eq!(s, "10");
4541    }
4542}