Skip to main content

submilli_engine/runtime/prelude/bigint/
ops.rs

1//! host-side BigInt runtime — `submilli:bigint.*` plus the
2//! `submilli:number.fromBigInt` arm.
3
4use crate::runtime::host::{abi_arg, abi_result};
5use wasmtime::{
6    ArrayRef, ArrayRefPre, ArrayType, AsContextMut, Caller, Engine, FieldType, FuncType, HeapType,
7    Linker, Mutability, RefType, Rooted, StorageType, StructRef, StructRefPre, Val, ValType,
8};
9
10use crate::runtime::host::{
11    range_error, read_string_arg, register_host_fn, type_error, write_submilli_string,
12};
13use crate::runtime::intrinsic_types::intrinsic_types;
14use crate::runtime::{NUMBER_MODULE_NAME, StoreData, fuel};
15
16pub const BIGINT_MODULE_NAME: &str = "submilli:bigint";
17
18/// A finite integer double has at most 1024 magnitude bits. Convert its exact
19/// represented value rather than saturating it through a fixed-width integer.
20pub(super) fn integer_number(n: f64, name: &str) -> wasmtime::Result<num_bigint::BigInt> {
21    if !n.is_finite() {
22        return Err(range_error(format!(
23            "{name}: cannot convert non-finite number to bigint ({n})"
24        )));
25    }
26    if n.fract() != 0.0 {
27        return Err(range_error(format!(
28            "{name}: cannot convert non-integer number to bigint ({n})"
29        )));
30    }
31    <num_bigint::BigInt as num_traits::FromPrimitive>::from_f64(n).ok_or_else(|| {
32        crate::runtime::host::fatal_host_error("BigInt conversion refused a finite integer double")
33    })
34}
35
36/// Standalone so cross-module canonicalization aligns with the consumer's `$rawBigInt`.
37pub(crate) fn limbs_array_type(engine: &Engine) -> ArrayType {
38    ArrayType::new(
39        engine,
40        FieldType::new(Mutability::Var, StorageType::ValType(ValType::I64)),
41    )
42}
43
44pub(crate) fn install(
45    linker: &mut Linker<StoreData>,
46    string_type: &ArrayType,
47) -> wasmtime::Result<()> {
48    let engine = linker.engine().clone();
49    let raw_string_param = ValType::Ref(RefType::new(false, HeapType::from(string_type.clone())));
50    let raw_string_result = raw_string_param.clone();
51    let limbs_array_ty = limbs_array_type(&engine);
52    let raw_bigint_result =
53        ValType::Ref(RefType::new(false, HeapType::from(limbs_array_ty.clone())));
54    let raw_bigint_param = raw_bigint_result.clone();
55
56    let from_string_ty = FuncType::new(
57        &engine,
58        [raw_string_param.clone()],
59        [ValType::I32, raw_bigint_result.clone()],
60    );
61    register_host_fn(
62        linker,
63        BIGINT_MODULE_NAME,
64        crate::mangle::host(BIGINT_MODULE_NAME, "fromString"),
65        from_string_ty,
66        /* deterministic = */ true,
67        |caller, params, results| -> wasmtime::Result<()> {
68            let s = read_string_arg(&mut *caller, abi_arg(params, 0)?, "bigint.fromString")?;
69            let parsed = parse_decimal(caller, &s, "bigint.fromString")?;
70            let (sign, magnitude) = parsed.into_parts();
71            let limbs = magnitude.to_u64_digits();
72            let arr = write_limbs(&mut *caller, &limbs)?;
73            *abi_result(results, 0)? = Val::I32(sign_to_i32(sign));
74            *abi_result(results, 1)? = Val::AnyRef(Some(arr.to_anyref()));
75            Ok(())
76        },
77    )?;
78
79    let from_number_ty = FuncType::new(
80        &engine,
81        [ValType::F64],
82        [ValType::I32, raw_bigint_result.clone()],
83    );
84    register_host_fn(
85        linker,
86        BIGINT_MODULE_NAME,
87        crate::mangle::host(BIGINT_MODULE_NAME, "fromNumber"),
88        from_number_ty,
89        /* deterministic = */ true,
90        |caller, params, results| -> wasmtime::Result<()> {
91            let n = match *abi_arg(params, 0)? {
92                Val::F64(bits) => f64::from_bits(bits),
93                ref other => {
94                    return Err(type_error(format!(
95                        "bigint.fromNumber expects f64, got {other:?}"
96                    )));
97                }
98            };
99            let parsed = integer_number(n, "bigint.fromNumber")?;
100            let (sign, magnitude) = parsed.into_parts();
101            let limbs = magnitude.to_u64_digits();
102            let arr = write_limbs(&mut *caller, &limbs)?;
103            *abi_result(results, 0)? = Val::I32(sign_to_i32(sign));
104            *abi_result(results, 1)? = Val::AnyRef(Some(arr.to_anyref()));
105            Ok(())
106        },
107    )?;
108
109    let to_string_ty = FuncType::new(
110        &engine,
111        [ValType::I32, raw_bigint_param.clone()],
112        [raw_string_result.clone()],
113    );
114    register_host_fn(
115        linker,
116        BIGINT_MODULE_NAME,
117        crate::mangle::host(BIGINT_MODULE_NAME, "toString"),
118        to_string_ty,
119        /* deterministic = */ true,
120        |caller, params, results| -> wasmtime::Result<()> {
121            let value = read_bigint_arg(
122                &mut *caller,
123                abi_arg(params, 0)?,
124                abi_arg(params, 1)?,
125                "bigint.toString",
126            )?;
127            let formatted = format_bigint(caller, &value, 10)?;
128            let arr = write_submilli_string(&mut *caller, &formatted)?;
129            *abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
130            Ok(())
131        },
132    )?;
133
134    let to_string_radix_ty = FuncType::new(
135        &engine,
136        [ValType::I32, raw_bigint_param.clone(), ValType::F64],
137        [raw_string_result.clone()],
138    );
139    register_host_fn(
140        linker,
141        BIGINT_MODULE_NAME,
142        crate::mangle::host(BIGINT_MODULE_NAME, "toStringRadix"),
143        to_string_radix_ty,
144        /* deterministic = */ true,
145        |caller, params, results| -> wasmtime::Result<()> {
146            let value = read_bigint_arg(
147                &mut *caller,
148                abi_arg(params, 0)?,
149                abi_arg(params, 1)?,
150                "bigint.toStringRadix",
151            )?;
152            let radix = match *abi_arg(params, 2)? {
153                Val::F64(bits) => f64::from_bits(bits),
154                ref other => {
155                    return Err(type_error(format!(
156                        "bigint.toStringRadix expects f64 radix, got {other:?}"
157                    )));
158                }
159            };
160            let truncated = radix.trunc();
161            if radix.is_nan() || !(2.0..=36.0).contains(&truncated) {
162                return Err(range_error("toString radix must be between 2 and 36"));
163            }
164            let formatted = format_bigint(caller, &value, truncated as u32)?;
165            let arr = write_submilli_string(&mut *caller, &formatted)?;
166            *abi_result(results, 0)? = Val::AnyRef(Some(arr.to_anyref()));
167            Ok(())
168        },
169    )?;
170
171    let binop_ty = FuncType::new(
172        &engine,
173        [
174            ValType::I32,
175            raw_bigint_param.clone(),
176            ValType::I32,
177            raw_bigint_param.clone(),
178        ],
179        [ValType::I32, raw_bigint_result.clone()],
180    );
181    for (name, op, cost) in [
182        ("add", BinOp::Add, Cost::Linear),
183        ("sub", BinOp::Sub, Cost::Linear),
184        ("mul", BinOp::Mul, Cost::Product),
185    ] {
186        register_host_fn(
187            linker,
188            BIGINT_MODULE_NAME,
189            crate::mangle::host(BIGINT_MODULE_NAME, name),
190            binop_ty.clone(),
191            /* deterministic = */ true,
192            move |caller, params, results| -> wasmtime::Result<()> {
193                run_binop(&mut *caller, params, results, name, cost, |a, b| {
194                    Ok(match op {
195                        BinOp::Add => a + b,
196                        BinOp::Sub => a - b,
197                        BinOp::Mul => a * b,
198                    })
199                })
200            },
201        )?;
202    }
203    for (name, kind) in [("div", DivKind::Div), ("mod", DivKind::Rem)] {
204        register_host_fn(
205            linker,
206            BIGINT_MODULE_NAME,
207            crate::mangle::host(BIGINT_MODULE_NAME, name),
208            binop_ty.clone(),
209            /* deterministic = */ true,
210            move |caller, params, results| -> wasmtime::Result<()> {
211                use num_traits::Zero;
212                let divisor = read_bigint_arg(
213                    caller,
214                    abi_arg(params, 2)?,
215                    abi_arg(params, 3)?,
216                    &format!("{name} rhs"),
217                )?;
218                if divisor.is_zero() {
219                    return Err(range_error("Division by zero"));
220                }
221                let dividend =
222                    read_bigint_arg(caller, &params[0], &params[1], &format!("{name} lhs"))?;
223                fuel::charge_host_fuel(&mut *caller, Cost::Product.of(&dividend, &divisor)?)?;
224                let result = match kind {
225                    DivKind::Div => dividend / divisor,
226                    DivKind::Rem => dividend % divisor,
227                };
228                write_binop_result(caller, results, result)
229            },
230        )?;
231    }
232
233    // num_bigint::pow takes u32, so that's the practical ceiling on the exponent.
234    register_host_fn(
235        linker,
236        BIGINT_MODULE_NAME,
237        crate::mangle::host(BIGINT_MODULE_NAME, "pow"),
238        binop_ty.clone(),
239        /* deterministic = */ true,
240        |caller, params, results| -> wasmtime::Result<()> {
241            run_binop(
242                &mut *caller,
243                params,
244                results,
245                "pow",
246                Cost::Pow,
247                |base, exp| {
248                    use num_traits::{Signed, ToPrimitive};
249                    if exp.is_negative() {
250                        return Err(range_error("bigint.pow: exponent must be non-negative"));
251                    }
252                    let exp_u32 = exp.to_u32().ok_or_else(|| {
253                        range_error("bigint.pow: exponent too large to fit in u32")
254                    })?;
255                    Ok(base.pow(exp_u32))
256                },
257            )
258        },
259    )?;
260
261    let cmp_ty = FuncType::new(
262        &engine,
263        [
264            ValType::I32,
265            raw_bigint_param.clone(),
266            ValType::I32,
267            raw_bigint_param.clone(),
268        ],
269        [ValType::I32],
270    );
271    register_host_fn(
272        linker,
273        BIGINT_MODULE_NAME,
274        crate::mangle::host(BIGINT_MODULE_NAME, "cmp"),
275        cmp_ty,
276        /* deterministic = */ true,
277        |caller, params, results| -> wasmtime::Result<()> {
278            let a = read_bigint_arg(
279                &mut *caller,
280                abi_arg(params, 0)?,
281                abi_arg(params, 1)?,
282                "bigint.cmp lhs",
283            )?;
284            let b = read_bigint_arg(
285                &mut *caller,
286                abi_arg(params, 2)?,
287                abi_arg(params, 3)?,
288                "bigint.cmp rhs",
289            )?;
290            *abi_result(results, 0)? = Val::I32(match a.cmp(&b) {
291                std::cmp::Ordering::Less => -1,
292                std::cmp::Ordering::Equal => 0,
293                std::cmp::Ordering::Greater => 1,
294            });
295            Ok(())
296        },
297    )?;
298
299    let neg_ty = FuncType::new(
300        &engine,
301        [ValType::I32, raw_bigint_param.clone()],
302        [ValType::I32, raw_bigint_result.clone()],
303    );
304    register_host_fn(
305        linker,
306        BIGINT_MODULE_NAME,
307        crate::mangle::host(BIGINT_MODULE_NAME, "neg"),
308        neg_ty,
309        /* deterministic = */ true,
310        |caller, params, results| -> wasmtime::Result<()> {
311            let sign = read_sign(abi_arg(params, 0)?)?;
312            let limbs = read_limbs_arg(&mut *caller, abi_arg(params, 1)?, "bigint.neg")?;
313            let arr = write_limbs(&mut *caller, &limbs)?;
314            *abi_result(results, 0)? = Val::I32(-sign);
315            *abi_result(results, 1)? = Val::AnyRef(Some(arr.to_anyref()));
316            Ok(())
317        },
318    )?;
319
320    let from_bigint_ty = FuncType::new(&engine, [ValType::I32, raw_bigint_param], [ValType::F64]);
321    register_host_fn(
322        linker,
323        NUMBER_MODULE_NAME,
324        crate::mangle::host(NUMBER_MODULE_NAME, "fromBigInt"),
325        from_bigint_ty,
326        /* deterministic = */ true,
327        |caller, params, results| -> wasmtime::Result<()> {
328            use num_traits::ToPrimitive;
329            let value = read_bigint_arg(
330                &mut *caller,
331                abi_arg(params, 0)?,
332                abi_arg(params, 1)?,
333                "number.fromBigInt",
334            )?;
335            let n = value.to_f64().unwrap_or(f64::INFINITY);
336            *abi_result(results, 0)? = Val::F64(n.to_bits());
337            Ok(())
338        },
339    )?;
340
341    Ok(())
342}
343
344#[derive(Copy, Clone)]
345enum BinOp {
346    Add,
347    Sub,
348    Mul,
349}
350
351#[derive(Copy, Clone)]
352enum DivKind {
353    Div,
354    Rem,
355}
356
357/// Bounds conversion work independently of the program's total fuel budget.
358pub(crate) const MAX_DECIMAL_INPUT_BYTES: usize = 65_536;
359const MAX_FORMAT_LIMBS: u64 = 4_096;
360
361pub(crate) fn parse_decimal(
362    caller: &mut Caller<'_, StoreData>,
363    text: &str,
364    operation: &str,
365) -> wasmtime::Result<num_bigint::BigInt> {
366    if text.len() > MAX_DECIMAL_INPUT_BYTES {
367        return Err(range_error(format!(
368            "{operation}: decimal input exceeds {MAX_DECIMAL_INPUT_BYTES} bytes; use a smaller integer"
369        )));
370    }
371    let trimmed = text.trim();
372    let digits = trimmed.len() as u64;
373    // Decimal conversion uses repeated multiplication, unlike the faster
374    // multiplication algorithms priced by bigint_product_cost.
375    fuel::charge(
376        &mut *caller,
377        fuel::ELEM,
378        digits.saturating_mul(digits.div_ceil(19)),
379    )?;
380    trimmed.parse().map_err(|_| {
381        crate::runtime::host::syntax_error(format!(
382            "{operation}: invalid bigint literal: {trimmed:?}"
383        ))
384    })
385}
386
387/// Power-of-two radices extract bits linearly; other radices repeatedly divide
388/// the remaining magnitude and need a quadratic work bound.
389pub(crate) fn format_bigint(
390    caller: &mut Caller<'_, StoreData>,
391    value: &num_bigint::BigInt,
392    radix: u32,
393) -> wasmtime::Result<String> {
394    if !(2..=36).contains(&radix) {
395        return Err(range_error("toString radix must be between 2 and 36"));
396    }
397    let limbs = value.bits().div_ceil(64);
398    if limbs > MAX_FORMAT_LIMBS {
399        return Err(range_error(format!(
400            "BigInt.toString: exceeds {MAX_FORMAT_LIMBS} limbs; format a smaller integer"
401        )));
402    }
403    let work = if radix.is_power_of_two() {
404        limbs.max(1)
405    } else {
406        limbs.max(1).saturating_mul(limbs.max(1))
407    };
408    fuel::charge(&mut *caller, fuel::ELEM, work)?;
409    Ok(value.to_str_radix(radix))
410}
411
412/// Limbs of the magnitude: the size variable of every BigInt cost.
413fn limbs_of(value: &num_bigint::BigInt) -> u64 {
414    value.magnitude().to_u64_digits().len() as u64
415}
416
417/// The largest BigInt `pow` may produce, in limbs: 512 KiB of magnitude. The
418/// result size is known before the work, so an oversized one is refused
419/// instead of built in host memory the store limit does not see.
420const MAX_POW_LIMBS: u64 = 1 << 16;
421
422/// How a binary operation's work scales with its operands.
423#[derive(Copy, Clone)]
424enum Cost {
425    /// A pass over the longer operand: add, sub.
426    Linear,
427    /// A limb pair per step: mul, div, mod.
428    Product,
429    /// Repeated squaring up to the result size.
430    Pow,
431}
432
433impl Cost {
434    /// The fuel for `a op b`, sized before the work. `Pow` refuses a result
435    /// over the cap here, before any of it is built.
436    fn of(self, a: &num_bigint::BigInt, b: &num_bigint::BigInt) -> wasmtime::Result<u64> {
437        let (la, lb) = (limbs_of(a), limbs_of(b));
438        Ok(match self {
439            Cost::Linear => fuel::ELEM.cost(la.max(lb)),
440            Cost::Product => fuel::bigint_product_cost(la, lb),
441            Cost::Pow => {
442                use num_traits::ToPrimitive;
443                // An exponent outside u32 is refused by the operation itself,
444                // with its own message; it costs nothing here.
445                match b.to_u32() {
446                    Some(exp) => pow_cost(a, exp)?,
447                    None => 0,
448                }
449            }
450        })
451    }
452}
453
454/// The work of `base ** exp`, dominated by the squarings near the result
455/// size; the result has `bits(base) * exp / 64` limbs. A base of magnitude 0
456/// or 1 has a one-limb result whatever the exponent.
457fn pow_cost(base: &num_bigint::BigInt, exp: u32) -> wasmtime::Result<u64> {
458    let bits = base.bits();
459    if bits <= 1 {
460        return Ok(fuel::ELEM.cost(1));
461    }
462    let result_limbs = bits.saturating_mul(u64::from(exp)).div_ceil(64);
463    if result_limbs > MAX_POW_LIMBS {
464        return Err(range_error("bigint.pow: result too large"));
465    }
466    Ok(fuel::bigint_product_cost(result_limbs, result_limbs))
467}
468
469fn run_binop(
470    caller: &mut Caller<'_, StoreData>,
471    params: &[Val],
472    results: &mut [Val],
473    op_name: &str,
474    cost: Cost,
475    op: impl FnOnce(num_bigint::BigInt, num_bigint::BigInt) -> wasmtime::Result<num_bigint::BigInt>,
476) -> wasmtime::Result<()> {
477    let a = read_bigint_arg(
478        caller,
479        abi_arg(params, 0)?,
480        abi_arg(params, 1)?,
481        &format!("{op_name} lhs"),
482    )?;
483    let b = read_bigint_arg(
484        caller,
485        abi_arg(params, 2)?,
486        abi_arg(params, 3)?,
487        &format!("{op_name} rhs"),
488    )?;
489    fuel::charge_host_fuel(&mut *caller, cost.of(&a, &b)?)?;
490    let r = op(a, b)?;
491    write_binop_result(caller, results, r)
492}
493
494fn write_binop_result(
495    caller: &mut Caller<'_, StoreData>,
496    results: &mut [Val],
497    value: num_bigint::BigInt,
498) -> wasmtime::Result<()> {
499    let (sign, magnitude) = value.into_parts();
500    let limbs = magnitude.to_u64_digits();
501    let arr = write_limbs(caller, &limbs)?;
502    *abi_result(results, 0)? = Val::I32(sign_to_i32(sign));
503    *abi_result(results, 1)? = Val::AnyRef(Some(arr.to_anyref()));
504    Ok(())
505}
506
507pub(crate) fn read_bigint_arg(
508    caller: &mut Caller<'_, StoreData>,
509    sign_val: &Val,
510    limbs_val: &Val,
511    name: &str,
512) -> wasmtime::Result<num_bigint::BigInt> {
513    let sign = read_sign(sign_val)?;
514    let limbs = read_limbs_arg(caller, limbs_val, name)?;
515    limbs_to_bigint(sign, &limbs)
516}
517
518/// Reconstruct a `num_bigint::BigInt` from the canonical `$bigint` payload —
519/// `sign` ∈ {−1, 0, 1} and little-endian u64 `limbs`. Shared by the host
520/// arithmetic ABI and the host-owned `$bigint` vtable.
521pub(crate) fn limbs_to_bigint(sign: i32, limbs: &[u64]) -> wasmtime::Result<num_bigint::BigInt> {
522    let count = limbs
523        .len()
524        .checked_mul(2)
525        .ok_or_else(|| crate::runtime::host::fatal_host_error("BigInt limb count overflow"))?;
526    let mut u32s = Vec::new();
527    u32s.try_reserve_exact(count)
528        .map_err(crate::runtime::host::fatal_host_error)?;
529    for w in limbs {
530        u32s.push((*w & 0xFFFF_FFFF) as u32);
531        u32s.push((*w >> 32) as u32);
532    }
533    let magnitude = num_bigint::BigUint::from_slice(&u32s);
534    let signum = match sign {
535        v if v > 0 => num_bigint::Sign::Plus,
536        v if v < 0 => num_bigint::Sign::Minus,
537        _ => num_bigint::Sign::NoSign,
538    };
539    Ok(num_bigint::BigInt::from_biguint(signum, magnitude))
540}
541
542/// Read a `$bigint` struct ref's `sign` (field 1) and little-endian u64 `limbs`
543/// (field 2). The host-owned `$bigint` vtable and the prelude-host method ABI
544/// both take a typed receiver this way (vs. the arithmetic ABI, which receives
545/// `sign` and `limbs` as separate stack values via [`read_bigint_arg`]).
546pub(crate) fn read_bigint_struct(
547    caller: &mut Caller<'_, StoreData>,
548    val: &Val,
549    name: &str,
550) -> wasmtime::Result<(i32, Vec<u64>)> {
551    let st: Rooted<StructRef> = match val {
552        Val::AnyRef(Some(any)) => any
553            .as_struct(&mut *caller)?
554            .ok_or_else(|| type_error(format!("{name}: receiver is not a struct")))?,
555        other => {
556            return Err(type_error(format!(
557                "{name}: expected (ref $bigint), got {other:?}"
558            )));
559        }
560    };
561    let sign = read_sign(&st.field(&mut *caller, 1)?)?;
562    let limbs_val = st.field(&mut *caller, 2)?;
563    let limbs = read_limbs_arg(caller, &limbs_val, name)?;
564    Ok((sign, limbs))
565}
566
567fn read_limbs_arg(
568    caller: &mut Caller<'_, StoreData>,
569    val: &Val,
570    name: &str,
571) -> wasmtime::Result<Vec<u64>> {
572    let arr = match val {
573        Val::AnyRef(Some(any)) => any.unwrap_array(&mut *caller)?,
574        Val::AnyRef(None) => {
575            return Err(type_error(format!("{name} limbs arg is null")));
576        }
577        other => {
578            return Err(type_error(format!(
579                "{name} expects (ref $rawBigInt), got {other:?}",
580            )));
581        }
582    };
583    let len = arr.len(&mut *caller)?;
584    fuel::charge(&mut *caller, fuel::COPY, u64::from(len).saturating_mul(8))?;
585    let mut out = Vec::new();
586    out.try_reserve_exact(len as usize)
587        .map_err(crate::runtime::host::fatal_host_error)?;
588    out.resize(len as usize, 0);
589    arr.copy_to_i64_slice(&*caller, &mut out)?;
590    Ok(out)
591}
592
593pub(crate) fn write_limbs(
594    mut ctx: impl AsContextMut<Data = StoreData>,
595    limbs: &[u64],
596) -> wasmtime::Result<Rooted<ArrayRef>> {
597    fuel::charge(&mut ctx, fuel::COPY, (limbs.len() as u64).saturating_mul(8))?;
598    let array_ty = limbs_array_type(ctx.as_context().engine());
599    let pre = ArrayRefPre::new(&mut ctx, array_ty);
600    ArrayRef::new_from_i64_slice(&mut ctx, &pre, limbs)
601}
602
603pub(crate) fn make_bigint_struct(
604    caller: &mut Caller<'_, StoreData>,
605    value: num_bigint::BigInt,
606) -> wasmtime::Result<Val> {
607    let (sign, magnitude) = value.into_parts();
608    let limbs = write_limbs(&mut *caller, &magnitude.to_u64_digits())?;
609    let intr = intrinsic_types(&mut *caller)?;
610    let vtable = crate::runtime::host::host_bigint_vtable(caller)?;
611    let pre = StructRefPre::new(&mut *caller, intr.bigint.clone());
612    let st = StructRef::new(
613        &mut *caller,
614        &pre,
615        &[
616            vtable,
617            Val::I32(sign_to_i32(sign)),
618            Val::AnyRef(Some(limbs.to_anyref())),
619        ],
620    )?;
621    Ok(Val::AnyRef(Some(st.to_anyref())))
622}
623
624pub(crate) fn sign_to_i32(sign: num_bigint::Sign) -> i32 {
625    match sign {
626        num_bigint::Sign::Minus => -1,
627        num_bigint::Sign::NoSign => 0,
628        num_bigint::Sign::Plus => 1,
629    }
630}
631
632#[cfg(test)]
633mod tests {
634    use super::*;
635
636    /// `div` and `mod` must stay on the synchronous wrapper. Nothing else pins
637    /// this: an async registration behaves identically end-to-end, so every
638    /// fixture would still pass if these two moved back. Calling the import
639    /// synchronously is the only way an embedder can observe its kind — an async
640    /// one refuses the call before the body ever runs.
641    #[test]
642    fn division_and_modulo_are_registered_synchronously() {
643        let cfg = crate::runtime::RuntimeConfig::default();
644        let engine = cfg.engine().expect("engine");
645        let data = StoreData::with_vfs(crate::runtime::Vfs::tempdir().expect("tempdir"));
646        let mut store = cfg.store_async(&engine, data).expect("store");
647        let mut linker = Linker::<StoreData>::new(&engine);
648        crate::runtime::host::install_host_functions(&mut linker).expect("install");
649
650        // A zero divisor short-circuits before any arithmetic, so an empty
651        // magnitude on both operands is enough to reach the guard.
652        let zero = write_limbs(&mut store, &[]).expect("limbs");
653        let args = [
654            Val::I32(0),
655            Val::AnyRef(Some(zero.to_anyref())),
656            Val::I32(0),
657            Val::AnyRef(Some(zero.to_anyref())),
658        ];
659
660        for name in ["div", "mod"] {
661            let mangled = crate::mangle::host(BIGINT_MODULE_NAME, name);
662            let func = match linker
663                .get(&mut store, BIGINT_MODULE_NAME, mangled.as_str())
664                .unwrap_or_else(|e| panic!("{name} resolves: {e}"))
665            {
666                wasmtime::Extern::Func(func) => func,
667                other => panic!("expected a func for {name}, got {other:?}"),
668            };
669            let mut results = [Val::I32(0), Val::AnyRef(None)];
670            let err = func
671                .call(&mut store, &args, &mut results)
672                .expect_err("divide by zero");
673            assert!(
674                format!("{err}").contains("Division by zero"),
675                "{name} must run synchronously; got: {err}"
676            );
677        }
678    }
679}
680
681fn read_sign(value: &Val) -> wasmtime::Result<i32> {
682    match value {
683        Val::I32(sign @ -1..=1) => Ok(*sign),
684        _ => Err(crate::runtime::host::invariant_trap(
685            "bigint: invalid canonical sign",
686        )),
687    }
688}
689
690#[cfg(test)]
691mod sign_tests {
692    use super::*;
693    #[test]
694    fn malformed_signs_trap_before_negation() {
695        for value in [
696            Val::I32(i32::MIN),
697            Val::I32(i32::MAX),
698            Val::I32(-2),
699            Val::I32(2),
700            Val::F64(0),
701        ] {
702            assert!(read_sign(&value).unwrap_err().is::<wasmtime::Trap>());
703        }
704        for sign in [-1, 0, 1] {
705            assert_eq!(-read_sign(&Val::I32(sign)).unwrap(), -sign);
706        }
707    }
708}