Skip to main content

stryke/
value.rs

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