//! bignum slice 1 — arbitrary-precision `Int` WAT helpers for wasm-gc.
//!
//! wasm-gc is the last backend still wrapping `i64`. VM and Rust are
//! `Int = ℤ` via `aver-rt::AverInt`; wasm-gc emits WebAssembly GC
//! directly and does not link aver-rt, so the small-int-optimized
//! bignum lives here, IN the emitter, matching `aver-rt/src/int.rs`
//! semantics EXACTLY.
//!
//! ## Representation (mirrors `AverInt`)
//!
//! ```text
//! (struct $aint
//!   (field $small (mut i64))                    ;; value when $magf == null
//!   (field $magf  (mut (ref null (array i64))))  ;; magnitude limbs
//!   (field $sign  (mut i32)))                    ;; -1 / 0 / +1, Big only
//! ```
//!
//! - `$magf == null` → **Small**: value is `$small` (the i64 fast path).
//! - `$magf != null` → **Big**: `$sign ∈ {-1,+1}`, `$magf` the
//!   little-endian unsigned magnitude, canonical (no leading-zero
//!   limbs, top limb non-zero).
//!
//! ### Limb radix (an internal detail, invisible to the ABI)
//!
//! The struct field type is `(array i64)`, but each limb carries only
//! **32 bits** of magnitude (`0..2^32`). This is a deliberate
//! implementation choice confined to these helpers: with 32-bit limbs a
//! limb add `a+b+carry` and a limb multiply `a*b` both fit losslessly in
//! a 64-bit lane, so carry/borrow propagation is exact with no
//! add-with-carry primitive (wasm has none). The `$AverInt` struct shape
//! is identical to the plan's — only the radix used inside decompose /
//! recompose / arithmetic differs, and nothing outside this module ever
//! observes a limb.
//!
//! ## Canonical invariant (non-negotiable)
//!
//! Any value that fits `i64` is ALWAYS Small. Every helper that can
//! produce a Big funnels through the inlined normalize epilogue, which
//! demotes an in-range magnitude back to Small — mirroring
//! `AverInt::from_bigint`. Eq / Ord / (future) Hash depend on this.
//!
//! ## Single-function discipline (the trap)
//!
//! `compile_wat_helper` keeps only the FIRST function's body and
//! discards the rest, AND does not rewrite call indices. So every helper
//! is ONE function with NO `call` to any sibling — all limb routines are
//! inlined. A stray `call` would be a silent miscompile only wasmtime
//! catches.
//!
//! ## Overflow detection on the i64 fast path (no wasm flag)
//!
//! - ADD: `(a^s) & (b^s) < 0`, `s = a+b`.
//! - SUB: `(a^b) & (a^d) < 0`, `d = a-b`.
//! - MUL: round-trip-divide oracle `p == 0 || (a != 0 && p/a == b)`,
//!   guarding `a == 0` and the `a == -1 && b == i64::MIN` `div_s` trap.
//! - NEG: overflow only at `i64::MIN`.

use wasm_encoder::Function;

use super::super::WasmGcError;
use super::super::types::TypeRegistry;
use super::super::wat_helper;

/// The `$mag` / `$aint` type declarations a bignum helper WAT needs, at
/// the exact indices the user module's TypeRegistry recorded, so the
/// spliced-in body's `struct.new $aint` / `array.* $mag` reference the
/// right slots. `padding_types(mag_idx)` reserves `0..mag_idx`; the mag
/// array lands at `mag_idx` and the struct at `mag_idx + 1`.
fn aint_type_decls(registry: &TypeRegistry) -> Result<String, WasmGcError> {
    let mag_idx = registry.aint_mag_array_idx.ok_or(WasmGcError::Validation(
        "bignum helper needs the $AverInt magnitude array slot, but it wasn't allocated".into(),
    ))?;
    let struct_idx = registry.aint_struct_idx.ok_or(WasmGcError::Validation(
        "bignum helper needs the $AverInt struct slot, but it wasn't allocated".into(),
    ))?;
    debug_assert_eq!(struct_idx, mag_idx + 1, "struct must sit right above mag");
    let padding = wat_helper::padding_types(mag_idx);
    Ok(format!(
        "{padding}\
         (type $mag (array (mut i64)))\n\
         (type $aint (struct (field $small (mut i64)) \
                             (field $magf (mut (ref null $mag))) \
                             (field $sign (mut i32))))\n"
    ))
}

/// Decompose `$AverInt` operand local `$op` into a non-null 32-bit-limb
/// magnitude array local `$opm` (little-endian, leading zeros possible) and a
/// sign local `$ops` (-1/0/+1). When the shared `__aint_decompose` sub-routine
/// has a registered fn index, emits a `call` to it (its two results land into
/// `$opm` / `$ops`); otherwise inlines the body. `$umag` is an i64 scratch the
/// inline path uses.
fn decompose(registry: &TypeRegistry, op: &str, opm: &str, ops: &str, umag: &str) -> String {
    if let Some(idx) = registry.aint_decompose_fn_idx {
        // `call` pushes `(mag, sign)`; pop sign then mag into the caller locals.
        return format!(
            r#"
            (call {idx} (local.get ${op}))
            (local.set ${ops})
            (local.set ${opm}) "#
        );
    }
    decompose_inline(op, opm, ops, umag)
}

/// Inlined WAT: decompose `$AverInt` operand local `$op` into a non-null
/// 32-bit-limb magnitude array local `$opm` (little-endian, leading
/// zeros possible) and a sign local `$ops` (-1/0/+1). A Small splits
/// `|small|` into two 32-bit limbs; zero → sign 0 + empty magnitude.
/// `|i64::MIN|` is recovered via `0 - small` (wrapping), giving the
/// correct unsigned `2^63` whose low/high 32-bit halves are then split.
fn decompose_inline(op: &str, opm: &str, ops: &str, umag: &str) -> String {
    // $umag is an i64 scratch holding |small| during the split.
    format!(
        r#"
            (if (ref.is_null (struct.get $aint $magf (local.get ${op})))
              (then
                (if (i64.eqz (struct.get $aint $small (local.get ${op})))
                  (then
                    (local.set ${ops} (i32.const 0))
                    (local.set ${opm} (array.new_default $mag (i32.const 0))))
                  (else
                    (local.set ${ops}
                      (if (result i32) (i64.lt_s (struct.get $aint $small (local.get ${op})) (i64.const 0))
                        (then (i32.const -1)) (else (i32.const 1))))
                    (local.set ${umag}
                      (if (result i64) (i32.lt_s (local.get ${ops}) (i32.const 0))
                        (then (i64.sub (i64.const 0) (struct.get $aint $small (local.get ${op}))))
                        (else (struct.get $aint $small (local.get ${op})))))
                    (local.set ${opm} (array.new_default $mag (i32.const 2)))
                    (array.set $mag (local.get ${opm}) (i32.const 0)
                      (i64.and (local.get ${umag}) (i64.const 0xffffffff)))
                    (array.set $mag (local.get ${opm}) (i32.const 1)
                      (i64.shr_u (local.get ${umag}) (i64.const 32))))))
              (else
                (local.set ${opm} (struct.get $aint $magf (local.get ${op})))
                (local.set ${ops} (struct.get $aint $sign (local.get ${op}))))) "#
    )
}

/// Normalize epilogue. Takes a working 32-bit-limb magnitude array local
/// `$rm` (leading zeros allowed) and a raw sign local `$rs`, leaves a
/// canonical `$AverInt` on the stack. When the shared `__aint_normalize`
/// sub-routine has a registered fn index, emits a `call` to it; otherwise
/// inlines the body, whose scratch locals are the fixed `$rlen $i $lo $hi
/// $tmpm` (declared by every caller's `arith_locals`/`divmod_locals`).
fn normalize(registry: &TypeRegistry, rm: &str, rs: &str) -> String {
    if let Some(idx) = registry.aint_normalize_fn_idx {
        return format!("(call {idx} (local.get ${rm}) (local.get ${rs}))");
    }
    normalize_inline(rm, rs, "rlen", "i", "lo", "hi", "tmpm")
}

