Skip to main content

catallaxy_runtime/
ops.rs

1//! Structural operations on values: equality, hashing, ordering, formatting, and arbitrary-precision
2//! integer arithmetic.
3//!
4//! Operations that may need user code (`==`, `hash`, `<=>`, `to_s` defined on a class) go through
5//! the [`Interop`] trait, which the VM implements. Implementations never hold a borrow of an object's
6//! contents across such a call; they re-read by index instead.
7
8use std::cmp::Ordering as CmpOrdering;
9use std::fmt::Write as _;
10use std::hash::Hasher;
11
12use num_bigint::BigInt;
13use num_integer::Integer;
14use num_traits::{Signed, ToPrimitive, Zero};
15use rustc_hash::FxHasher;
16
17use crate::class::{heap_class, ids, Class, ObjKind};
18use crate::dict::Probe;
19use crate::mutator::{panic, Mutator, RtResult, Trap};
20use crate::object::*;
21use crate::ownership::{read_list, with_read};
22use crate::symbol::{intern, Sym};
23use crate::value::{fits_small, Value};
24
25/// Calls back into the VM for operations defined in Catallaxy code.
26pub trait Interop {
27    fn mutator(&mut self) -> &mut Mutator;
28    /// Calls method `name` on `recv` if its class defines it (not interface defaults).
29    /// Returns `Ok(None)` when the class has no such method.
30    fn call_method(&mut self, recv: Value, name: Sym, args: &[Value]) -> RtResult<Option<Value>>;
31}
32
33pub struct Syms {
34    pub eq: Sym,
35    pub hash: Sym,
36    pub cmp: Sym,
37    pub to_s: Sym,
38    pub inspect: Sym,
39}
40
41pub static SYMS: std::sync::LazyLock<Syms> = std::sync::LazyLock::new(|| Syms {
42    eq: intern("=="),
43    hash: intern("hash"),
44    cmp: intern("<=>"),
45    to_s: intern("to_s"),
46    inspect: intern("inspect"),
47});
48
49pub fn hash_bytes(b: &[u8]) -> u64 {
50    let mut h = FxHasher::default();
51    h.write(b);
52    h.write_usize(b.len());
53    mix(h.finish())
54}
55
56#[inline]
57fn mix(mut x: u64) -> u64 {
58    // splitmix64 finalizer: spreads Fx output across all bits for the open-addressing table.
59    x ^= x >> 30;
60    x = x.wrapping_mul(0xbf58_476d_1ce4_e5b9);
61    x ^= x >> 27;
62    x = x.wrapping_mul(0x94d0_49bb_1331_11eb);
63    x ^ (x >> 31)
64}
65
66#[inline]
67pub fn hash_int(i: i64) -> u64 {
68    mix(i as u64 ^ 0x5151_5151)
69}
70
71fn user_class(h: &catallaxy_gc::Header) -> &'static Class {
72    unsafe { heap_class(h) }
73}
74
75fn is_user_instance(cls: &Class) -> bool {
76    cls.kind == ObjKind::Instance && cls.id >= ids::FIRST_USER
77}
78
79// ---------------------------------------------------------------------------------------------
80// Numbers
81
82pub fn is_int(v: Value) -> bool {
83    v.is_small_int() || (v.is_ptr() && kind_of(v) == ObjKind::BigInt)
84}
85
86/// The big integer behind an Int value (callers check `is_int`; anything else reads as 0).
87fn bigint_ref(v: Value) -> Option<&'static BigInt> {
88    if v.is_ptr() && kind_of(v) == ObjKind::BigInt {
89        Some(&unsafe { as_bigint(v) }.value)
90    } else {
91        None
92    }
93}
94
95pub fn to_bigint(v: Value) -> BigInt {
96    if v.is_small_int() {
97        BigInt::from(v.as_small_int())
98    } else {
99        bigint_ref(v).cloned().unwrap_or_default()
100    }
101}
102
103pub fn int_to_f64(v: Value) -> f64 {
104    if v.is_small_int() {
105        v.as_small_int() as f64
106    } else {
107        bigint_ref(v).and_then(|b| b.to_f64()).unwrap_or(f64::NAN)
108    }
109}
110
111/// The value as i64 if it is an Int that fits.
112#[inline]
113pub fn int_as_i64(v: Value) -> Option<i64> {
114    if v.is_small_int() {
115        Some(v.as_small_int())
116    } else {
117        bigint_ref(v).and_then(|b| b.to_i64())
118    }
119}
120
121#[inline]
122pub fn int_add(m: &mut Mutator, a: Value, b: Value) -> Value {
123    if Value::both_small_ints(a, b) {
124        return m.int(a.as_small_int() + b.as_small_int());
125    }
126    m.int_from_big(to_bigint(a) + to_bigint(b))
127}
128
129#[inline]
130pub fn int_sub(m: &mut Mutator, a: Value, b: Value) -> Value {
131    if Value::both_small_ints(a, b) {
132        return m.int(a.as_small_int() - b.as_small_int());
133    }
134    m.int_from_big(to_bigint(a) - to_bigint(b))
135}
136
137#[inline]
138pub fn int_mul(m: &mut Mutator, a: Value, b: Value) -> Value {
139    if Value::both_small_ints(a, b) {
140        if let Some(p) = a.as_small_int().checked_mul(b.as_small_int()) {
141            return m.int(p);
142        }
143    }
144    m.int_from_big(to_bigint(a) * to_bigint(b))
145}
146
147fn zero_div<T>() -> RtResult<T> {
148    panic("ZeroDivisionError: divided by 0")
149}
150
151/// Floor division (rounds toward negative infinity).
152pub fn int_div(m: &mut Mutator, a: Value, b: Value) -> RtResult<Value> {
153    if Value::both_small_ints(a, b) {
154        let (x, y) = (a.as_small_int(), b.as_small_int());
155        if y == 0 {
156            return zero_div();
157        }
158        return Ok(m.int(Integer::div_floor(&x, &y)));
159    }
160    let y = to_bigint(b);
161    if y.is_zero() {
162        return zero_div();
163    }
164    Ok(m.int_from_big(to_bigint(a).div_floor(&y)))
165}
166
167/// Floor modulo: the result has the sign of the divisor.
168pub fn int_mod(m: &mut Mutator, a: Value, b: Value) -> RtResult<Value> {
169    if Value::both_small_ints(a, b) {
170        let (x, y) = (a.as_small_int(), b.as_small_int());
171        if y == 0 {
172            return zero_div();
173        }
174        return Ok(m.int(Integer::mod_floor(&x, &y)));
175    }
176    let y = to_bigint(b);
177    if y.is_zero() {
178        return zero_div();
179    }
180    Ok(m.int_from_big(to_bigint(a).mod_floor(&y)))
181}
182
183pub fn int_pow(m: &mut Mutator, a: Value, b: Value) -> RtResult<Value> {
184    let Some(e) = int_as_i64(b) else { return panic("exponent too large") };
185    if e < 0 {
186        return panic("negative exponent for Int ** Int; use Float");
187    }
188    if e > u32::MAX as i64 {
189        return panic("exponent too large");
190    }
191    if let Some(x) = a.is_small_int().then(|| a.as_small_int()) {
192        if let Some(p) = x.checked_pow(e as u32) {
193            return Ok(m.int(p));
194        }
195    }
196    Ok(m.int_from_big(num_traits::pow(to_bigint(a), e as usize)))
197}
198
199pub fn int_neg(m: &mut Mutator, a: Value) -> Value {
200    if a.is_small_int() {
201        return m.int(-a.as_small_int());
202    }
203    m.int_from_big(-to_bigint(a))
204}
205
206pub fn int_bitop(m: &mut Mutator, op: u8, a: Value, b: Value) -> RtResult<Value> {
207    if Value::both_small_ints(a, b) {
208        let (x, y) = (a.as_small_int(), b.as_small_int());
209        let r = match op {
210            b'&' => x & y,
211            b'|' => x | y,
212            b'^' => x ^ y,
213            b'<' => {
214                if y < 0 {
215                    return panic("negative shift count");
216                }
217                if y < 16 && fits_small(x << y) && (x << y) >> y == x {
218                    x << y
219                } else {
220                    return Ok(m.int_from_big(BigInt::from(x) << (y as usize)));
221                }
222            }
223            b'>' => {
224                if y < 0 {
225                    return panic("negative shift count");
226                }
227                x >> y.min(63)
228            }
229            _ => unreachable!(),
230        };
231        return Ok(m.int(r));
232    }
233    let (x, y) = (to_bigint(a), to_bigint(b));
234    let r = match op {
235        b'&' => x & y,
236        b'|' => x | y,
237        b'^' => x ^ y,
238        b'<' | b'>' => {
239            let Some(n) = y.to_usize() else { return panic("invalid shift count") };
240            if op == b'<' {
241                x << n
242            } else {
243                x >> n
244            }
245        }
246        _ => unreachable!(),
247    };
248    Ok(m.int_from_big(r))
249}
250
251pub fn int_cmp(a: Value, b: Value) -> CmpOrdering {
252    if Value::both_small_ints(a, b) {
253        return a.as_small_int().cmp(&b.as_small_int());
254    }
255    to_bigint(a).cmp(&to_bigint(b))
256}
257
258/// Truncates a float toward zero into an Int (panics on NaN/infinity).
259pub fn float_to_int(m: &mut Mutator, f: f64) -> RtResult<Value> {
260    if !f.is_finite() {
261        return panic(format!("FloatDomainError: {}", format_float(f)));
262    }
263    let t = f.trunc();
264    if t.abs() < 1e15 {
265        return Ok(m.int(t as i64));
266    }
267    use num_traits::FromPrimitive;
268    Ok(m.int_from_big(BigInt::from_f64(t).unwrap()))
269}
270
271fn num_f64(v: Value) -> Option<f64> {
272    if v.is_float() {
273        Some(v.as_f64())
274    } else if is_int(v) {
275        Some(int_to_f64(v))
276    } else {
277        None
278    }
279}
280
281// ---------------------------------------------------------------------------------------------
282// Formatting
283
284/// Formats a float the way Catallaxy prints it: always with a decimal point or exponent.
285pub fn format_float(f: f64) -> String {
286    if f.is_nan() {
287        return "NaN".into();
288    }
289    if f.is_infinite() {
290        return if f > 0.0 { "Infinity".into() } else { "-Infinity".into() };
291    }
292    let a = f.abs();
293    if a != 0.0 && !(1e-4..1e16).contains(&a) {
294        // Shortest round-trip mantissa, Ruby-style exponent: 1.0e+20, 2.5e-07.
295        let s = format!("{:e}", f);
296        let (mant, exp) = s.split_once('e').unwrap();
297        let mant = if mant.contains('.') { mant.to_string() } else { format!("{mant}.0") };
298        let e: i32 = exp.parse().unwrap();
299        return format!("{mant}e{}{:02}", if e < 0 { '-' } else { '+' }, e.abs());
300    }
301    let s = format!("{}", f);
302    if s.contains('.') {
303        s
304    } else {
305        format!("{s}.0")
306    }
307}
308
309pub fn format_int(v: Value) -> String {
310    if v.is_small_int() {
311        v.as_small_int().to_string()
312    } else {
313        to_bigint(v).to_string()
314    }
315}
316
317/// Writes `s` as a quoted string literal.
318pub fn quote_str(out: &mut String, s: &str) {
319    out.push('"');
320    for c in s.chars() {
321        match c {
322            '"' => out.push_str("\\\""),
323            '\\' => out.push_str("\\\\"),
324            '\n' => out.push_str("\\n"),
325            '\t' => out.push_str("\\t"),
326            '\r' => out.push_str("\\r"),
327            '\0' => out.push_str("\\0"),
328            '#' => out.push('#'),
329            c if (c as u32) < 0x20 => {
330                let _ = write!(out, "\\u{{{:x}}}", c as u32);
331            }
332            c => out.push(c),
333        }
334    }
335    out.push('"');
336}
337
338/// `to_s`: strings print raw, everything else as `inspect`.
339pub fn to_s(ix: &mut dyn Interop, v: Value) -> RtResult<String> {
340    if let Some(s) = str_of(v) {
341        return Ok(s.to_string());
342    }
343    let mut out = String::new();
344    write_value(ix, v, &mut out, false, 0)?;
345    Ok(out)
346}
347
348/// `inspect`: strings print quoted.
349pub fn inspect(ix: &mut dyn Interop, v: Value) -> RtResult<String> {
350    let mut out = String::new();
351    write_value(ix, v, &mut out, true, 0)?;
352    Ok(out)
353}
354
355fn write_value(ix: &mut dyn Interop, v: Value, out: &mut String, quoted: bool, depth: usize) -> RtResult<()> {
356    if depth > 64 {
357        out.push_str("...");
358        return Ok(());
359    }
360    if v.is_float() {
361        out.push_str(&format_float(v.as_f64()));
362        return Ok(());
363    }
364    if v.is_small_int() {
365        let _ = write!(out, "{}", v.as_small_int());
366        return Ok(());
367    }
368    if v.is_nil() {
369        out.push_str("nil");
370        return Ok(());
371    }
372    if v.is_bool() {
373        out.push_str(if v.is_true() { "true" } else { "false" });
374        return Ok(());
375    }
376    if !v.is_ptr() {
377        out.push_str("<?>");
378        return Ok(());
379    }
380    let h = unsafe { &*v.as_ptr() };
381    let cls = user_class(h);
382    // User-defined to_s / inspect take precedence.
383    if cls.id >= ids::FIRST_USER || cls.kind == ObjKind::Instance && cls.id != ids::RANGE {
384        let name = if quoted && cls.lookup(SYMS.inspect).is_some() { SYMS.inspect } else { SYMS.to_s };
385        if cls.lookup(name).is_some() {
386            if let Some(r) = ix.call_method(v, name, &[])? {
387                match str_of(r) {
388                    Some(s) => out.push_str(s),
389                    None => return panic(format!("{}#{} must return a String", cls.name, name)),
390                }
391                return Ok(());
392            }
393        }
394    }
395    let m = ix.mutator() as *mut Mutator;
396    let m = unsafe { &*m };
397    match cls.kind {
398        ObjKind::Str => {
399            let s = unsafe { as_str_obj(v) }.as_str();
400            if quoted {
401                quote_str(out, s)
402            } else {
403                out.push_str(s)
404            }
405        }
406        ObjKind::BigInt => out.push_str(&format_int(v)),
407        ObjKind::List => {
408            out.push('[');
409            let mut i = 0;
410            loop {
411                let item = read_list(m, v)?.get(i);
412                let Some(item) = item else { break };
413                if i > 0 {
414                    out.push_str(", ");
415                }
416                write_value(ix, item, out, true, depth + 1)?;
417                i += 1;
418            }
419            out.push(']');
420        }
421        ObjKind::Tuple => {
422            out.push('(');
423            let elems = unsafe { as_tuple(v) }.elems();
424            for (i, &e) in elems.iter().enumerate() {
425                if i > 0 {
426                    out.push_str(", ");
427                }
428                write_value(ix, e, out, true, depth + 1)?;
429            }
430            if elems.len() == 1 {
431                out.push(',');
432            }
433            out.push(')');
434        }
435        ObjKind::Dict | ObjKind::Set => {
436            let is_set = cls.kind == ObjKind::Set;
437            out.push_str(if is_set { "Set[" } else { "[" });
438            let n = with_read(m, v, || unsafe { as_dict(v).table().raw_len() })?;
439            let mut first = true;
440            for i in 0..n {
441                let e = with_read(m, v, || unsafe { as_dict(v).table().raw_entry(i) })?;
442                let Some(e) = e else { continue };
443                if !first {
444                    out.push_str(", ");
445                }
446                first = false;
447                write_value(ix, e.key, out, true, depth + 1)?;
448                if !is_set {
449                    out.push_str(": ");
450                    write_value(ix, e.value, out, true, depth + 1)?;
451                }
452            }
453            if first && !is_set {
454                out.push(':');
455            }
456            out.push(']');
457        }
458        ObjKind::Instance if cls.id == ids::RANGE => {
459            let r = unsafe { as_instance(v) };
460            write_value(ix, r.get(0), out, true, depth + 1)?;
461            out.push_str(if r.get(2).is_true() { "..." } else { ".." });
462            write_value(ix, r.get(1), out, true, depth + 1)?;
463        }
464        ObjKind::Instance => {
465            let o = unsafe { as_instance(v) };
466            out.push_str(&cls.name);
467            let n = cls.nfields();
468            if cls.variant_tag.is_some() {
469                if n > 0 {
470                    out.push('(');
471                    for i in 0..n {
472                        if i > 0 {
473                            out.push_str(", ");
474                        }
475                        write_value(ix, o.get(i), out, true, depth + 1)?;
476                    }
477                    out.push(')');
478                }
479            } else {
480                out.push('(');
481                for i in 0..n {
482                    if i > 0 {
483                        out.push_str(", ");
484                    }
485                    let _ = write!(out, "{}: ", cls.field_names[i]);
486                    let fv = if h.state() == catallaxy_gc::ObjState::Shared { o.get_acquire(i) } else { o.get(i) };
487                    write_value(ix, fv, out, true, depth + 1)?;
488                }
489                out.push(')');
490            }
491        }
492        ObjKind::StringBuilder => {
493            let s = with_read(m, v, || unsafe { (*as_sb(v).buf.get()).clone() })?;
494            if quoted {
495                quote_str(out, &s)
496            } else {
497                out.push_str(&s)
498            }
499        }
500        ObjKind::Closure => out.push_str("#<fn>"),
501        _ => {
502            let _ = write!(out, "#<{}>", cls.name);
503        }
504    }
505    Ok(())
506}
507
508// ---------------------------------------------------------------------------------------------
509// Equality and hashing
510
511/// `==`: structural for builtins, enums and tuples; user `==` for classes defining it; identity
512/// otherwise. `Int == Float` compares numerically.
513pub fn values_eq(ix: &mut dyn Interop, a: Value, b: Value) -> RtResult<bool> {
514    if a.0 == b.0 {
515        return Ok(!(a.is_float() && a.as_f64().is_nan()));
516    }
517    if a.is_float() || b.is_float() || a.is_small_int() || b.is_small_int() {
518        return Ok(match (num_f64(a), num_f64(b)) {
519            (Some(x), Some(y)) if a.is_float() || b.is_float() => x == y,
520            _ => is_int(a) && is_int(b) && int_cmp(a, b) == CmpOrdering::Equal,
521        });
522    }
523    if !a.is_ptr() || !b.is_ptr() {
524        return Ok(false);
525    }
526    let (ha, hb) = unsafe { (&*a.as_ptr(), &*b.as_ptr()) };
527    let (ca, cb) = (user_class(ha), user_class(hb));
528    if is_user_instance(ca) && ca.lookup(SYMS.eq).is_some() {
529        if let Some(r) = ix.call_method(a, SYMS.eq, &[b])? {
530            return Ok(r.is_true());
531        }
532    }
533    if ca.kind != cb.kind {
534        return Ok(false);
535    }
536    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
537    match ca.kind {
538        ObjKind::Str => Ok(unsafe { as_str_obj(a).as_str() == as_str_obj(b).as_str() }),
539        ObjKind::BigInt => Ok(unsafe { as_bigint(a).value == as_bigint(b).value }),
540        ObjKind::Tuple => {
541            let (ta, tb) = unsafe { (as_tuple(a), as_tuple(b)) };
542            if ta.len != tb.len {
543                return Ok(false);
544            }
545            for i in 0..ta.len {
546                if !values_eq(ix, ta.elems()[i], tb.elems()[i])? {
547                    return Ok(false);
548                }
549            }
550            Ok(true)
551        }
552        ObjKind::List => {
553            let len = |v: Value| read_list(m, v).map(|l| l.len());
554            if len(a)? != len(b)? {
555                return Ok(false);
556            }
557            let mut i = 0;
558            loop {
559                let x = read_list(m, a)?.get(i);
560                let y = read_list(m, b)?.get(i);
561                match (x, y) {
562                    (Some(x), Some(y)) => {
563                        if !values_eq(ix, x, y)? {
564                            return Ok(false);
565                        }
566                    }
567                    (None, None) => return Ok(true),
568                    _ => return Ok(false),
569                }
570                i += 1;
571            }
572        }
573        ObjKind::Dict | ObjKind::Set => {
574            let len = |v: Value| with_read(m, v, || unsafe { as_dict(v).table().len() });
575            if len(a)? != len(b)? {
576                return Ok(false);
577            }
578            let n = with_read(m, a, || unsafe { as_dict(a).table().raw_len() })?;
579            for i in 0..n {
580                let e = with_read(m, a, || unsafe { as_dict(a).table().raw_entry(i) })?;
581                let Some(e) = e else { continue };
582                match dict_lookup(ix, b, e.key)? {
583                    None => return Ok(false),
584                    Some(idx) => {
585                        if ca.kind == ObjKind::Dict {
586                            let bv = with_read(m, b, || unsafe { as_dict(b).table().entry(idx).value })?;
587                            if !values_eq(ix, e.value, bv)? {
588                                return Ok(false);
589                            }
590                        }
591                    }
592                }
593            }
594            Ok(true)
595        }
596        ObjKind::Instance => {
597            // Enum variants and ranges compare structurally; other instances by identity.
598            if ca.id != cb.id || !(ca.variant_tag.is_some() || ca.id == ids::RANGE) {
599                return Ok(false);
600            }
601            let (oa, ob) = unsafe { (as_instance(a), as_instance(b)) };
602            for i in 0..ca.nfields() {
603                if !values_eq(ix, oa.get(i), ob.get(i))? {
604                    return Ok(false);
605                }
606            }
607            Ok(true)
608        }
609        _ => Ok(false),
610    }
611}
612
613/// Hash consistent with [`values_eq`].
614pub fn hash_value(ix: &mut dyn Interop, v: Value) -> RtResult<u64> {
615    if v.is_small_int() {
616        return Ok(hash_int(v.as_small_int()));
617    }
618    if v.is_float() {
619        let f = v.as_f64();
620        if f.fract() == 0.0 && f.abs() < 1e15 {
621            return Ok(hash_int(f as i64));
622        }
623        return Ok(mix(f.to_bits()));
624    }
625    if !v.is_ptr() {
626        return Ok(mix(v.0));
627    }
628    let h = unsafe { &*v.as_ptr() };
629    let cls = user_class(h);
630    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
631    match cls.kind {
632        ObjKind::Str => Ok(unsafe { as_str_obj(v) }.hash_code()),
633        ObjKind::BigInt => {
634            let b = &unsafe { as_bigint(v) }.value;
635            Ok(hash_bytes(&b.to_signed_bytes_le()))
636        }
637        ObjKind::Tuple => {
638            let mut acc = 0x7475_706c_u64;
639            for &e in unsafe { as_tuple(v) }.elems() {
640                acc = mix(acc ^ hash_value(ix, e)?);
641            }
642            Ok(acc)
643        }
644        ObjKind::List => {
645            let mut acc = 0x6c69_7374_u64;
646            let mut i = 0;
647            loop {
648                let x = read_list(m, v)?.get(i);
649                let Some(x) = x else { break };
650                acc = mix(acc ^ hash_value(ix, x)?);
651                i += 1;
652            }
653            Ok(acc)
654        }
655        ObjKind::Dict | ObjKind::Set => {
656            let mut acc = 0x6469_6374_u64;
657            let n = with_read(m, v, || unsafe { as_dict(v).table().raw_len() })?;
658            for i in 0..n {
659                let e = with_read(m, v, || unsafe { as_dict(v).table().raw_entry(i) })?;
660                if let Some(e) = e {
661                    let hv = if cls.kind == ObjKind::Dict { hash_value(ix, e.value)? } else { 0 };
662                    acc ^= mix(e.hash ^ hv.rotate_left(17));
663                }
664            }
665            Ok(acc)
666        }
667        ObjKind::Instance => {
668            if is_user_instance(cls) && cls.lookup(SYMS.hash).is_some() {
669                if let Some(r) = ix.call_method(v, SYMS.hash, &[])? {
670                    return Ok(if r.is_small_int() { hash_int(r.as_small_int()) } else { mix(r.0) });
671                }
672            }
673            if cls.variant_tag.is_some() || cls.id == ids::RANGE {
674                let o = unsafe { as_instance(v) };
675                let mut acc = mix(cls.id as u64);
676                for i in 0..cls.nfields() {
677                    acc = mix(acc ^ hash_value(ix, o.get(i))?);
678                }
679                return Ok(acc);
680            }
681            Ok(mix(h.identity_hash() as u64))
682        }
683        _ => Ok(mix(h.identity_hash() as u64)),
684    }
685}
686
687/// Finds `key` in a dict or set, returning its entry index.
688pub fn dict_lookup(ix: &mut dyn Interop, dict: Value, key: Value) -> RtResult<Option<usize>> {
689    let hash = hash_value(ix, key)?;
690    dict_lookup_hashed(ix, dict, key, hash)
691}
692
693pub fn dict_lookup_hashed(ix: &mut dyn Interop, dict: Value, key: Value, hash: u64) -> RtResult<Option<usize>> {
694    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
695    loop {
696        let (probe, version) =
697            with_read(m, dict, || unsafe { let t = as_dict(dict).table(); (t.probe(hash, key), t.version()) })?;
698        match probe {
699            Probe::Found(i) => return Ok(Some(i)),
700            Probe::Absent => return Ok(None),
701            Probe::Check(cands) => {
702                let mut found = None;
703                for idx in cands {
704                    let k = with_read(m, dict, || unsafe { as_dict(dict).table().entry(idx).key })?;
705                    if values_eq(ix, k, key)? {
706                        found = Some(idx);
707                        break;
708                    }
709                }
710                let still = with_read(m, dict, || unsafe { as_dict(dict).table().version() })?;
711                if still == version {
712                    return Ok(found);
713                }
714                // The table changed under a user `==`; probe again.
715            }
716        }
717    }
718}
719
720/// `<=>`-style comparison. Returns None if the values are not comparable.
721pub fn compare(ix: &mut dyn Interop, a: Value, b: Value) -> RtResult<Option<CmpOrdering>> {
722    if is_int(a) && is_int(b) {
723        return Ok(Some(int_cmp(a, b)));
724    }
725    if let (Some(x), Some(y)) = (num_f64(a), num_f64(b)) {
726        return Ok(x.partial_cmp(&y));
727    }
728    if !a.is_ptr() || !b.is_ptr() {
729        if a.is_bool() && b.is_bool() {
730            return Ok(Some(a.is_true().cmp(&b.is_true())));
731        }
732        return Ok(None);
733    }
734    let (ca, cb) = unsafe { (heap_class(a.as_ptr()), heap_class(b.as_ptr())) };
735    if is_user_instance(ca) && ca.lookup(SYMS.cmp).is_some() {
736        if let Some(r) = ix.call_method(a, SYMS.cmp, &[b])? {
737            return Ok(int_as_i64(r).map(|i| i.cmp(&0)));
738        }
739    }
740    if ca.kind != cb.kind {
741        return Ok(None);
742    }
743    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
744    match ca.kind {
745        ObjKind::Str => Ok(Some(unsafe { as_str_obj(a).as_str().cmp(as_str_obj(b).as_str()) })),
746        ObjKind::Tuple | ObjKind::List => {
747            let get = |v: Value, i: usize| -> RtResult<Option<Value>> {
748                if ca.kind == ObjKind::Tuple {
749                    Ok(unsafe { as_tuple(v) }.elems().get(i).copied())
750                } else {
751                    read_list(m, v).map(|l| l.get(i))
752                }
753            };
754            let mut i = 0;
755            loop {
756                match (get(a, i)?, get(b, i)?) {
757                    (None, None) => return Ok(Some(CmpOrdering::Equal)),
758                    (None, Some(_)) => return Ok(Some(CmpOrdering::Less)),
759                    (Some(_), None) => return Ok(Some(CmpOrdering::Greater)),
760                    (Some(x), Some(y)) => match compare(ix, x, y)? {
761                        Some(CmpOrdering::Equal) => {}
762                        other => return Ok(other),
763                    },
764                }
765                i += 1;
766            }
767        }
768        ObjKind::Instance if ca.variant_tag.is_some() && ca.enum_id == cb.enum_id => {
769            let (ta, tb) = (ca.variant_tag.unwrap(), cb.variant_tag.unwrap());
770            if ta != tb {
771                return Ok(Some(ta.cmp(&tb)));
772            }
773            let (oa, ob) = unsafe { (as_instance(a), as_instance(b)) };
774            for i in 0..ca.nfields() {
775                match compare(ix, oa.get(i), ob.get(i))? {
776                    Some(CmpOrdering::Equal) => {}
777                    other => return Ok(other),
778                }
779            }
780            Ok(Some(CmpOrdering::Equal))
781        }
782        _ => Ok(None),
783    }
784}
785
786/// Compares or panics with a clear message.
787pub fn compare_or_panic(ix: &mut dyn Interop, a: Value, b: Value) -> RtResult<CmpOrdering> {
788    match compare(ix, a, b)? {
789        Some(o) => Ok(o),
790        None => Err(Trap::Panic(format!(
791            "ArgumentError: comparison of {} with {} failed",
792            crate::class::class_of(a).name,
793            crate::class::class_of(b).name
794        ))),
795    }
796}
797
798pub fn sign_of(v: Value) -> i64 {
799    if v.is_small_int() {
800        v.as_small_int().signum()
801    } else {
802        let b = to_bigint(v);
803        if b.is_negative() {
804            -1
805        } else if b.is_zero() {
806            0
807        } else {
808            1
809        }
810    }
811}