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_dict_unchecked, 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    crate::stack::check()?;
396    let m = ix.mutator() as *mut Mutator;
397    let m = unsafe { &*m };
398    match cls.kind {
399        ObjKind::Str => {
400            let s = unsafe { as_str_obj(v) }.as_str();
401            if quoted {
402                quote_str(out, s)
403            } else {
404                out.push_str(s)
405            }
406        }
407        ObjKind::BigInt => out.push_str(&format_int(v)),
408        ObjKind::List => {
409            out.push('[');
410            let mut i = 0;
411            loop {
412                let item = read_list(m, v)?.get(i);
413                let Some(item) = item else { break };
414                if i > 0 {
415                    out.push_str(", ");
416                }
417                write_value(ix, item, out, true, depth + 1)?;
418                i += 1;
419            }
420            out.push(']');
421        }
422        ObjKind::Tuple => {
423            out.push('(');
424            let elems = unsafe { as_tuple(v) }.elems();
425            for (i, &e) in elems.iter().enumerate() {
426                if i > 0 {
427                    out.push_str(", ");
428                }
429                write_value(ix, e, out, true, depth + 1)?;
430            }
431            if elems.len() == 1 {
432                out.push(',');
433            }
434            out.push(')');
435        }
436        // SAFETY (the unchecked reads): matched on the kind.
437        ObjKind::Dict | ObjKind::Set => {
438            let is_set = cls.kind == ObjKind::Set;
439            out.push_str(if is_set { "Set[" } else { "[" });
440            let n = unsafe { read_dict_unchecked(m, v, |t| t.raw_len()) }?;
441            let mut first = true;
442            for i in 0..n {
443                let e = unsafe { read_dict_unchecked(m, v, |t| t.raw_entry(i)) }?;
444                let Some(e) = e else { continue };
445                if !first {
446                    out.push_str(", ");
447                }
448                first = false;
449                write_value(ix, e.key, out, true, depth + 1)?;
450                if !is_set {
451                    out.push_str(": ");
452                    write_value(ix, e.value, out, true, depth + 1)?;
453                }
454            }
455            if first && !is_set {
456                out.push(':');
457            }
458            out.push(']');
459        }
460        ObjKind::Instance if cls.id == ids::RANGE => {
461            let r = unsafe { as_instance(v) };
462            write_value(ix, r.get(0), out, true, depth + 1)?;
463            out.push_str(if r.get(2).is_true() { "..." } else { ".." });
464            write_value(ix, r.get(1), out, true, depth + 1)?;
465        }
466        ObjKind::Instance => {
467            let o = unsafe { as_instance(v) };
468            out.push_str(&cls.name);
469            let n = cls.nfields();
470            if cls.variant_tag.is_some() {
471                if n > 0 {
472                    out.push('(');
473                    for i in 0..n {
474                        if i > 0 {
475                            out.push_str(", ");
476                        }
477                        write_value(ix, o.get(i), out, true, depth + 1)?;
478                    }
479                    out.push(')');
480                }
481            } else {
482                out.push('(');
483                for i in 0..n {
484                    if i > 0 {
485                        out.push_str(", ");
486                    }
487                    let _ = write!(out, "{}: ", cls.field_names[i]);
488                    let fv = if h.state() == catallaxy_gc::ObjState::Shared { o.get_acquire(i) } else { o.get(i) };
489                    write_value(ix, fv, out, true, depth + 1)?;
490                }
491                out.push(')');
492            }
493        }
494        ObjKind::StringBuilder => {
495            let s = with_read(m, v, || unsafe { (*as_sb(v).buf.get()).clone() })?;
496            if quoted {
497                quote_str(out, &s)
498            } else {
499                out.push_str(&s)
500            }
501        }
502        ObjKind::Closure => out.push_str("#<fn>"),
503        _ => {
504            let _ = write!(out, "#<{}>", cls.name);
505        }
506    }
507    Ok(())
508}
509
510// ---------------------------------------------------------------------------------------------
511// Equality and hashing
512
513/// `==`: structural for builtins, enums and tuples; user `==` for classes defining it; identity
514/// otherwise. `Int == Float` compares numerically.
515pub fn values_eq(ix: &mut dyn Interop, a: Value, b: Value) -> RtResult<bool> {
516    if a.0 == b.0 {
517        return Ok(!(a.is_float() && a.as_f64().is_nan()));
518    }
519    if a.is_float() || b.is_float() || a.is_small_int() || b.is_small_int() {
520        return Ok(match (num_f64(a), num_f64(b)) {
521            (Some(x), Some(y)) if a.is_float() || b.is_float() => x == y,
522            _ => is_int(a) && is_int(b) && int_cmp(a, b) == CmpOrdering::Equal,
523        });
524    }
525    if !a.is_ptr() || !b.is_ptr() {
526        return Ok(false);
527    }
528    let (ha, hb) = unsafe { (&*a.as_ptr(), &*b.as_ptr()) };
529    let (ca, cb) = (user_class(ha), user_class(hb));
530    if is_user_instance(ca) && ca.lookup(SYMS.eq).is_some() {
531        if let Some(r) = ix.call_method(a, SYMS.eq, &[b])? {
532            return Ok(r.is_true());
533        }
534    }
535    if ca.kind != cb.kind {
536        return Ok(false);
537    }
538    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
539    match ca.kind {
540        ObjKind::Str => Ok(unsafe { as_str_obj(a).as_str() == as_str_obj(b).as_str() }),
541        ObjKind::BigInt => Ok(unsafe { as_bigint(a).value == as_bigint(b).value }),
542        ObjKind::Tuple => {
543            crate::stack::check()?;
544            let (ta, tb) = unsafe { (as_tuple(a), as_tuple(b)) };
545            if ta.len != tb.len {
546                return Ok(false);
547            }
548            for i in 0..ta.len {
549                if !values_eq(ix, ta.elems()[i], tb.elems()[i])? {
550                    return Ok(false);
551                }
552            }
553            Ok(true)
554        }
555        ObjKind::List => {
556            crate::stack::check()?;
557            let len = |v: Value| read_list(m, v).map(|l| l.len());
558            if len(a)? != len(b)? {
559                return Ok(false);
560            }
561            let mut i = 0;
562            loop {
563                let x = read_list(m, a)?.get(i);
564                let y = read_list(m, b)?.get(i);
565                match (x, y) {
566                    (Some(x), Some(y)) => {
567                        if !values_eq(ix, x, y)? {
568                            return Ok(false);
569                        }
570                    }
571                    (None, None) => return Ok(true),
572                    _ => return Ok(false),
573                }
574                i += 1;
575            }
576        }
577        // SAFETY (the unchecked reads): matched on the kind, which `b` shares.
578        ObjKind::Dict | ObjKind::Set => {
579            crate::stack::check()?;
580            let len = |v: Value| unsafe { read_dict_unchecked(m, v, |t| t.len()) };
581            if len(a)? != len(b)? {
582                return Ok(false);
583            }
584            let n = unsafe { read_dict_unchecked(m, a, |t| t.raw_len()) }?;
585            for i in 0..n {
586                let e = unsafe { read_dict_unchecked(m, a, |t| t.raw_entry(i)) }?;
587                let Some(e) = e else { continue };
588                match dict_lookup(ix, b, e.key)? {
589                    None => return Ok(false),
590                    Some(idx) => {
591                        if ca.kind == ObjKind::Dict {
592                            let bv = unsafe { read_dict_unchecked(m, b, |t| t.entry(idx).value) }?;
593                            if !values_eq(ix, e.value, bv)? {
594                                return Ok(false);
595                            }
596                        }
597                    }
598                }
599            }
600            Ok(true)
601        }
602        ObjKind::Instance => {
603            // Enum variants and ranges compare structurally; other instances by identity.
604            if ca.id != cb.id || !(ca.variant_tag.is_some() || ca.id == ids::RANGE) {
605                return Ok(false);
606            }
607            crate::stack::check()?;
608            let (oa, ob) = unsafe { (as_instance(a), as_instance(b)) };
609            for i in 0..ca.nfields() {
610                if !values_eq(ix, oa.get(i), ob.get(i))? {
611                    return Ok(false);
612                }
613            }
614            Ok(true)
615        }
616        _ => Ok(false),
617    }
618}
619
620/// Hash consistent with [`values_eq`].
621pub fn hash_value(ix: &mut dyn Interop, v: Value) -> RtResult<u64> {
622    if v.is_small_int() {
623        return Ok(hash_int(v.as_small_int()));
624    }
625    if v.is_float() {
626        let f = v.as_f64();
627        if f.fract() == 0.0 && f.abs() < 1e15 {
628            return Ok(hash_int(f as i64));
629        }
630        return Ok(mix(f.to_bits()));
631    }
632    if !v.is_ptr() {
633        return Ok(mix(v.0));
634    }
635    let h = unsafe { &*v.as_ptr() };
636    let cls = user_class(h);
637    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
638    match cls.kind {
639        ObjKind::Str => Ok(unsafe { as_str_obj(v) }.hash_code()),
640        ObjKind::BigInt => {
641            let b = &unsafe { as_bigint(v) }.value;
642            Ok(hash_bytes(&b.to_signed_bytes_le()))
643        }
644        ObjKind::Tuple => {
645            crate::stack::check()?;
646            let mut acc = 0x7475_706c_u64;
647            for &e in unsafe { as_tuple(v) }.elems() {
648                acc = mix(acc ^ hash_value(ix, e)?);
649            }
650            Ok(acc)
651        }
652        ObjKind::List => {
653            crate::stack::check()?;
654            let mut acc = 0x6c69_7374_u64;
655            let mut i = 0;
656            loop {
657                let x = read_list(m, v)?.get(i);
658                let Some(x) = x else { break };
659                acc = mix(acc ^ hash_value(ix, x)?);
660                i += 1;
661            }
662            Ok(acc)
663        }
664        // SAFETY (the unchecked reads): matched on the kind.
665        ObjKind::Dict | ObjKind::Set => {
666            crate::stack::check()?;
667            let mut acc = 0x6469_6374_u64;
668            let n = unsafe { read_dict_unchecked(m, v, |t| t.raw_len()) }?;
669            for i in 0..n {
670                let e = unsafe { read_dict_unchecked(m, v, |t| t.raw_entry(i)) }?;
671                if let Some(e) = e {
672                    let hv = if cls.kind == ObjKind::Dict { hash_value(ix, e.value)? } else { 0 };
673                    acc ^= mix(e.hash ^ hv.rotate_left(17));
674                }
675            }
676            Ok(acc)
677        }
678        ObjKind::Instance => {
679            if is_user_instance(cls) && cls.lookup(SYMS.hash).is_some() {
680                if let Some(r) = ix.call_method(v, SYMS.hash, &[])? {
681                    return Ok(if r.is_small_int() { hash_int(r.as_small_int()) } else { mix(r.0) });
682                }
683            }
684            if cls.variant_tag.is_some() || cls.id == ids::RANGE {
685                crate::stack::check()?;
686                let o = unsafe { as_instance(v) };
687                let mut acc = mix(cls.id as u64);
688                for i in 0..cls.nfields() {
689                    acc = mix(acc ^ hash_value(ix, o.get(i))?);
690                }
691                return Ok(acc);
692            }
693            Ok(mix(h.identity_hash() as u64))
694        }
695        _ => Ok(mix(h.identity_hash() as u64)),
696    }
697}
698
699/// Finds `key` in a dict or set, returning its entry index.
700pub fn dict_lookup(ix: &mut dyn Interop, dict: Value, key: Value) -> RtResult<Option<usize>> {
701    let hash = hash_value(ix, key)?;
702    dict_lookup_hashed(ix, dict, key, hash)
703}
704
705/// Raises a `TypeError` if `dict` is not a dict or set.
706pub fn dict_lookup_hashed(ix: &mut dyn Interop, dict: Value, key: Value, hash: u64) -> RtResult<Option<usize>> {
707    dict_of(dict)?;
708    // SAFETY: checked just above.
709    unsafe { dict_lookup_hashed_unchecked(ix, dict, key, hash) }
710}
711
712/// [`dict_lookup_hashed`] without the kind check.
713///
714/// # Safety
715/// `dict` must be a dict or set, checked by the caller (static types are not enough).
716pub unsafe fn dict_lookup_hashed_unchecked(
717    ix: &mut dyn Interop,
718    dict: Value,
719    key: Value,
720    hash: u64,
721) -> RtResult<Option<usize>> {
722    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
723    // SAFETY (the reads below): the caller checked that `dict` is a dict or set.
724    loop {
725        let (probe, version) = unsafe { read_dict_unchecked(m, dict, |t| (t.probe(hash, key), t.version())) }?;
726        match probe {
727            Probe::Found(i) => return Ok(Some(i)),
728            Probe::Absent => return Ok(None),
729            Probe::Check(cands) => {
730                let mut found = None;
731                for idx in cands {
732                    let k = unsafe { read_dict_unchecked(m, dict, |t| t.entry(idx).key) }?;
733                    if values_eq(ix, k, key)? {
734                        found = Some(idx);
735                        break;
736                    }
737                }
738                let still = unsafe { read_dict_unchecked(m, dict, |t| t.version()) }?;
739                if still == version {
740                    return Ok(found);
741                }
742                // The table changed under a user `==`; probe again.
743            }
744        }
745    }
746}
747
748/// `<=>`-style comparison. Returns None if the values are not comparable.
749pub fn compare(ix: &mut dyn Interop, a: Value, b: Value) -> RtResult<Option<CmpOrdering>> {
750    if is_int(a) && is_int(b) {
751        return Ok(Some(int_cmp(a, b)));
752    }
753    if let (Some(x), Some(y)) = (num_f64(a), num_f64(b)) {
754        return Ok(x.partial_cmp(&y));
755    }
756    if !a.is_ptr() || !b.is_ptr() {
757        if a.is_bool() && b.is_bool() {
758            return Ok(Some(a.is_true().cmp(&b.is_true())));
759        }
760        return Ok(None);
761    }
762    let (ca, cb) = unsafe { (heap_class(a.as_ptr()), heap_class(b.as_ptr())) };
763    if is_user_instance(ca) && ca.lookup(SYMS.cmp).is_some() {
764        if let Some(r) = ix.call_method(a, SYMS.cmp, &[b])? {
765            return Ok(int_as_i64(r).map(|i| i.cmp(&0)));
766        }
767    }
768    if ca.kind != cb.kind {
769        return Ok(None);
770    }
771    let m = unsafe { &*(ix.mutator() as *mut Mutator) };
772    match ca.kind {
773        ObjKind::Str => Ok(Some(unsafe { as_str_obj(a).as_str().cmp(as_str_obj(b).as_str()) })),
774        ObjKind::Tuple | ObjKind::List => {
775            crate::stack::check()?;
776            let get = |v: Value, i: usize| -> RtResult<Option<Value>> {
777                if ca.kind == ObjKind::Tuple {
778                    Ok(unsafe { as_tuple(v) }.elems().get(i).copied())
779                } else {
780                    read_list(m, v).map(|l| l.get(i))
781                }
782            };
783            let mut i = 0;
784            loop {
785                match (get(a, i)?, get(b, i)?) {
786                    (None, None) => return Ok(Some(CmpOrdering::Equal)),
787                    (None, Some(_)) => return Ok(Some(CmpOrdering::Less)),
788                    (Some(_), None) => return Ok(Some(CmpOrdering::Greater)),
789                    (Some(x), Some(y)) => match compare(ix, x, y)? {
790                        Some(CmpOrdering::Equal) => {}
791                        other => return Ok(other),
792                    },
793                }
794                i += 1;
795            }
796        }
797        ObjKind::Instance if ca.variant_tag.is_some() && ca.enum_id == cb.enum_id => {
798            let (ta, tb) = (ca.variant_tag.unwrap(), cb.variant_tag.unwrap());
799            if ta != tb {
800                return Ok(Some(ta.cmp(&tb)));
801            }
802            crate::stack::check()?;
803            let (oa, ob) = unsafe { (as_instance(a), as_instance(b)) };
804            for i in 0..ca.nfields() {
805                match compare(ix, oa.get(i), ob.get(i))? {
806                    Some(CmpOrdering::Equal) => {}
807                    other => return Ok(other),
808                }
809            }
810            Ok(Some(CmpOrdering::Equal))
811        }
812        _ => Ok(None),
813    }
814}
815
816/// Compares or panics with a clear message.
817pub fn compare_or_panic(ix: &mut dyn Interop, a: Value, b: Value) -> RtResult<CmpOrdering> {
818    match compare(ix, a, b)? {
819        Some(o) => Ok(o),
820        None => Err(Trap::Panic(format!(
821            "ArgumentError: comparison of {} with {} failed",
822            crate::class::class_of(a).name,
823            crate::class::class_of(b).name
824        ))),
825    }
826}
827
828pub fn sign_of(v: Value) -> i64 {
829    if v.is_small_int() {
830        v.as_small_int().signum()
831    } else {
832        let b = to_bigint(v);
833        if b.is_negative() {
834            -1
835        } else if b.is_zero() {
836            0
837        } else {
838            1
839        }
840    }
841}