/// Inlined WAT normalize epilogue. Takes a working 32-bit-limb magnitude
/// array local `$rm` (leading zeros allowed) and a raw sign local `$rs`,
/// leaves a canonical `$AverInt` on the stack. Scratch: `$rlen $i $hi
/// $lo` (`$rlen $i` i32, `$hi $lo` i64) plus result-array local `$tmpm`.
///
/// Strips leading zeros; magnitude 0 → Small(0); a magnitude of ≤2 limbs
/// whose 64-bit value fits the signed-i64 range for `$rs` → Small (incl.
/// the lone `i64::MIN`); else a tight Big.
fn normalize_inline(
    rm: &str,
    rs: &str,
    rlen: &str,
    i: &str,
    lo: &str,
    hi: &str,
    tmpm: &str,
) -> String {
    format!(
        r#"
            ;; strip leading zero limbs
            (local.set ${rlen} (array.len (local.get ${rm})))
            (block $strip_done (loop $strip
              (br_if $strip_done (i32.eqz (local.get ${rlen})))
              (br_if $strip_done
                (i64.ne (array.get $mag (local.get ${rm}) (i32.sub (local.get ${rlen}) (i32.const 1))) (i64.const 0)))
              (local.set ${rlen} (i32.sub (local.get ${rlen}) (i32.const 1)))
              (br $strip)))
            (if (result (ref null $aint)) (i32.eqz (local.get ${rlen}))
              (then
                (struct.new $aint (i64.const 0) (ref.null $mag) (i32.const 0)))
              (else
                (if (result (ref null $aint)) (i32.le_u (local.get ${rlen}) (i32.const 2))
                  (then
                    ;; reassemble ≤2 limbs into a 64-bit unsigned value
                    (local.set ${lo} (i64.and (array.get $mag (local.get ${rm}) (i32.const 0)) (i64.const 0xffffffff)))
                    (local.set ${hi}
                      (if (result i64) (i32.eq (local.get ${rlen}) (i32.const 2))
                        (then (i64.and (array.get $mag (local.get ${rm}) (i32.const 1)) (i64.const 0xffffffff)))
                        (else (i64.const 0))))
                    (local.set ${lo} (i64.or (local.get ${lo}) (i64.shl (local.get ${hi}) (i64.const 32))))
                    ;; $lo now holds the full magnitude as an unsigned i64.
                    (if (result (ref null $aint))
                        (i64.eqz (i64.shr_u (local.get ${lo}) (i64.const 63)))
                      (then
                        ;; top bit clear → fits i64::MAX either sign → Small
                        (struct.new $aint
                          (if (result i64) (i32.lt_s (local.get ${rs}) (i32.const 0))
                            (then (i64.sub (i64.const 0) (local.get ${lo}))) (else (local.get ${lo})))
                          (ref.null $mag) (i32.const 0)))
                      (else
                        ;; top bit set: only -2^63 (i64::MIN) demotes.
                        (if (result (ref null $aint))
                            (i32.and (i32.lt_s (local.get ${rs}) (i32.const 0))
                                     (i64.eq (local.get ${lo}) (i64.const 0x8000000000000000)))
                          (then
                            (struct.new $aint (i64.const 0x8000000000000000) (ref.null $mag) (i32.const 0)))
                          (else
                            {tight_big})))))
                  (else
                    {tight_big})))) "#,
        tight_big = tight_big(rm, rs, rlen, i, tmpm),
    )
}

/// Inlined WAT that copies the surviving `$rlen` limbs of `$rm` into a
/// tight array `$tmpm` and builds a Big `$AverInt` (used by `normalize`
/// for the genuinely-out-of-range case). `$i` is i32 scratch.
fn tight_big(rm: &str, rs: &str, rlen: &str, i: &str, tmpm: &str) -> String {
    format!(
        r#"
                        (local.set ${tmpm} (array.new_default $mag (local.get ${rlen})))
                        (local.set ${i} (i32.const 0))
                        (block $copy_done (loop $copy
                          (br_if $copy_done (i32.ge_u (local.get ${i}) (local.get ${rlen})))
                          (array.set $mag (local.get ${tmpm}) (local.get ${i})
                            (array.get $mag (local.get ${rm}) (local.get ${i})))
                          (local.set ${i} (i32.add (local.get ${i}) (i32.const 1)))
                          (br $copy)))
                        (struct.new $aint (i64.const 0) (local.get ${tmpm})
                          (if (result i32) (i32.lt_s (local.get ${rs}) (i32.const 0)) (then (i32.const -1)) (else (i32.const 1)))) "#
    )
}

/// Unsigned-magnitude compare of arrays `$am` (stripped len `$alen`) and
/// `$bm` (stripped len `$blen`) → -1/0/1 into i32 `$cmp`. When the shared
/// `__aint_umag_cmp` sub-routine has a registered fn index, emits a `call`
/// to it; otherwise inlines the body (i32 scratch `$j`).
fn umag_cmp(
    registry: &TypeRegistry,
    am: &str,
    alen: &str,
    bm: &str,
    blen: &str,
    cmp: &str,
    j: &str,
) -> String {
    if let Some(idx) = registry.aint_umag_cmp_fn_idx {
        return format!(
            "(local.set ${cmp} (call {idx} (local.get ${am}) (local.get ${alen}) (local.get ${bm}) (local.get ${blen})))"
        );
    }
    umag_cmp_inline(am, alen, bm, blen, cmp, j)
}

/// Inlined unsigned-magnitude compare of arrays `$am` (stripped len
/// `$alen`) and `$bm` (stripped len `$blen`) → -1/0/1 into i32 `$cmp`.
/// i32 scratch `$j`.
fn umag_cmp_inline(am: &str, alen: &str, bm: &str, blen: &str, cmp: &str, j: &str) -> String {
    format!(
        r#"
            (if (i32.ne (local.get ${alen}) (local.get ${blen}))
              (then
                (local.set ${cmp}
                  (if (result i32) (i32.gt_u (local.get ${alen}) (local.get ${blen})) (then (i32.const 1)) (else (i32.const -1)))))
              (else
                (local.set ${cmp} (i32.const 0))
                (local.set ${j} (local.get ${alen}))
                (block $ucmp_done (loop $ucmp
                  (br_if $ucmp_done (i32.eqz (local.get ${j})))
                  (local.set ${j} (i32.sub (local.get ${j}) (i32.const 1)))
                  (if (i64.ne (array.get $mag (local.get ${am}) (local.get ${j}))
                              (array.get $mag (local.get ${bm}) (local.get ${j})))
                    (then
                      (local.set ${cmp}
                        (if (result i32) (i64.gt_u (array.get $mag (local.get ${am}) (local.get ${j}))
                                                   (array.get $mag (local.get ${bm}) (local.get ${j})))
                          (then (i32.const 1)) (else (i32.const -1))))
                      (br $ucmp_done)))
                  (br $ucmp))))) "#
    )
}

/// Type declarations for the bignum formatter, which references the
/// `$string` array slot AS WELL AS `$mag` / `$aint`. The registry
/// allocates `string_idx < mag_idx < struct_idx`, so we pad to each in
/// turn: `padding_types(n)` injects `n` empty `(type (struct))` so the
/// NEXT declared type lands at index `n`. Returns the full WAT prelude.
fn aint_and_string_decls(registry: &TypeRegistry) -> Result<String, WasmGcError> {
    let string_idx = registry
        .string_array_type_idx
        .ok_or(WasmGcError::Validation(
            "bignum String.fromInt needs the String slot, but it wasn't allocated".into(),
        ))?;
    let mag_idx = registry.aint_mag_array_idx.ok_or(WasmGcError::Validation(
        "bignum String.fromInt needs the $mag slot, but it wasn't allocated".into(),
    ))?;
    let struct_idx = registry.aint_struct_idx.ok_or(WasmGcError::Validation(
        "bignum String.fromInt needs the $aint slot, but it wasn't allocated".into(),
    ))?;
    if !(string_idx < mag_idx && mag_idx + 1 == struct_idx) {
        return Err(WasmGcError::Validation(format!(
            "bignum String.fromInt expects string({string_idx}) < mag({mag_idx}) and \
             struct({struct_idx}) == mag+1; layout invariant broken"
        )));
    }
    let pad_to_string = wat_helper::padding_types(string_idx);
    // After declaring $string we are at index string_idx+1; pad the gap
    // up to mag_idx, then declare $mag and $aint.
    let gap = wat_helper::padding_types(mag_idx - (string_idx + 1));
    Ok(format!(
        "{pad_to_string}\
         (type $string (array (mut i8)))\n\
         {gap}\
         (type $mag (array (mut i64)))\n\
         (type $aint (struct (field $small (mut i64)) \
                             (field $magf (mut (ref null $mag))) \
                             (field $sign (mut i32))))\n"
    ))
}

