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