Skip to main content

stryke/
value.rs

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