/// Type declarations for the bignum `Int.fromString` parser, which
/// references `$string`, `$mag`, `$aint` AND the `Result<Int,String>`
/// carrier struct (ok field is the `$aint` ref under bignum, err field
/// `$string`). The registry allocates `string_idx < mag_idx < struct_idx`
/// (struct = mag+1), and the result slot lands above all three (after any
/// intervening vector/list types). Pads each gap in turn so every named
/// type lands at the user module's exact idx.
fn aint_string_result_decls(registry: &TypeRegistry) -> Result<String, WasmGcError> {
    let string_idx = registry
        .string_array_type_idx
        .ok_or(WasmGcError::Validation(
            "bignum Int.fromString needs the String slot, but it wasn't allocated".into(),
        ))?;
    let mag_idx = registry.aint_mag_array_idx.ok_or(WasmGcError::Validation(
        "bignum Int.fromString needs the $mag slot, but it wasn't allocated".into(),
    ))?;
    let struct_idx = registry.aint_struct_idx.ok_or(WasmGcError::Validation(
        "bignum Int.fromString needs the $aint slot, but it wasn't allocated".into(),
    ))?;
    let result_idx =
        registry
            .result_type_idx("Result<Int,String>")
            .ok_or(WasmGcError::Validation(
                "bignum Int.fromString needs the Result<Int,String> slot, but it wasn't allocated"
                    .into(),
            ))?;
    if !(string_idx < mag_idx && mag_idx + 1 == struct_idx && struct_idx < result_idx) {
        return Err(WasmGcError::Validation(format!(
            "bignum Int.fromString expects string({string_idx}) < mag({mag_idx}), \
             struct({struct_idx}) == mag+1, and result({result_idx}) > struct; \
             layout invariant broken"
        )));
    }
    let pad_to_string = wat_helper::padding_types(string_idx);
    // After `$string` at string_idx we are at string_idx+1; pad to mag_idx,
    // declare `$mag` + `$aint`, then pad to result_idx and declare `$result`.
    let gap_to_mag = wat_helper::padding_types(mag_idx - (string_idx + 1));
    let gap_to_result = wat_helper::padding_types(result_idx - (struct_idx + 1));
    Ok(format!(
        "{pad_to_string}\
         (type $string (array (mut i8)))\n\
         {gap_to_mag}\
         (type $mag (array (mut i64)))\n\
         (type $aint (struct (field $small (mut i64)) \
                             (field $magf (mut (ref null $mag))) \
                             (field $sign (mut i32))))\n\
         {gap_to_result}\
         (type $result (struct (field (mut i32)) (field (mut (ref null $aint))) \
                               (field (mut (ref null $string)))))\n"
    ))
}

/// Inlined WAT that builds the VM-identical `Cannot parse '<s>' as Int`
/// error message into a fresh `$string` and leaves an `Err`
/// `(ref null $result)` on the stack. Mirrors `AverInt::from_str`'s
/// rejection wrapped by `Int::from_string` (`src/types/int.rs`). Reuses
/// the helper's `$len`/`$i`/`$err` locals; the prefix is `Cannot parse '`
/// (14 bytes) and the suffix `' as Int` (8 bytes).
fn from_string_err_build() -> String {
    // ASCII for "Cannot parse '" then the original string then "' as Int".
    let prefix: [u8; 14] = *b"Cannot parse '";
    let suffix: [u8; 8] = *b"' as Int";
    let mut set_prefix = String::new();
    for (i, b) in prefix.iter().enumerate() {
        set_prefix.push_str(&format!(
            "(array.set $string (local.get $err) (i32.const {i}) (i32.const {b}))\n"
        ));
    }
    let mut set_suffix = String::new();
    for (k, b) in suffix.iter().enumerate() {
        // suffix byte k lands at index 14 + len + k.
        set_suffix.push_str(&format!(
            "(array.set $string (local.get $err) (i32.add (i32.const {pos}) (local.get $len)) (i32.const {b}))\n",
            pos = 14 + k
        ));
    }
    format!(
        r#"
            (local.set $err (array.new_default $string (i32.add (local.get $len) (i32.const 22))))
            {set_prefix}
            ;; copy the original string into err[14..14+len]
            (array.copy $string $string (local.get $err) (i32.const 14)
              (local.get $s) (i32.const 0) (local.get $len))
            {set_suffix}
            (struct.new $result (i32.const 0) (ref.null $aint) (local.get $err))
        "#
    )
}

/// `Int.fromString(s) -> Result<Int, String>` under bignum. Accumulates
/// decimal digits into a 32-bit-limb magnitude (`mag = mag*10 + digit`)
/// then normalizes to a canonical `$AverInt`, so a 38-digit string parses
/// to the EXACT value (not the wrapped-i64 the scalar helper produces).
/// Acceptance matches the VM (`AverInt::from_str`): one optional leading
/// `+`/`-`, then `≥1` ASCII digit, no whitespace/garbage; on any
/// rejection returns `Err("Cannot parse '<s>' as Int")` byte-identically.
pub(super) fn emit_aint_from_string(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_string_result_decls(registry)?;
    let err_build = from_string_err_build();
    let norm = normalize(registry, "rm", "rs");
    let func_pad = func_pad(&[(registry.aint_normalize_fn_idx, NORMALIZE_SIG)]);
    let wat = format!(
        include_str!("wat/from_string.wat"),
        decls = decls,
        func_pad = func_pad,
        err_build = err_build,
        norm = norm,
    );
    wat_helper::compile_wat_helper(&wat)
}

/// `String.fromInt` under bignum — formats an `$AverInt` to a decimal
/// `(ref null $string)`. Small reads `$small` and runs the same i64
/// digit loop the scalar helper uses; Big repeatedly divides the 32-bit
/// limb magnitude by 10 (the ONLY divmod in slice 1, and it is by a
/// constant), collecting remainder digits low→high, then reverses them
/// into the output array with a leading `-` for negative sign.
pub(super) fn emit_string_from_aint(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_and_string_decls(registry)?;
    let wat = format!(include_str!("wat/string_from_aint.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// WAT signatures the shared sub-routines are `call`ed with, in `$`-typed
/// WAT form. Must match the `params`/`results` declared in `builtins/mod.rs`,
/// because the standalone helper module's placeholder `(func)` at each
/// sub-routine's real index is type-checked against the `call`.
const DECOMPOSE_SIG: &str = "(param (ref null $aint)) (result (ref null $mag) i32)";
const NORMALIZE_SIG: &str = "(param (ref null $mag)) (param i32) (result (ref null $aint))";
const STRIP_SIG: &str = "(param (ref null $mag)) (result i32)";
const UMAG_CMP_SIG: &str =
    "(param (ref null $mag)) (param i32) (param (ref null $mag)) (param i32) (result i32)";

/// Build the function-index scaffolding a multi-helper bignum WAT needs so its
/// `(call <real-idx>)` instructions validate in the standalone module. Only
/// the shared sub-routines the helper actually `call`s contribute a stub; a
/// `None` fn idx (bignum off, or a not-yet-factored sub-routine) is skipped —
/// the generator inlined the body in that case, so no `call` was emitted.
/// Returns the WAT fragment to splice right after `{decls}`. See
/// `wat_helper::func_placeholders` for the index/relocation contract.
fn func_pad(callees: &[(Option<u32>, &'static str)]) -> String {
    let stubs: Vec<wat_helper::CalleeStub> = callees
        .iter()
        .filter_map(|(idx, sig)| idx.map(|abs_idx| wat_helper::CalleeStub { abs_idx, sig }))
        .collect();
    wat_helper::func_placeholders(&stubs)
}

/// `__aint_decompose(a) -> (mag, sign)` — shared sub-routine. Splits an
/// `$AverInt` into a non-null 32-bit-limb magnitude (leading zeros possible)
/// and a sign (-1/0/+1), leaving both on the stack `(mag, sign)`. The body is
/// the same logic the inlined `decompose` generator emits, lifted into a
/// callable function so add/sub/mul/divmod/cmp `call` it once instead of each
/// carrying a private copy. A leaf (no inter-helper `call`).
pub(super) fn emit_aint_decompose(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/decompose.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_normalize(rm, rs) -> $AverInt` — shared sub-routine. Strips leading
/// zeros from a working magnitude, demotes an in-range value to Small (incl.
/// the lone `i64::MIN`) or builds a tight Big, leaving a canonical `$AverInt`.
/// Same logic the inlined `normalize` epilogue emits; a leaf.
pub(super) fn emit_aint_normalize(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(
        include_str!("wat/normalize.wat"),
        decls = decls,
        tight_big = tight_big("rm", "rs", "rlen", "i", "tmpm"),
    );
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_strip(arr) -> i32` — shared sub-routine. Count of limbs after
/// dropping leading-zero limbs. Same logic the inlined `strip` generator
/// emits; a leaf.
pub(super) fn emit_aint_strip(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/strip.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_umag_cmp(am, alen, bm, blen) -> i32` (-1/0/1) — shared sub-routine.
/// Unsigned magnitude compare of two stripped limb arrays. Same logic the
/// inlined `umag_cmp` generator emits; a leaf.
pub(super) fn emit_aint_umag_cmp(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/umag_cmp.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_from_i64(n: i64) -> $AverInt` — canonical Small constructor.
pub(super) fn emit_aint_from_i64(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/from_i64.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_to_f64(a) -> f64` — `Float.fromInt` under bignum. Small is an
/// exact `i64→f64` round; Big is CORRECTLY ROUNDED (round-to-nearest-even,
/// bit-identical to `num_bigint::BigInt::to_f64`) via the sticky-jam
/// technique: reduce the magnitude to its top 64 significant bits `M` with
/// shift `s = sigbits - 64`, OR every dropped lower bit into a sticky bit,
/// jam sticky into `M`'s LSB, `f64.convert_i64_u(M)`, then multiply by the
/// EXACT power of two `2^s` (saturating to `±inf` for magnitudes past f64
/// range). A naive high→low Horner accumulation (`acc = acc*2^32 + limb`)
/// double-rounds and drifts 1 ULP on ~10% of >=3-limb magnitudes — the
/// sticky-jam single-rounds, matching `AverInt::to_f64` (`aver-rt/src/int.rs`).
pub(super) fn emit_aint_to_f64(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/to_f64.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_from_f64(f) -> $AverInt` — `Int.fromFloat` under bignum.
/// Non-finite → `0`; an in-`i64`-range truncated value stays Small; an
/// out-of-range finite magnitude is built EXACTLY from the IEEE-754
/// mantissa/exponent (a pure left shift, since `|t| >= 2^63` forces a
/// non-negative exponent), matching `AverInt::from_f64_trunc`.
pub(super) fn emit_aint_from_f64(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let norm = normalize(registry, "rm", "rs");
    let func_pad = func_pad(&[(registry.aint_normalize_fn_idx, NORMALIZE_SIG)]);
    let wat = format!(
        include_str!("wat/from_f64.wat"),
        decls = decls,
        func_pad = func_pad,
        norm = norm
    );
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_to_index(a) -> i32` — extract a wasm array index from an
/// `$AverInt`. A Small in `[0, 2^31)` passes through; a negative /
/// `>= 2^31` Small or ANY Big returns the OOB sentinel `-1`, so a Big
/// index bounds-checks as out-of-range (the VM's `Option.None`) instead
/// of being `I32WrapI64`-truncated into a wrong in-range slot.
pub(super) fn emit_aint_to_index(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/to_index.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_to_i64_sat(a) -> i64` — saturating Int→i64 lowering for an Int
/// ARGUMENT into an i64-typed builtin slot. A Small passes its `$small`
/// through; a positive Big saturates to i64::MAX, a negative Big to
/// i64::MIN. Used where an out-of-i64 magnitude only needs to clamp to
/// "all"/"none" (`List.take`/`drop` counts) or past-end (`String.charAt`/
/// `slice` indices, `Char.fromCode`), which the receiving helper already
/// handles as a saturated boundary.
pub(super) fn emit_aint_to_i64_sat(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/to_i64_sat.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_to_i64_checked(a) -> i64` — CHECKED Int→i64 lowering for an Int
/// argument crossing the HOST EFFECT boundary (a `Random` bound,
/// `Time.sleep` ms, a network port, a `Terminal` coordinate). A Small
/// passes its `$small` through; a Big TRAPS (`unreachable`), since a
/// canonical Big is by definition outside i64 range. This mirrors the VM
/// host services' checked `AverInt::to_i64()` — which ERRORS on an out-of-
/// i64 effect arg — so an out-of-range effect arg REJECTS on wasm-gc
/// instead of silently saturating to i64::MAX/MIN and proceeding (e.g.
/// `Time.sleep(2^63)` would otherwise saturate to a ~292-million-year
/// hang). Self-contained: no inter-helper `call`, so
/// `compile_wat_helper`'s single-function discipline holds.
pub(super) fn emit_aint_to_i64_checked(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/to_i64_checked.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_neg(a) -> $AverInt`. Small fast path with `i64::MIN`
/// promotion; Big flips the sign field (magnitude unchanged, stays
/// canonical).
pub(super) fn emit_aint_neg(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/neg.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_abs(a) -> $AverInt` (slice 2). `|a|` over ℤ: a non-negative
/// value is unchanged; `|i64::MIN|` promotes to the Big `+2^63`; a
/// negative Big flips its sign field to `+1`. Mirrors `AverInt::abs`.
pub(super) fn emit_aint_abs(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/abs.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_eq(a, b) -> i32` (1/0). Leans on the canonical invariant:
/// equal Small ↔ equal $small; equal Big ↔ same sign + same limbs; a
/// Small and a Big are never equal.
pub(super) fn emit_aint_eq(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/eq.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_hash(a) -> i32`. Equal `$AverInt` values MUST hash equal —
/// the Eq/Hash agreement Map/Set keys and record/sum fields rely on. A
/// Small folds its i64 `$small` to i32 (`s ^ (s >>> 32)`, then wrap); a
/// Big DJB2-folds its 32-bit limb magnitude and mixes the sign. The
/// canonical invariant guarantees a Small and a Big are never equal, so
/// the two schemes need not agree across the boundary — only equal
/// Smalls (same `$small`) and equal Bigs (same sign + same limbs) must
/// collide, which they do. Self-contained: no inter-helper `call`, so
/// `compile_wat_helper`'s single-function discipline holds. Unlike the
/// slice-3 `$small`-field projection (where every Big collapses to
/// `$small == 0` and all Big keys collide into one bucket), this gives
/// Big keys a real distribution — relevant for `Map<Int, V>` / `Set<Int>`
/// holding many distinct Big keys.
pub(super) fn emit_aint_hash(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let wat = format!(include_str!("wat/hash.wat"), decls = decls);
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_cmp(a, b) -> i32` (-1/0/1). Sign first, then unsigned
/// magnitude (flipped for two negatives). Mirrors `AverInt::cmp`.
pub(super) fn emit_aint_cmp(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let decomp_a = decompose(registry, "a", "am", "as_", "umag");
    let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
    let strip_a = strip(registry, "am", "alen", "sa", "la");
    let strip_b = strip(registry, "bm", "blen", "sb", "lb");
    let cmp = umag_cmp(registry, "am", "alen", "bm", "blen", "cmp", "j");
    let func_pad = func_pad(&[
        (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
        (registry.aint_strip_fn_idx, STRIP_SIG),
        (registry.aint_umag_cmp_fn_idx, UMAG_CMP_SIG),
    ]);
    let wat = format!(
        include_str!("wat/cmp.wat"),
        decls = decls,
        func_pad = func_pad,
        decomp_a = decomp_a,
        decomp_b = decomp_b,
        strip_a = strip_a,
        strip_b = strip_b,
        cmp = cmp,
    );
    wat_helper::compile_wat_helper(&wat)
}

/// Leading-zero strip of array `$arr` into i32 len local `$len`. When the
/// shared `__aint_strip` sub-routine has a registered fn index, emits a `call`
/// to it; otherwise inlines a loop using unique block/loop labels `$bl`/`$lp`.
fn strip(registry: &TypeRegistry, arr: &str, len: &str, bl: &str, lp: &str) -> String {
    if let Some(idx) = registry.aint_strip_fn_idx {
        return format!("(local.set ${len} (call {idx} (local.get ${arr})))");
    }
    strip_inline(arr, len, bl, lp)
}

/// Inlined leading-zero strip of array `$arr` into i32 len local `$len`,
/// using unique block/loop label names `$bl`/`$lp`.
fn strip_inline(arr: &str, len: &str, bl: &str, lp: &str) -> String {
    format!(
        r#"
            (local.set ${len} (array.len (local.get ${arr})))
            (block ${bl} (loop ${lp}
              (br_if ${bl} (i32.eqz (local.get ${len})))
              (br_if ${bl} (i64.ne (array.get $mag (local.get ${arr}) (i32.sub (local.get ${len}) (i32.const 1))) (i64.const 0)))
              (local.set ${len} (i32.sub (local.get ${len}) (i32.const 1)))
              (br ${lp}))) "#
    )
}

/// Inlined signed-magnitude combine. Operands are decomposed locals
/// `(am, as_)` and `(bm, beff)` where `beff` is the EFFECTIVE sign of b
/// (caller passes `bs` for add, the negation `-bs` for sub). Produces a
/// working magnitude into `$rm` + raw sign into `$rs`, NOT normalized.
///
/// Assumes the caller has stripped `am`→`$alen`, `bm`→`$blen` and
/// declared scratch `$rlen $i $carry $cmp $j $borrow $diff` with the
/// types named below. 32-bit limbs make carry/borrow exact.
fn signed_combine(registry: &TypeRegistry) -> String {
    let cmp = umag_cmp(registry, "am", "alen", "bm", "blen", "cmp", "j");
    format!(
        r#"
            ;; signs "agree" when one is zero, or the two non-zero signs match.
            (if (i32.or (i32.eqz (local.get $as_))
                  (i32.or (i32.eqz (local.get $beff)) (i32.eq (local.get $as_) (local.get $beff))))
              (then
                ;; ── magnitude ADD ── rlen = max(alen,blen)+1
                (local.set $rlen (i32.add (i32.const 1)
                  (if (result i32) (i32.gt_u (local.get $alen) (local.get $blen)) (then (local.get $alen)) (else (local.get $blen)))))
                (local.set $rm (array.new_default $mag (local.get $rlen)))
                (local.set $i (i32.const 0))
                (local.set $carry (i64.const 0))
                (block $add_done (loop $add_lp
                  (br_if $add_done (i32.ge_u (local.get $i) (local.get $rlen)))
                  (local.set $carry (i64.add (local.get $carry)
                    (i64.add
                      (if (result i64) (i32.lt_u (local.get $i) (local.get $alen)) (then (array.get $mag (local.get $am) (local.get $i))) (else (i64.const 0)))
                      (if (result i64) (i32.lt_u (local.get $i) (local.get $blen)) (then (array.get $mag (local.get $bm) (local.get $i))) (else (i64.const 0))))))
                  (array.set $mag (local.get $rm) (local.get $i) (i64.and (local.get $carry) (i64.const 0xffffffff)))
                  (local.set $carry (i64.shr_u (local.get $carry) (i64.const 32)))
                  (local.set $i (i32.add (local.get $i) (i32.const 1)))
                  (br $add_lp)))
                (local.set $rs (if (result i32) (i32.eqz (local.get $as_)) (then (local.get $beff)) (else (local.get $as_)))))
              (else
                ;; ── magnitude SUBTRACT (differing signs) ── larger - smaller
                {cmp}
                (if (i32.eqz (local.get $cmp))
                  (then
                    (local.set $rm (array.new_default $mag (i32.const 0)))
                    (local.set $rs (i32.const 0)))
                  (else
                    ;; result sign = sign of the larger magnitude operand
                    (local.set $rs (if (result i32) (i32.gt_s (local.get $cmp) (i32.const 0)) (then (local.get $as_)) (else (local.get $beff))))
                    (local.set $rlen (if (result i32) (i32.gt_u (local.get $alen) (local.get $blen)) (then (local.get $alen)) (else (local.get $blen))))
                    (local.set $rm (array.new_default $mag (local.get $rlen)))
                    (local.set $i (i32.const 0))
                    (local.set $borrow (i64.const 0))
                    (block $sub_done (loop $sub_lp
                      (br_if $sub_done (i32.ge_u (local.get $i) (local.get $rlen)))
                      ;; diff = larger_limb - smaller_limb - borrow, in 64 bits.
                      ;; "larger" is am if cmp>0 else bm.
                      (local.set $diff (i64.sub
                        (i64.sub
                          (if (result i64) (i32.gt_s (local.get $cmp) (i32.const 0))
                            (then (if (result i64) (i32.lt_u (local.get $i) (local.get $alen)) (then (array.get $mag (local.get $am) (local.get $i))) (else (i64.const 0))))
                            (else (if (result i64) (i32.lt_u (local.get $i) (local.get $blen)) (then (array.get $mag (local.get $bm) (local.get $i))) (else (i64.const 0)))))
                          (if (result i64) (i32.gt_s (local.get $cmp) (i32.const 0))
                            (then (if (result i64) (i32.lt_u (local.get $i) (local.get $blen)) (then (array.get $mag (local.get $bm) (local.get $i))) (else (i64.const 0))))
                            (else (if (result i64) (i32.lt_u (local.get $i) (local.get $alen)) (then (array.get $mag (local.get $am) (local.get $i))) (else (i64.const 0))))))
                        (local.get $borrow)))
                      ;; if diff < 0 (as signed) add 2^32 and set borrow=1
                      (if (i64.lt_s (local.get $diff) (i64.const 0))
                        (then
                          (local.set $diff (i64.add (local.get $diff) (i64.const 0x100000000)))
                          (local.set $borrow (i64.const 1)))
                        (else (local.set $borrow (i64.const 0))))
                      (array.set $mag (local.get $rm) (local.get $i) (i64.and (local.get $diff) (i64.const 0xffffffff)))
                      (local.set $i (i32.add (local.get $i) (i32.const 1)))
                      (br $sub_lp)))))))
    "#
    )
}

/// Common locals declaration shared by add/sub: decomposed operands,
/// lengths, carry/borrow scratch, and normalize scratch.
fn arith_locals() -> &'static str {
    r#"
            (local $am (ref null $mag)) (local $as_ i32)
            (local $bm (ref null $mag)) (local $bs i32) (local $beff i32)
            (local $alen i32) (local $blen i32) (local $rlen i32)
            (local $rm (ref null $mag)) (local $rs i32)
            (local $i i32) (local $j i32) (local $cmp i32)
            (local $carry i64) (local $borrow i64) (local $diff i64) (local $umag i64)
            (local $lo i64) (local $hi i64) (local $tmpm (ref null $mag)) "#
}

/// `__aint_add(a, b) -> $AverInt`. i64 fast path with `(a^s)&(b^s)<0`
/// overflow detection; on overflow OR either operand Big, decompose +
/// signed-magnitude add + normalize.
pub(super) fn emit_aint_add(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    emit_aint_addsub(registry, false)
}

/// `__aint_sub(a, b) -> $AverInt`. i64 fast path with `(a^b)&(a^d)<0`
/// overflow detection; slow path negates b's effective sign.
pub(super) fn emit_aint_sub(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    emit_aint_addsub(registry, true)
}

fn emit_aint_addsub(registry: &TypeRegistry, is_sub: bool) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let locals = arith_locals();
    let decomp_a = decompose(registry, "a", "am", "as_", "umag");
    let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
    let strip_a = strip(registry, "am", "alen", "sa", "la");
    let strip_b = strip(registry, "bm", "blen", "sb", "lb");
    let combine = signed_combine(registry);
    let norm = normalize(registry, "rm", "rs");
    let func_pad = func_pad(&[
        (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
        (registry.aint_strip_fn_idx, STRIP_SIG),
        (registry.aint_umag_cmp_fn_idx, UMAG_CMP_SIG),
        (registry.aint_normalize_fn_idx, NORMALIZE_SIG),
    ]);
    // i64 fast-path: both Small.
    let (fast_op, overflow_check) = if is_sub {
        (
            "(i64.sub (struct.get $aint $small (local.get $a)) (struct.get $aint $small (local.get $b)))",
            // (a^b) & (a^d) < 0, d = result
            "(i64.lt_s (i64.and (i64.xor (struct.get $aint $small (local.get $a)) (struct.get $aint $small (local.get $b))) (i64.xor (struct.get $aint $small (local.get $a)) (local.get $r))) (i64.const 0))",
        )
    } else {
        (
            "(i64.add (struct.get $aint $small (local.get $a)) (struct.get $aint $small (local.get $b)))",
            // (a^s) & (b^s) < 0, s = result
            "(i64.lt_s (i64.and (i64.xor (struct.get $aint $small (local.get $a)) (local.get $r)) (i64.xor (struct.get $aint $small (local.get $b)) (local.get $r))) (i64.const 0))",
        )
    };
    let beff_set = if is_sub {
        "(local.set $beff (i32.sub (i32.const 0) (local.get $bs)))"
    } else {
        "(local.set $beff (local.get $bs))"
    };
    let wat = format!(
        include_str!("wat/addsub.wat"),
        decls = decls,
        func_pad = func_pad,
        locals = locals,
        decomp_a = decomp_a,
        decomp_b = decomp_b,
        beff_set = beff_set,
        strip_a = strip_a,
        strip_b = strip_b,
        combine = combine,
        norm = norm,
        fast_op = fast_op,
        overflow_check = overflow_check,
    );
    wat_helper::compile_wat_helper(&wat)
}

/// `__aint_mul(a, b) -> $AverInt`. i64 fast path with the round-trip-
/// divide overflow oracle (`p == 0 || (a != 0 && p/a == b)`), guarding
/// `a == 0` and the `a == -1 && b == i64::MIN` `div_s` trap. Slow path:
/// schoolbook 32-bit-limb magnitude multiply, sign = product of signs.
pub(super) fn emit_aint_mul(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let locals = arith_locals();
    let decomp_a = decompose(registry, "a", "am", "as_", "umag");
    let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
    let strip_a = strip(registry, "am", "alen", "sa", "la");
    let strip_b = strip(registry, "bm", "blen", "sb", "lb");
    let norm = normalize(registry, "rm", "rs");
    let func_pad = func_pad(&[
        (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
        (registry.aint_strip_fn_idx, STRIP_SIG),
        (registry.aint_normalize_fn_idx, NORMALIZE_SIG),
    ]);
    let wat = format!(
        include_str!("wat/mul.wat"),
        decls = decls,
        func_pad = func_pad,
        locals = locals,
        decomp_a = decomp_a,
        decomp_b = decomp_b,
        strip_a = strip_a,
        strip_b = strip_b,
        norm = norm,
        mul_body = mul_magnitude(),
    );
    wat_helper::compile_wat_helper(&wat)
}

/// Locals for the divmod helper: decomposed operands + lengths, the
/// long-division working set (quotient `$qm`, working remainder `$rwm`
/// stripped to `$rwlen`, the bit/word/offset cursor), borrow/carry
/// scratch, the Euclidean-adjust output `$rwout`, and the normalize
/// scratch (`$rm $rs $rlen $lo $hi $tmpm`) shared by both result epilogues.
fn divmod_locals() -> &'static str {
    r#"
            (local $am (ref null $mag)) (local $as_ i32)
            (local $bm (ref null $mag)) (local $bs i32)
            (local $alen i32) (local $blen i32)
            (local $qm (ref null $mag)) (local $rwm (ref null $mag)) (local $rwlen i32)
            (local $rwout (ref null $mag)) (local $rm (ref null $mag)) (local $rs i32) (local $rlen i32)
            (local $bit i32) (local $word i32) (local $off i32)
            (local $i i32) (local $j i32) (local $cmp i32) (local $negadj i32)
            (local $carry i64) (local $acc i64) (local $borrow i64) (local $diff i64) (local $umag i64)
            (local $lo i64) (local $hi i64) (local $tmpm (ref null $mag)) "#
}

/// `__aint_divmod(a, b, want_mod) -> $AverInt`. Euclidean division
/// (`want_mod == 0`) or modulo (`want_mod != 0`), matching
/// `aver-rt::AverInt::{div,rem}_euclid` and the VM EXACTLY: the
/// remainder is always in `[0, |b|)`. The b == 0 partiality is handled
/// by the caller BEFORE this helper (the boxed `Int.div`/`Int.mod`
/// lowering), so this assumes `b != 0`.
///
/// Fast path: both operands Small AND not the `i64::MIN / -1` overflow
/// (whose ℤ quotient `+2^63` is a Big) — reuse the same i64 Euclidean
/// arithmetic the wrapping backend emits (`i64.div_s`/`i64.rem_s` plus
/// the rem<0 lift). Slow path: decompose, run unsigned shift-subtract
/// long division of the magnitudes to a truncating quotient+remainder,
/// then apply the sign rules + the Euclidean adjustment and normalize.
///
/// Euclidean lift (derived from `euclid_div_rem`): when the dividend is
/// negative and the truncating remainder is nonzero, the quotient's
/// magnitude is `|trunc_q| + 1` (sign = `as_ * bs`, unchanged) and the
/// remainder is `|b| - trunc_rem` (always non-negative). Otherwise the
/// truncating quotient/remainder magnitudes pass through with sign
/// `as_ * bs` / `as_` respectively. Both results renormalize.
pub(super) fn emit_aint_divmod(registry: &TypeRegistry) -> Result<Function, WasmGcError> {
    let decls = aint_type_decls(registry)?;
    let locals = divmod_locals();
    let decomp_a = decompose(registry, "a", "am", "as_", "umag");
    let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
    let strip_a = strip(registry, "am", "alen", "sa", "la");
    let strip_b = strip(registry, "bm", "blen", "sb", "lb");
    // r vs |b| during long division: $rwm (stripped $rwlen) ⋛ $bm ($blen).
    let cmp_r_b = umag_cmp(registry, "rwm", "rwlen", "bm", "blen", "cmp", "j");
    // Two normalize epilogues, one per result branch; they're in disjoint
    // `if` arms so reusing the same scratch locals is safe.
    let norm_q = normalize(registry, "rm", "rs");
    let norm_r = normalize(registry, "rm", "rs");
    let func_pad = func_pad(&[
        (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
        (registry.aint_strip_fn_idx, STRIP_SIG),
        (registry.aint_umag_cmp_fn_idx, UMAG_CMP_SIG),
        (registry.aint_normalize_fn_idx, NORMALIZE_SIG),
    ]);
    let wat = format!(
        include_str!("wat/divmod.wat"),
        decls = decls,
        func_pad = func_pad,
        locals = locals,
        decomp_a = decomp_a,
        decomp_b = decomp_b,
        strip_a = strip_a,
        strip_b = strip_b,
        cmp_r_b = cmp_r_b,
        norm_q = norm_q,
        norm_r = norm_r,
    );
    wat_helper::compile_wat_helper(&wat)
}

/// Inlined schoolbook 32-bit-limb magnitude multiply. Reads stripped
/// `am`/`$alen`, `bm`/`$blen`; writes the product magnitude into `$rm`
/// (len `$alen+$blen`) and the result sign into `$rs` (= as_ * bs, with
/// 0 when either is zero). Scratch i32 `$i $j $k`, i64 `$carry $prod`.
fn mul_magnitude() -> String {
    r#"
            ;; result sign: 0 if either operand zero, else product of signs.
            (local.set $rs (i32.mul (local.get $as_) (local.get $bs)))
            (local.set $rlen (i32.add (local.get $alen) (local.get $blen)))
            (if (i32.eqz (local.get $rlen)) (then (local.set $rlen (i32.const 1))))
            (local.set $rm (array.new_default $mag (local.get $rlen)))
            (local.set $i (i32.const 0))
            (block $mi_done (loop $mi
              (br_if $mi_done (i32.ge_u (local.get $i) (local.get $alen)))
              (local.set $carry (i64.const 0))
              (local.set $j (i32.const 0))
              (block $mj_done (loop $mj
                (br_if $mj_done (i32.ge_u (local.get $j) (local.get $blen)))
                (local.set $k (i32.add (local.get $i) (local.get $j)))
                ;; prod = am[i]*bm[j] + rm[k] + carry  (fits in 64 bits:
                ;; (2^32-1)^2 + 2*(2^32-1) < 2^64)
                (local.set $prod (i64.add
                  (i64.add
                    (i64.mul (array.get $mag (local.get $am) (local.get $i))
                             (array.get $mag (local.get $bm) (local.get $j)))
                    (array.get $mag (local.get $rm) (local.get $k)))
                  (local.get $carry)))
                (array.set $mag (local.get $rm) (local.get $k) (i64.and (local.get $prod) (i64.const 0xffffffff)))
                (local.set $carry (i64.shr_u (local.get $prod) (i64.const 32)))
                (local.set $j (i32.add (local.get $j) (i32.const 1)))
                (br $mj)))
              ;; ripple the final carry up from rm[i+blen], masking each
              ;; limb to 32 bits so no lane is left holding > 2^32-1.
              (local.set $k (i32.add (local.get $i) (local.get $blen)))
              (block $mc_done (loop $mc
                (br_if $mc_done (i64.eqz (local.get $carry)))
                (local.set $prod (i64.add (array.get $mag (local.get $rm) (local.get $k)) (local.get $carry)))
                (array.set $mag (local.get $rm) (local.get $k) (i64.and (local.get $prod) (i64.const 0xffffffff)))
                (local.set $carry (i64.shr_u (local.get $prod) (i64.const 32)))
                (local.set $k (i32.add (local.get $k) (i32.const 1)))
                (br $mc)))
              (local.set $i (i32.add (local.get $i) (i32.const 1)))
              (br $mi)))
    "#
    .to_string()
}

#[cfg(test)]
mod tests {
    use super::*;

    /// A `TypeRegistry` with the bignum slots wired to a self-consistent
    /// layout (`string=0 < mag=1`, `struct=mag+1=2`) — the layout
    /// invariant both `aint_type_decls` and `aint_and_string_decls`
    /// require. Built from an empty program so no parsing/resolution
    /// machinery is needed, then the four bignum/String index fields are
    /// overwritten directly (they are `pub(super)`, visible here).
    fn bignum_registry() -> TypeRegistry {
        bignum_registry_for_validation()
    }

    /// Same self-consistent bignum layout, re-exported to the
    /// `validation_guard` sibling module so its full-validate test renders
    /// helpers against an identical type section.
    pub(super) fn bignum_registry_for_validation() -> TypeRegistry {
        let mut registry = TypeRegistry::build_with_handler(&[], &[], false);
        registry.bignum = true;
        registry.string_array_type_idx = Some(0);
        registry.aint_mag_array_idx = Some(1);
        registry.aint_struct_idx = Some(2);
        // `Int.fromString` (slice 3) carries a `Result<Int,String>` whose
        // ok field is the `$aint` ref. Register it just above the struct
        // slot so `aint_string_result_decls` finds a valid layout.
        registry
            .result_types
            .insert("Result<Int,String>".to_string(), 3);
        registry.result_order.push("Result<Int,String>".to_string());
        registry
    }

    /// PARSE GUARD — the whole point of moving the WAT bodies into
    /// `.wat` files. Every leaf helper's template is formatted and run
    /// through `wat::parse_str` (inside `compile_wat_helper`). A stray
    /// `)`/`(` in any `.wat` file — the exact bug class hand-balancing
    /// parens in `r#"…"#` invited — fails HERE at `cargo test`, instead
    /// of only when a program happens to exercise that op in wasmtime.
    #[test]
    fn every_bignum_helper_wat_parses() {
        let registry = bignum_registry();

        // Each leaf helper emits a full `(module … (func "helper" …))`;
        // `compile_wat_helper` parses it, so `Ok` ⇒ the WAT is
        // syntactically valid and the locals/body extracted cleanly.
        let cases: &[(&str, fn(&TypeRegistry) -> Result<Function, WasmGcError>)] = &[
            ("emit_aint_from_i64", emit_aint_from_i64),
            ("emit_aint_neg", emit_aint_neg),
            ("emit_aint_abs", emit_aint_abs),
            ("emit_aint_eq", emit_aint_eq),
            ("emit_aint_hash", emit_aint_hash),
            ("emit_aint_cmp", emit_aint_cmp),
            ("emit_aint_add", emit_aint_add),
            ("emit_aint_sub", emit_aint_sub),
            ("emit_aint_mul", emit_aint_mul),
            ("emit_aint_divmod", emit_aint_divmod),
            ("emit_string_from_aint", emit_string_from_aint),
            ("emit_aint_from_string", emit_aint_from_string),
            ("emit_aint_to_f64", emit_aint_to_f64),
            ("emit_aint_from_f64", emit_aint_from_f64),
            ("emit_aint_to_index", emit_aint_to_index),
            // Shared sub-routines — their standalone modules must parse too.
            ("emit_aint_decompose", emit_aint_decompose),
            ("emit_aint_normalize", emit_aint_normalize),
            ("emit_aint_strip", emit_aint_strip),
            ("emit_aint_umag_cmp", emit_aint_umag_cmp),
        ];

        for (name, emit) in cases {
            let result = emit(&registry);
            assert!(
                result.is_ok(),
                "bignum helper `{name}` failed to emit/parse its WAT: {:?}",
                result.err()
            );
        }
    }
}

/// VALIDATION guard — stronger than `every_bignum_helper_wat_parses`.
/// The parse guard only runs `wat::parse_str` (syntax), so it accepts a
/// helper whose control flow type-checks WRONG — e.g. an
/// `(if (result (ref null $aint)) …)` whose arms leave nothing on the
/// stack, or a branch with a mismatched value type. Those are *validation*
/// errors wasmtime raises at instantiation, the exact class slice 2's
/// divmod hit twice (spurious result annotations; a stale length local).
/// Here every leaf helper's rendered module is run through the full
/// `wasmparser` validator (GC + tail-call features on), so a stack-type
/// bug in any `.wat` fails at `cargo test`, not only when a program
/// happens to divide in wasmtime.
#[cfg(test)]
mod validation_guard {
    use super::tests::bignum_registry_for_validation;
    use super::*;

    /// Validate the divmod + abs helpers (the slice-2 additions whose
    /// control flow is non-trivial) by rendering their full WAT module and
    /// running the binary validator. Renders via the same generators the
    /// emitter uses.
    #[test]
    fn slice2_helper_modules_validate() {
        let registry = bignum_registry_for_validation();

        let modules: Vec<(&str, String)> = vec![
            ("__aint_divmod", render_divmod(&registry)),
            (
                "__aint_abs",
                render_simple(&registry, include_str!("wat/abs.wat")),
            ),
            (
                "__aint_neg",
                render_simple(&registry, include_str!("wat/neg.wat")),
            ),
            // slice 3 — decimal parse / Float bridges / index extraction.
            // Their control flow (limb-accumulate loops, the error-message
            // builder, the ±inf saturating Float path) is exactly the class
            // the parse-only guard cannot catch, so validate the full module.
            ("__aint_from_string", render_from_string(&registry)),
            // slice 4 (eq+hash gap) — `__aint_hash` has a limb-fold loop +
            // a Small/Big branch returning i32 on both arms; validate the
            // full module so a stack-type slip fails at `cargo test`.
            (
                "__aint_hash",
                render_simple(&registry, include_str!("wat/hash.wat")),
            ),
            (
                "__aint_to_f64",
                render_simple(&registry, include_str!("wat/to_f64.wat")),
            ),
            ("__aint_from_f64", render_from_f64(&registry)),
            (
                "__aint_to_index",
                render_simple(&registry, include_str!("wat/to_index.wat")),
            ),
            // Shared sub-routines (size dedup) — their standalone modules
            // must VALIDATE: the demote-to-Small / build-Big branch
            // (`__aint_normalize`) returns `(ref null $aint)` on every arm, the
            // decompose splits return `(mag, sign)`, the strip/cmp return i32.
            // A paren slip or stack-type mismatch fails here, not only when a
            // program happens to hit the divide path in wasmtime.
            ("__aint_decompose", render_decompose(&registry)),
            ("__aint_normalize", render_normalize(&registry)),
            ("__aint_strip", render_strip(&registry)),
            ("__aint_umag_cmp", render_umag_cmp(&registry)),
        ];

        for (name, wat) in modules {
            let bytes = wat::parse_str(&wat)
                .unwrap_or_else(|e| panic!("helper `{name}` failed to parse: {e}"));
            let mut validator =
                wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
            validator
                .validate_all(&bytes)
                .unwrap_or_else(|e| panic!("helper `{name}` failed to VALIDATE: {e}"));
        }
    }

    /// CALL-PATH validate — the arithmetic helpers rendered with the `call`
    /// path ACTIVE. Setting the shared fn-index fields on the registry flips
    /// each generator from inlining to emitting `(call <idx>)` plus the
    /// `func_pad` placeholder scaffolding (`(func <sig> unreachable)` at each
    /// callee's index). The resulting STANDALONE module — placeholders at
    /// indices 0..=3 then the exported helper — is exactly what
    /// `compile_wat_helper` parses, and it self-validates: the `call <idx>`
    /// type-checks against the placeholder of matching signature. A wrong
    /// call-index/signature, a `func_pad` slot at the wrong index, or a stack
    /// mismatch in the `call` shim fails here — the bug class the inline-only
    /// standalone guards above structurally cannot see.
    #[test]
    fn shared_subroutine_call_path_validates() {
        let mut registry = bignum_registry_for_validation();
        // Pick non-trivial indices so an off-by-one in `func_pad`'s padding (or
        // in the lifted `call`'s LEB128 index) would mis-resolve and fail.
        registry.aint_decompose_fn_idx = Some(7);
        registry.aint_normalize_fn_idx = Some(8);
        registry.aint_strip_fn_idx = Some(9);
        registry.aint_umag_cmp_fn_idx = Some(10);

        // Each helper's full standalone module text (decls + func_pad +
        // exported helper), validated as a unit. `func_pad` declares placeholder
        // funcs up to index 10 so every `(call 7..10)` resolves to a func of the
        // right signature.
        let modules: Vec<(&str, String)> = vec![
            ("__aint_add", render_addsub(&registry, false)),
            ("__aint_sub", render_addsub(&registry, true)),
            ("__aint_mul", render_mul(&registry)),
            ("__aint_divmod", render_divmod(&registry)),
            ("__aint_cmp", render_cmp(&registry)),
            ("__aint_from_string", render_from_string(&registry)),
            ("__aint_from_f64", render_from_f64(&registry)),
        ];
        for (name, wat) in modules {
            let bytes = wat::parse_str(&wat)
                .unwrap_or_else(|e| panic!("call-path helper `{name}` failed to parse: {e}"));
            let mut validator =
                wasmparser::Validator::new_with_features(wasmparser::WasmFeatures::all());
            validator
                .validate_all(&bytes)
                .unwrap_or_else(|e| panic!("call-path helper `{name}` failed to VALIDATE: {e}"));
        }
    }

    fn render_addsub(registry: &TypeRegistry, is_sub: bool) -> String {
        let decls = aint_type_decls(registry).unwrap();
        let locals = arith_locals();
        let decomp_a = decompose(registry, "a", "am", "as_", "umag");
        let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
        let strip_a = strip(registry, "am", "alen", "sa", "la");
        let strip_b = strip(registry, "bm", "blen", "sb", "lb");
        let combine = signed_combine(registry);
        let norm = normalize(registry, "rm", "rs");
        let func_pad = func_pad(&[
            (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
            (registry.aint_strip_fn_idx, STRIP_SIG),
            (registry.aint_umag_cmp_fn_idx, UMAG_CMP_SIG),
            (registry.aint_normalize_fn_idx, NORMALIZE_SIG),
        ]);
        let (fast_op, overflow_check) = if is_sub {
            (
                "(i64.sub (struct.get $aint $small (local.get $a)) (struct.get $aint $small (local.get $b)))",
                "(i64.lt_s (i64.and (i64.xor (struct.get $aint $small (local.get $a)) (struct.get $aint $small (local.get $b))) (i64.xor (struct.get $aint $small (local.get $a)) (local.get $r))) (i64.const 0))",
            )
        } else {
            (
                "(i64.add (struct.get $aint $small (local.get $a)) (struct.get $aint $small (local.get $b)))",
                "(i64.lt_s (i64.and (i64.xor (struct.get $aint $small (local.get $a)) (local.get $r)) (i64.xor (struct.get $aint $small (local.get $b)) (local.get $r))) (i64.const 0))",
            )
        };
        let beff_set = if is_sub {
            "(local.set $beff (i32.sub (i32.const 0) (local.get $bs)))"
        } else {
            "(local.set $beff (local.get $bs))"
        };
        format!(
            include_str!("wat/addsub.wat"),
            decls = decls,
            func_pad = func_pad,
            locals = locals,
            decomp_a = decomp_a,
            decomp_b = decomp_b,
            beff_set = beff_set,
            strip_a = strip_a,
            strip_b = strip_b,
            combine = combine,
            norm = norm,
            fast_op = fast_op,
            overflow_check = overflow_check,
        )
    }

    fn render_mul(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        let locals = arith_locals();
        let decomp_a = decompose(registry, "a", "am", "as_", "umag");
        let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
        let strip_a = strip(registry, "am", "alen", "sa", "la");
        let strip_b = strip(registry, "bm", "blen", "sb", "lb");
        let norm = normalize(registry, "rm", "rs");
        let func_pad = func_pad(&[
            (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
            (registry.aint_strip_fn_idx, STRIP_SIG),
            (registry.aint_normalize_fn_idx, NORMALIZE_SIG),
        ]);
        format!(
            include_str!("wat/mul.wat"),
            decls = decls,
            func_pad = func_pad,
            locals = locals,
            decomp_a = decomp_a,
            decomp_b = decomp_b,
            strip_a = strip_a,
            strip_b = strip_b,
            norm = norm,
            mul_body = mul_magnitude(),
        )
    }

    fn render_cmp(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        let decomp_a = decompose(registry, "a", "am", "as_", "umag");
        let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
        let strip_a = strip(registry, "am", "alen", "sa", "la");
        let strip_b = strip(registry, "bm", "blen", "sb", "lb");
        let cmp = umag_cmp(registry, "am", "alen", "bm", "blen", "cmp", "j");
        let func_pad = func_pad(&[
            (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
            (registry.aint_strip_fn_idx, STRIP_SIG),
            (registry.aint_umag_cmp_fn_idx, UMAG_CMP_SIG),
        ]);
        format!(
            include_str!("wat/cmp.wat"),
            decls = decls,
            func_pad = func_pad,
            decomp_a = decomp_a,
            decomp_b = decomp_b,
            strip_a = strip_a,
            strip_b = strip_b,
            cmp = cmp,
        )
    }

    fn render_decompose(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        format!(include_str!("wat/decompose.wat"), decls = decls)
    }

    fn render_normalize(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        format!(
            include_str!("wat/normalize.wat"),
            decls = decls,
            tight_big = tight_big("rm", "rs", "rlen", "i", "tmpm"),
        )
    }

    fn render_strip(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        format!(include_str!("wat/strip.wat"), decls = decls)
    }

    fn render_umag_cmp(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        format!(include_str!("wat/umag_cmp.wat"), decls = decls)
    }

    fn render_simple(registry: &TypeRegistry, template: &str) -> String {
        let decls = aint_type_decls(registry).unwrap();
        template.replace("{decls}", &decls)
    }

    fn render_from_string(registry: &TypeRegistry) -> String {
        let decls = aint_string_result_decls(registry).unwrap();
        let err_build = from_string_err_build();
        let norm = normalize(registry, "rm", "rs");
        let func_pad = func_pad(&[(registry.aint_normalize_fn_idx, NORMALIZE_SIG)]);
        format!(
            include_str!("wat/from_string.wat"),
            decls = decls,
            func_pad = func_pad,
            err_build = err_build,
            norm = norm,
        )
    }

    fn render_from_f64(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        let norm = normalize(registry, "rm", "rs");
        let func_pad = func_pad(&[(registry.aint_normalize_fn_idx, NORMALIZE_SIG)]);
        format!(
            include_str!("wat/from_f64.wat"),
            decls = decls,
            func_pad = func_pad,
            norm = norm
        )
    }

    fn render_divmod(registry: &TypeRegistry) -> String {
        let decls = aint_type_decls(registry).unwrap();
        let locals = divmod_locals();
        let decomp_a = decompose(registry, "a", "am", "as_", "umag");
        let decomp_b = decompose(registry, "b", "bm", "bs", "umag");
        let strip_a = strip(registry, "am", "alen", "sa", "la");
        let strip_b = strip(registry, "bm", "blen", "sb", "lb");
        let cmp_r_b = umag_cmp(registry, "rwm", "rwlen", "bm", "blen", "cmp", "j");
        let norm_q = normalize(registry, "rm", "rs");
        let norm_r = normalize(registry, "rm", "rs");
        let func_pad = func_pad(&[
            (registry.aint_decompose_fn_idx, DECOMPOSE_SIG),
            (registry.aint_strip_fn_idx, STRIP_SIG),
            (registry.aint_umag_cmp_fn_idx, UMAG_CMP_SIG),
            (registry.aint_normalize_fn_idx, NORMALIZE_SIG),
        ]);
        format!(
            include_str!("wat/divmod.wat"),
            decls = decls,
            func_pad = func_pad,
            locals = locals,
            decomp_a = decomp_a,
            decomp_b = decomp_b,
            strip_a = strip_a,
            strip_b = strip_b,
            cmp_r_b = cmp_r_b,
            norm_q = norm_q,
            norm_r = norm_r,
        )
    }
}
