@noPervasives
module Numbers

from "runtime/unsafe/memory" include Memory
from "runtime/unsafe/tags" include Tags
from "runtime/exception" include Exception
from "runtime/bigint" include Bigint as BI

from "runtime/unsafe/constants" include Constants
use Constants.{
  _SMIN8_I32,
  _SMAX8_I32,
  _SMIN8_I64,
  _SMAX8_I64,
  _SMIN16_I32,
  _SMAX16_I32,
  _SMIN16_I64,
  _SMAX16_I64,
  _UMAX8_I32,
  _UMAX8_I64,
  _UMAX16_I32,
  _UMAX16_I64,
  _SMAX32_I64 as _I32_MAX,
  _SMIN32_I64 as _I32_MIN,
  _SMAX_I64 as _I64_MAX,
  _SMIN_I64 as _I64_MIN,
  _UMAX32_I64 as _U32_MAX,
  _UMIN32_I64 as _U32_MIN,
}
from "runtime/unsafe/wasmi32" include WasmI32
from "runtime/unsafe/wasmi64" include WasmI64
from "runtime/unsafe/wasmf32" include WasmF32
from "runtime/unsafe/wasmf64" include WasmF64

primitive (!) = "@not"
primitive (&&) = "@and"
primitive (||) = "@or"
primitive throw = "@throw"
primitive ignore = "@ignore"

exception UnknownNumberTag
exception InvariantViolation

from "runtime/dataStructures" include DataStructures
use DataStructures.{
  newRational,
  newInt32,
  newInt64,
  newFloat32,
  newFloat64,
  tagInt8,
  tagInt16,
  tagUint8,
  tagUint16,
  untagInt8,
  untagInt16,
  untagUint8,
  untagUint16,
}

@unsafe
let _F32_MAX = 3.40282347e+38W
@unsafe
let _F32_MIN = 1.401298464324817e-45W
@unsafe
let _F32_MAX_SAFE_INTEGER = 16777215.0w
@unsafe
let _F64_MAX_SAFE_INTEGER = 9007199254740991.0W

use WasmI32.{ (==), (!=), (^), (<<), (>>) }

@unsafe
provide let tagSimple = x => {
  x << 1n ^ 1n
}

@unsafe
let untagSimple = x => {
  x >> 1n
}

@unsafe
let isSimpleNumber = x => {
  use WasmI32.{ (&) }
  (x & Tags._GRAIN_NUMBER_TAG_MASK) == Tags._GRAIN_NUMBER_TAG_TYPE
}

@unsafe
provide let isBoxedNumber = x => {
  use WasmI32.{ (&) }
  if ((x & Tags._GRAIN_GENERIC_TAG_MASK) == Tags._GRAIN_GENERIC_HEAP_TAG_TYPE) {
    WasmI32.load(x, 0n) == Tags._GRAIN_BOXED_NUM_HEAP_TAG
  } else {
    false
  }
}

@unsafe
provide let isFloat = x => {
  if (isBoxedNumber(x)) {
    let tag = WasmI32.load(x, 4n)
    tag == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG
  } else {
    false
  }
}

@unsafe
provide let isInteger = x => {
  if (isBoxedNumber(x)) {
    let tag = WasmI32.load(x, 4n)
    tag == Tags._GRAIN_INT64_BOXED_NUM_TAG
      || tag == Tags._GRAIN_BIGINT_BOXED_NUM_TAG
  } else {
    true
  }
}

@unsafe
provide let isRational = x => {
  if (isBoxedNumber(x)) {
    let tag = WasmI32.load(x, 4n)
    tag == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG
  } else {
    false
  }
}

@unsafe
provide let isNaN = x => {
  use WasmF64.{ (!=) }
  if (isBoxedNumber(x)) {
    // Boxed numbers can have multiple subtypes, of which float64 can be NaN.
    let tag = WasmI32.load(x, 4n)
    if (tag == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG) {
      // uses the fact that NaN is the only number not equal to itself
      let wf64 = WasmF64.load(x, 8n)
      wf64 != wf64
    } else {
      // Neither rational numbers nor boxed integers can be infinite or NaN.
      // Grain doesn't allow creating a rational with denominator of zero either.
      false
    }
  } else {
    // Simple numbers are integers and cannot be NaN.
    false
  }
}

@unsafe
let isBigInt = x => {
  if (isBoxedNumber(x)) {
    let tag = WasmI32.load(x, 4n)
    tag == Tags._GRAIN_BIGINT_BOXED_NUM_TAG
  } else {
    false
  }
}

@unsafe
provide let isNumber = x => {
  // x is a number if it is a literal number or a boxed_num heap value
  isSimpleNumber(x) || isBoxedNumber(x)
}

@unsafe
let safeI64toI32 = x => {
  use WasmI64.{ (<), (>) }
  if (x > _I32_MAX || x < _I32_MIN) {
    throw Exception.Overflow
  } else {
    WasmI32.wrapI64(x)
  }
}

@unsafe
let i32neg = x => {
  use WasmI32.{ (-) }
  0n - x
}

@unsafe
let i64not = x => {
  use WasmI64.{ (^) }
  x ^ 0xffffffffffffffffN
}
@unsafe
let i64neg = x => {
  use WasmI64.{ (-) }
  0N - x
}

// https://en.wikipedia.org/wiki/Binary_GCD_algorithm
@unsafe
let rec gcdHelp = (x, y) => {
  use WasmI64.{ (==), (!=), (&), (-), (<<), (>>), (>) }
  if (x == y || WasmI64.eqz(x)) {
    y
  } else if (WasmI64.eqz(y)) {
    x
  } else if ((i64not(x) & 1N) != 0N) {
    // x is even
    if ((y & 1N) != 0N) {
      // y is odd
      gcdHelp(x >> 1N, y)
    } else {
      gcdHelp(x >> 1N, y >> 1N) << 1N
    }
  } else if ((i64not(y) & 1N) != 0N) {
    // y is even and x is odd
    gcdHelp(x, y >> 1N)
  } else if (x > y) {
    gcdHelp(x - y, y)
  } else {
    gcdHelp(y - x, x)
  }
}

@unsafe
let gcd = (x, y) => {
  use WasmI64.{ (<) }
  // Algorithm above breaks on negatives, so
  // we make sure that they are positive at the beginning
  let x = if (x < 0N) {
    i64neg(x)
  } else {
    x
  }
  let y = if (y < 0N) {
    i64neg(y)
  } else {
    y
  }
  gcdHelp(x, y)
}

@unsafe
let gcd32 = (x, y) => {
  WasmI32.wrapI64(gcd(WasmI64.extendI32S(x), WasmI64.extendI32S(y)))
}

@unsafe
provide let reducedInteger = x => {
  use WasmI64.{ (>>), (<), (>) }
  // TODO(#1736): Remove the formatter ignore when parsing is fixed
  //formatter-ignore
  if (x > (_I32_MAX >> 1N) || x < (_I32_MIN >> 1N)) {
    newInt64(x)
  } else {
    tagSimple(WasmI32.wrapI64(x))
  }
}

@unsafe
provide let reducedUnsignedInteger = x => {
  use WasmI64.{ (>>>) }
  if (WasmI64.gtU(x, _I64_MAX)) {
    BI.makeWrappedUint64(x)
  } else if (WasmI64.gtU(x, _I32_MAX >>> 1N)) {
    newInt64(x)
  } else {
    tagSimple(WasmI32.wrapI64(x))
  }
}

@unsafe
let reducedBigInteger = x => {
  if (BI.canConvertToInt64(x)) {
    // CONVENTION: We assume that this function is called in
    //             some sort of tail position, meaning that
    //             the original input is no longer used after
    //             this function returns.
    let ret = reducedInteger(BI.toInt64(x))
    Memory.decRef(x)
    ret
  } else {
    x
  }
}

@unsafe
let reducedFractionBigInt = (x, y, keepRational) => {
  let mut x = x
  let mut y = y
  let mut needsDecref = false

  if (BI.isNegative(y)) {
    // Normalization 1: Never do negative/negative
    // Normalization 2: Never allow a negative denominator
    needsDecref = true
    x = BI.negate(x)
    y = BI.negate(y)
  }
  if (BI.eqz(y)) {
    throw Exception.DivisionByZero
  }
  let quotremResult = Memory.malloc(8n)
  BI.quotRem(x, y, quotremResult)
  // Note that the contents of quotRem are malloc'ed
  // inside of quotRem and need to be manually freed.
  let q = WasmI32.load(quotremResult, 0n)
  let r = WasmI32.load(quotremResult, 4n)
  // free container used to store quotrem result
  Memory.free(quotremResult)
  let ret = if (!keepRational && BI.eqz(r)) {
    // if remainder is zero, then return the quotient.
    // We decRef the remainder, since we no longer need it
    Memory.decRef(r)
    reducedBigInteger(q)
  } else {
    // remainder is nonzero. we don't need the quotient and
    // remainder anymore, so we discard them.
    Memory.decRef(q)
    Memory.decRef(r)
    let factor = BI.gcd(x, y)
    let xdiv = BI.div(x, factor)
    let ydiv = BI.div(y, factor)
    let ret = newRational(xdiv, ydiv)
    Memory.decRef(factor)
    ret
  }
  if (needsDecref) {
    Memory.decRef(x)
    Memory.decRef(y)
    void
  }
  ret
}

@unsafe
let reducedFraction64 = (x, y) => {
  use WasmI64.{ (/), (<) }
  let mut x = x
  let mut y = y

  if (y < 0N) {
    // Normalization 1: Never do negative/negative
    // Normalization 2: Never allow a negative denominator
    x = i64neg(x)
    y = i64neg(y)
  }
  if (WasmI64.eqz(y)) {
    throw Exception.DivisionByZero
  }
  if (WasmI64.eqz(WasmI64.remS(x, y))) {
    reducedInteger(x / y)
  } else {
    let factor = gcd(x, y)
    let xdiv = x / factor
    let ydiv = y / factor
    newRational(BI.makeWrappedInt64(xdiv), BI.makeWrappedInt64(ydiv))
  }
}

// Accessor functions

/* Memory Layout:
 * [GRAIN_BOXED_NUM_HEAP_TAG , <boxed_num tag> , <number-specific payload>...]
 * (payload depends on boxed_num tag...see below)
 *
 * Payloads:
 * For Int32:
 * [number]
 *
 * For Int64:
 * [number: i64]
 *
 * For Float32:
 * [number: f32]
 *
 * For Float64:
 * [number: f64]
 *
 * For Rational:
 * [numerator, denominator]
 */

@unsafe
provide let boxedNumberTag = xptr => {
  WasmI32.load(xptr, 4n)
}

@unsafe
provide let boxedInt64Number = xptr => {
  WasmI64.load(xptr, 8n)
}

@unsafe
provide let boxedFloat64Number = xptr => {
  WasmF64.load(xptr, 8n)
}

@unsafe
provide let boxedRationalNumerator = xptr => {
  WasmI32.load(xptr, 8n)
}

@unsafe
provide let boxedRationalDenominator = xptr => {
  WasmI32.load(xptr, 12n)
}

@unsafe
provide let coerceNumberToWasmF32 = (x: Number) => {
  let xVal = WasmI32.fromGrain(x)
  let result = if (isSimpleNumber(xVal)) {
    WasmF32.convertI32S(untagSimple(xVal))
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        WasmF32.convertI64S(boxedInt64Number(xVal))
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toFloat32(xVal)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        use WasmF32.{ (/) }
        BI.toFloat32(boxedRationalNumerator(xVal))
          / BI.toFloat32(boxedRationalDenominator(xVal))
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (<), (>) }
        let boxedVal = boxedFloat64Number(xVal)
        if (boxedVal > _F32_MAX || boxedVal < _F32_MIN) {
          // Not an actual return value
          throw Exception.Overflow
        } else {
          WasmF32.demoteF64(boxedVal)
        }
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
  ignore(x)
  result
}

@unsafe
provide let coerceNumberToWasmF64 = (x: Number) => {
  let xVal = WasmI32.fromGrain(x)
  let result = if (isSimpleNumber(xVal)) {
    WasmF64.convertI32S(untagSimple(xVal))
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        WasmF64.convertI64S(boxedInt64Number(xVal))
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toFloat64(xVal)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        use WasmF64.{ (/) }
        BI.toFloat64(boxedRationalNumerator(xVal))
          / BI.toFloat64(boxedRationalDenominator(xVal))
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        boxedFloat64Number(xVal)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
  ignore(x)
  result
}

@unsafe
provide let coerceNumberToWasmI64 = (x: Number) => {
  let xVal = WasmI32.fromGrain(x)
  let result = if (isSimpleNumber(xVal)) {
    WasmI64.extendI32S(untagSimple(xVal))
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        boxedInt64Number(xVal)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toInt64(xVal)
      },
      _ => {
        // rationals are never integral, and we refuse to coerce floats to ints
        throw Exception.NumberNotIntlike
      },
    }
  }
  ignore(x)
  result
}

@unsafe
provide let coerceNumberToWasmI32 = (x: Number) => {
  use WasmI64.{ (<), (>) }
  let xVal = WasmI32.fromGrain(x)
  let result = if (isSimpleNumber(xVal)) {
    untagSimple(xVal)
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let int64 = boxedInt64Number(xVal)
        if (int64 > _I32_MAX || int64 < _I32_MIN) {
          throw Exception.Overflow
        }
        WasmI32.wrapI64(int64)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toInt32(xVal)
      },
      _ => {
        // rationals are never integral, and we refuse to coerce floats to ints
        throw Exception.NumberNotIntlike
      },
    }
  }
  ignore(x)
  result
}

@unsafe
provide let coerceNumberToUnsignedWasmI64 = (x: Number) => {
  use WasmI32.{ (<) }
  let xVal = WasmI32.fromGrain(x)
  let result = if (isSimpleNumber(xVal)) {
    let num = untagSimple(xVal)
    if (num < 0n) {
      throw Exception.Overflow
    }
    WasmI64.extendI32U(num)
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmI64.{ (<) }
        let int64 = boxedInt64Number(xVal)
        if (int64 < 0N) {
          throw Exception.Overflow
        }
        int64
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toUnsignedInt64(xVal)
      },
      _ => {
        // rationals are never integral, and we refuse to coerce floats to ints
        throw Exception.NumberNotIntlike
      },
    }
  }
  ignore(x)
  result
}

@unsafe
provide let coerceNumberToUnsignedWasmI32 = (x: Number) => {
  use WasmI32.{ (<) }
  let xVal = WasmI32.fromGrain(x)
  let result = if (isSimpleNumber(xVal)) {
    let num = untagSimple(xVal)
    if (num < 0n) {
      throw Exception.Overflow
    }
    num
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmI64.{ (<), (>) }
        let int64 = boxedInt64Number(xVal)
        if (int64 > _U32_MAX || int64 < _U32_MIN) {
          throw Exception.Overflow
        }
        WasmI32.wrapI64(int64)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toInt32(xVal)
      },
      _ => {
        // rationals are never integral, and we refuse to coerce floats to ints
        throw Exception.NumberNotIntlike
      },
    }
  }
  ignore(x)
  result
}

@unsafe
let coerceNumberToBigInt = (x: Number) => {
  let xVal = WasmI32.fromGrain(x)
  let result = if (isSimpleNumber(xVal)) {
    BI.makeWrappedInt32(untagSimple(xVal))
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        BI.makeWrappedInt64(boxedInt64Number(xVal))
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        Memory.incRef(xVal)
        xVal
      },
      _ => {
        // rationals are never integral, and we refuse to coerce floats to ints
        throw Exception.NumberNotIntlike
      },
    }
  }
  ignore(x)
  result
}

@unsafe
let isIntegerF32 = value => {
  use WasmF32.{ (==) }
  value == WasmF32.trunc(value)
}

@unsafe
let isIntegerF64 = value => {
  use WasmF64.{ (==) }
  value == WasmF64.trunc(value)
}

@unsafe
let isSafeIntegerF32 = value => {
  use WasmF32.{ (==), (<=) }
  WasmF32.abs(value) <= _F32_MAX_SAFE_INTEGER && WasmF32.trunc(value) == value
}

@unsafe
let isSafeIntegerF64 = value => {
  use WasmF64.{ (==), (<=) }
  WasmF64.abs(value) <= _F64_MAX_SAFE_INTEGER && WasmF64.trunc(value) == value
}

/** Number-aware equality checking
  * The basic idea is that we first figure out the type of the
  * number on the LHS, and then figure out if the RHS number is equal
  * to that number
  *
  * NOTE: The preconditions in these functions are important, so do NOT
  *       provide them!
  */
@unsafe
let numberEqualSimpleHelp = (x, y) => {
  // PRECONDITION: x is a "simple" number (value tag is 0) and x !== y and isNumber(y)
  if (isSimpleNumber(y)) {
    // x !== y, so they must be different
    false
  } else {
    let xval = untagSimple(x) // <- actual int value of x
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmI64.{ (==) }
        let yBoxedVal = boxedInt64Number(y)
        WasmI64.extendI32S(xval) == yBoxedVal
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        WasmI32.eqz(BI.cmpI64(y, WasmI64.extendI32S(xval)))
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        // NOTE: we always store in most reduced form, so a rational and an int are never equal
        false
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (==) }
        let yBoxedVal = boxedFloat64Number(y)
        isSafeIntegerF64(yBoxedVal) && WasmF64.convertI32S(xval) == yBoxedVal
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberEqualInt64Help = (xBoxedVal, y) => {
  // PRECONDITION: x !== y and isNumber(y)
  // Basic number:
  if (isSimpleNumber(y)) {
    use WasmI64.{ (==) }
    xBoxedVal == WasmI64.extendI32S(untagSimple(y))
  } else {
    // Boxed number:
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmI64.{ (==) }
        let yBoxedVal = boxedInt64Number(y)
        xBoxedVal == yBoxedVal
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        WasmI32.eqz(BI.cmpI64(y, xBoxedVal))
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        // NOTE: we always store in most reduced form, so a rational and an int are never equal
        false
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmI64.{ (==) }
        let yBoxedVal = boxedFloat64Number(y)
        isSafeIntegerF64(yBoxedVal) && xBoxedVal == WasmI64.truncF64S(yBoxedVal)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberEqualRationalHelp = (xptr, y) => {
  // PRECONDITION: x is rational and x !== y and isNumber(y)
  // Basic number: (we know it's not equal, since we never store ints as rationals)
  if (isSimpleNumber(y)) {
    false
  } else {
    let xNumerator = boxedRationalNumerator(xptr)
    let xDenominator = boxedRationalDenominator(xptr)
    // Boxed number:
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        false
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        false
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        let yNumerator = boxedRationalNumerator(y)
        let yDenominator = boxedRationalDenominator(y)
        BI.eq(xNumerator, yNumerator) && BI.eq(xDenominator, yDenominator)
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (==), (/) }
        let yBoxedVal = boxedFloat64Number(y)
        let xAsFloat = BI.toFloat64(xNumerator) / BI.toFloat64(xDenominator)
        // TODO(#303): maybe we should have some sort of tolerance?
        xAsFloat == yBoxedVal
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberEqualFloat64Help = (x, y) => {
  let xIsInteger = isIntegerF64(x)
  // Basic number:
  if (isSimpleNumber(y)) {
    use WasmF64.{ (==) }
    xIsInteger && x == WasmF64.convertI32S(untagSimple(y))
  } else {
    // Boxed number
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmF64.{ (==) }
        let yBoxedVal = boxedInt64Number(y)
        isSafeIntegerF64(x) && x == WasmF64.convertI64S(yBoxedVal)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        WasmI32.eqz(BI.cmpF64(y, x))
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        use WasmF64.{ (==), (/) }
        let yNumerator = boxedRationalNumerator(y)
        let yDenominator = boxedRationalDenominator(y)
        let yAsFloat = BI.toFloat64(yNumerator) / BI.toFloat64(yDenominator)
        x == yAsFloat
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (==) }
        let yBoxedVal = boxedFloat64Number(y)
        // TODO(#303): maybe we should have some sort of tolerance?
        x == yBoxedVal
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberEqualBigIntHelp = (x, y) => {
  if (isSimpleNumber(y)) {
    WasmI32.eqz(BI.cmpI64(x, WasmI64.extendI32S(untagSimple(y))))
  } else {
    // Boxed number
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let yBoxedVal = boxedInt64Number(y)
        WasmI32.eqz(BI.cmpI64(x, yBoxedVal))
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.eq(x, y)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        // Rationals are reduced, so it must be unequal
        false
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        let yBoxedVal = boxedFloat64Number(y)
        WasmI32.eqz(BI.cmpF64(x, yBoxedVal))
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
provide let numberEqual = (x, y) => {
  if (isSimpleNumber(x)) {
    // Short circuit if non-pointer value is the same
    x == y || numberEqualSimpleHelp(x, y)
  } else {
    // Boxed number
    let xBoxedNumberTag = boxedNumberTag(x)
    match (xBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let xBoxedVal = boxedInt64Number(x)
        numberEqualInt64Help(xBoxedVal, y)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        numberEqualRationalHelp(x, y)
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        numberEqualFloat64Help(boxedFloat64Number(x), y)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        numberEqualBigIntHelp(x, y)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

/*
 * ===== PLUS & MINUS =====
 * (same schema as equal())
 */

@unsafe
let numberAddSubSimpleHelp = (x, y, isSub) => {
  use WasmI64.{ (+), (-) }
  // PRECONDITION: x is a "simple" number (value tag is 0) and isNumber(y)
  if (isSimpleNumber(y)) {
    let x = WasmI64.extendI32S(untagSimple(x))
    let y = WasmI64.extendI32S(untagSimple(y))
    let result = if (isSub) {
      x - y
    } else {
      x + y
    }
    reducedInteger(result)
  } else {
    let xval = untagSimple(x) // <- actual int value of x
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmI64.{ (<), (>), (>=) }
        let yBoxedVal = boxedInt64Number(y)
        let xval64 = WasmI64.extendI32S(xval)
        let result = if (isSub) xval64 - yBoxedVal else xval64 + yBoxedVal
        if (
          yBoxedVal >= 0N && result < xval64
          || yBoxedVal < 0N && result > xval64
        ) {
          // Overflow. Promote to BigInt
          let xBig = BI.makeWrappedInt32(xval)
          let yBig = BI.makeWrappedInt64(yBoxedVal)
          let res = if (isSub) {
            BI.sub(xBig, yBig)
          } else {
            BI.add(xBig, yBig)
          }
          Memory.decRef(xBig)
          Memory.decRef(yBig)
          reducedBigInteger(res)
        } else {
          reducedInteger(result)
        }
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        // Promote x to bigint and do operation
        let xBig = BI.makeWrappedInt32(xval)
        let result = if (isSub) BI.sub(xBig, y) else BI.add(xBig, y)
        Memory.decRef(xBig)
        reducedBigInteger(result)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        let xBig = BI.makeWrappedInt32(xval)
        let yNumerator = boxedRationalNumerator(y)
        let yDenominator = boxedRationalDenominator(y)
        let expandedXNumerator = BI.mul(xBig, yDenominator)
        Memory.decRef(xBig)
        let result = if (isSub)
          BI.sub(expandedXNumerator, yNumerator)
        else
          BI.add(expandedXNumerator, yNumerator)
        let ret = reducedFractionBigInt(result, yDenominator, false)
        Memory.decRef(expandedXNumerator)
        Memory.decRef(result)
        ret
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (+), (-) }
        let yBoxedVal = boxedFloat64Number(y)
        let xval = WasmF64.convertI32S(xval)
        let result = if (isSub) xval - yBoxedVal else xval + yBoxedVal
        newFloat64(result)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberAddSubInt64Help = (xval, y, isSub) => {
  use WasmI64.{ (+), (-), (<), (>), (>), (>=) }
  if (isSimpleNumber(y)) {
    let yval = WasmI64.extendI32S(untagSimple(y))
    let result = if (isSub) xval - yval else xval + yval
    if (yval >= 0N && result < xval || yval < 0N && result > xval) {
      // Overflow. Promote to BigInt
      let xBig = BI.makeWrappedInt64(xval)
      let yBig = BI.makeWrappedInt64(yval)
      let res = if (isSub) {
        BI.sub(xBig, yBig)
      } else {
        BI.add(xBig, yBig)
      }
      Memory.decRef(xBig)
      Memory.decRef(yBig)
      reducedBigInteger(res)
    } else {
      reducedInteger(result)
    }
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let yBoxedVal = boxedInt64Number(y)
        let xval64 = xval
        let result = if (isSub) xval64 - yBoxedVal else xval64 + yBoxedVal
        if (
          yBoxedVal >= 0N && result < xval64
          || yBoxedVal < 0N && result > xval64
        ) {
          // Overflow. Promote to BigInt
          let xBig = BI.makeWrappedInt64(xval64)
          let yBig = BI.makeWrappedInt64(yBoxedVal)
          let res = if (isSub) {
            BI.sub(xBig, yBig)
          } else {
            BI.add(xBig, yBig)
          }
          Memory.decRef(xBig)
          Memory.decRef(yBig)
          reducedBigInteger(res)
        } else {
          reducedInteger(result)
        }
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        // Promote x to bigint and do operation
        let xBig = BI.makeWrappedInt64(xval)
        let result = if (isSub) BI.sub(xBig, y) else BI.add(xBig, y)
        Memory.decRef(xBig)
        reducedBigInteger(result)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        let xBig = BI.makeWrappedInt64(xval)
        let yNumerator = boxedRationalNumerator(y)
        let yDenominator = boxedRationalDenominator(y)
        let expandedXNumerator = BI.mul(xBig, yDenominator)
        Memory.decRef(xBig)
        let result = if (isSub)
          BI.sub(expandedXNumerator, yNumerator)
        else
          BI.add(expandedXNumerator, yNumerator)
        let ret = reducedFractionBigInt(result, yDenominator, false)
        Memory.decRef(expandedXNumerator)
        Memory.decRef(result)
        ret
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (+), (-) }
        let xval = WasmF64.convertI64S(xval)
        let yBoxedVal = boxedFloat64Number(y)
        let result = if (isSub) xval - yBoxedVal else xval + yBoxedVal
        newFloat64(result)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberAddSubFloat64Help = (xval, y, isSub) => {
  use WasmF64.{ (+), (-) }
  // incRef y to reuse it via WasmI32.toGrain
  Memory.incRef(y)
  let yval = coerceNumberToWasmF64(WasmI32.toGrain(y): Number)
  let result = if (isSub) xval - yval else xval + yval
  newFloat64(result)
}

@unsafe
let numberAddSubBigIntHelp = (x, y, isSub) => {
  if (isSimpleNumber(y)) {
    let yval = WasmI64.extendI32S(untagSimple(y))
    let yBig = BI.makeWrappedInt64(yval)
    let res = if (isSub) {
      BI.sub(x, yBig)
    } else {
      BI.add(x, yBig)
    }
    Memory.decRef(yBig)
    reducedBigInteger(res)
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let yBoxedVal = boxedInt64Number(y)
        let yBig = BI.makeWrappedInt64(yBoxedVal)
        let res = if (isSub) {
          BI.sub(x, yBig)
        } else {
          BI.add(x, yBig)
        }
        Memory.decRef(yBig)
        reducedBigInteger(res)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        let res = if (isSub) {
          BI.sub(x, y)
        } else {
          BI.add(x, y)
        }
        reducedBigInteger(res)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        let yNumerator = boxedRationalNumerator(y)
        let yDenominator = boxedRationalDenominator(y)
        let expandedXNumerator = BI.mul(x, yDenominator)
        let result = if (isSub)
          BI.sub(expandedXNumerator, yNumerator)
        else
          BI.add(expandedXNumerator, yNumerator)
        Memory.decRef(expandedXNumerator)
        let ret = reducedFractionBigInt(result, yDenominator, false)
        Memory.decRef(result)
        ret
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (+), (-) }
        let xval = BI.toFloat64(x)
        let yBoxedVal = boxedFloat64Number(y)
        let result = if (isSub) xval - yBoxedVal else xval + yBoxedVal
        newFloat64(result)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
provide let addSubRational = (x, y, isSub, keepRational) => {
  let xNumerator = boxedRationalNumerator(x)
  let xDenominator = boxedRationalDenominator(x)
  let yNumerator = boxedRationalNumerator(y)
  let yDenominator = boxedRationalDenominator(y)
  if (BI.eq(xDenominator, yDenominator)) {
    let newNumerator = if (isSub)
      BI.sub(xNumerator, yNumerator)
    else
      BI.add(xNumerator, yNumerator)
    let ret = reducedFractionBigInt(newNumerator, xDenominator, keepRational)
    Memory.decRef(newNumerator)
    ret
  } else {
    let numerator1 = BI.mul(xNumerator, yDenominator)
    let numerator2 = BI.mul(yNumerator, xDenominator)
    let numerator = if (isSub)
      BI.sub(numerator1, numerator2)
    else
      BI.add(numerator1, numerator2)
    let denominator = BI.mul(xDenominator, yDenominator)
    let ret = reducedFractionBigInt(numerator, denominator, keepRational)
    Memory.decRef(numerator1)
    Memory.decRef(numerator2)
    Memory.decRef(numerator)
    ret
  }
}

@unsafe
provide let timesDivideRational = (x, y, isDivide, keepRational) => {
  let xNumerator = boxedRationalNumerator(x)
  let xDenominator = boxedRationalDenominator(x)
  let yNumerator = boxedRationalNumerator(y)
  let yDenominator = boxedRationalDenominator(y)
  // (a / b) * (c / d) == (a * c) / (b * d)
  // (a / b) / (c / d) == (a * d) / (b * c)
  let numerator = if (isDivide)
    BI.mul(xNumerator, yDenominator)
  else
    BI.mul(xNumerator, yNumerator)
  let denominator = if (isDivide)
    BI.mul(xDenominator, yNumerator)
  else
    BI.mul(xDenominator, yDenominator)
  reducedFractionBigInt(numerator, denominator, keepRational)
}

@unsafe
provide let rationalsEqual = (x, y) => {
  let xNumerator = boxedRationalNumerator(x)
  let xDenominator = boxedRationalDenominator(x)
  let yNumerator = boxedRationalNumerator(y)
  let yDenominator = boxedRationalDenominator(y)
  BI.eq(xNumerator, yNumerator) && BI.eq(xDenominator, yDenominator)
}

@unsafe
provide let cmpRationals = (x, y) => {
  // Comparing rationals efficiently is an open problem
  // Producing a definitive answer is quite expensive, so if the two
  // values are not strictly equal we approximate an answer

  let xNumerator = boxedRationalNumerator(x)
  let xDenominator = boxedRationalDenominator(x)
  let yNumerator = boxedRationalNumerator(y)
  let yDenominator = boxedRationalDenominator(y)

  if (
    BI.cmp(xNumerator, yNumerator) == 0n
    && BI.cmp(xDenominator, yDenominator) == 0n
  ) {
    0n
  } else {
    use WasmF64.{ (/), (<) }
    let xf = BI.toFloat64(xNumerator) / BI.toFloat64(xDenominator)
    let yf = BI.toFloat64(yNumerator) / BI.toFloat64(yDenominator)
    if (xf < yf) -1n else 1n
  }
}

/**
 * Finds the numerator of the rational number.
 *
 * @param x: The rational number to inspect
 * @returns The numerator of the rational number
 *
 * @since v0.6.0
 */
@unsafe
provide let rationalNumerator = (x: Rational) => {
  let xVal = WasmI32.fromGrain(x)
  let num = boxedRationalNumerator(xVal)
  ignore(x)
  Memory.incRef(num)
  WasmI32.toGrain(reducedBigInteger(num)): Number
}

/**
 * Finds the denominator of the rational number.
 *
 * @param x: The rational number to inspect
 * @returns The denominator of the rational number
 *
 * @since v0.6.0
 */
@unsafe
provide let rationalDenominator = (x: Rational) => {
  let xVal = WasmI32.fromGrain(x)
  let num = boxedRationalDenominator(xVal)
  ignore(x)
  Memory.incRef(num)
  WasmI32.toGrain(reducedBigInteger(num)): Number
}

@unsafe
let numberAddSubRationalHelp = (x, y, isSub) => {
  let xNumerator = boxedRationalNumerator(x)
  let xDenominator = boxedRationalDenominator(x)
  if (isSimpleNumber(y)) {
    let yval = untagSimple(y)
    let yBig = BI.makeWrappedInt32(yval)
    let expandedYNumerator = BI.mul(xDenominator, yBig)
    let result = if (isSub)
      BI.sub(xNumerator, expandedYNumerator)
    else
      BI.add(xNumerator, expandedYNumerator)
    Memory.decRef(expandedYNumerator)
    Memory.decRef(yBig)
    let ret = reducedFractionBigInt(result, xDenominator, false)
    Memory.decRef(result)
    ret
  } else {
    let ytag = boxedNumberTag(y)
    match (ytag) {
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        // The one case we don't delegate is rational +/- rational
        addSubRational(x, y, isSub, false)
      },
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let yBig = BI.makeWrappedInt64(boxedInt64Number(y))
        let expandedYNumerator = BI.mul(yBig, xDenominator)
        Memory.decRef(yBig)
        let result = if (isSub)
          BI.sub(xNumerator, expandedYNumerator)
        else
          BI.add(xNumerator, expandedYNumerator)
        let ret = reducedFractionBigInt(result, xDenominator, false)
        Memory.decRef(expandedYNumerator)
        Memory.decRef(result)
        ret
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        let expandedYNumerator = BI.mul(xDenominator, y)
        let result = if (isSub)
          BI.sub(xNumerator, expandedYNumerator)
        else
          BI.add(xNumerator, expandedYNumerator)
        Memory.decRef(expandedYNumerator)
        let ret = reducedFractionBigInt(result, xDenominator, false)
        Memory.decRef(result)
        ret
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (/), (+), (-) }
        let xnumfloat = BI.toFloat64(xNumerator)
        let xdenfloat = BI.toFloat64(xDenominator)
        let xval = xnumfloat / xdenfloat
        let yval = boxedFloat64Number(y)
        let result = if (isSub) xval - yval else xval + yval
        let ret = newFloat64(result)
        ret
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberAddSubHelp = (x, y, isSub) => {
  if (isSimpleNumber(x)) {
    numberAddSubSimpleHelp(x, y, isSub)
  } else {
    let xtag = boxedNumberTag(x)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        numberAddSubInt64Help(boxedInt64Number(x), y, isSub)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        numberAddSubBigIntHelp(x, y, isSub)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        numberAddSubRationalHelp(x, y, isSub)
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        numberAddSubFloat64Help(boxedFloat64Number(x), y, isSub)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberAdd = (x, y) => {
  WasmI32.toGrain(numberAddSubHelp(x, y, false)): Number
}

@unsafe
let numberSub = (x, y) => {
  WasmI32.toGrain(numberAddSubHelp(x, y, true)): Number
}

/*
 * ===== TIMES & DIVIDE =====
 * (same schema as equal())
 */

@unsafe
let safeI64Multiply = (x, y) => {
  use WasmI64.{ (!=), (*), (/) }
  let prod = x * y
  if (x != 0N) {
    if (prod / x != y) {
      let xBig = BI.makeWrappedInt64(x)
      let yBig = BI.makeWrappedInt64(y)
      let result = BI.mul(xBig, yBig)
      Memory.decRef(xBig)
      Memory.decRef(yBig)
      result
    } else {
      reducedInteger(prod)
    }
  } else {
    reducedInteger(prod)
  }
}

@unsafe
let numberTimesDivideInt64Help = (xval, y, isDivide) => {
  if (isSimpleNumber(y)) {
    if (isDivide) {
      reducedFraction64(xval, WasmI64.extendI32S(untagSimple(y)))
    } else {
      safeI64Multiply(xval, WasmI64.extendI32S(untagSimple(y)))
    }
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let yBoxedVal = boxedInt64Number(y)
        if (isDivide) {
          reducedFraction64(xval, yBoxedVal)
        } else {
          safeI64Multiply(xval, yBoxedVal)
        }
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        let xBig = BI.makeWrappedInt64(xval)
        let ret = if (isDivide) {
          reducedFractionBigInt(xBig, y, false)
        } else {
          reducedBigInteger(BI.mul(xBig, y))
        }
        Memory.decRef(xBig)
        ret
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        let yNumerator = boxedRationalNumerator(y)
        let yDenominator = boxedRationalDenominator(y)
        let xBig = BI.makeWrappedInt64(xval)
        let ret = if (isDivide) {
          // x / (a / b) == (x * b) / a
          let numerator = BI.mul(xBig, yDenominator)
          let ret = reducedFractionBigInt(numerator, yNumerator, false)
          Memory.decRef(numerator)
          ret
        } else {
          // x * (a / b) == (x * a) / b
          let numerator = BI.mul(xBig, yNumerator)
          let ret = reducedFractionBigInt(numerator, yDenominator, false)
          Memory.decRef(numerator)
          ret
        }
        Memory.decRef(xBig)
        ret
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (/), (*) }
        let xval = WasmF64.convertI64S(xval)
        let yBoxedVal = boxedFloat64Number(y)
        if (isDivide) {
          newFloat64(xval / yBoxedVal)
        } else {
          newFloat64(xval * yBoxedVal)
        }
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberTimesDivideBigIntHelp = (x, y, isDivide) => {
  if (isSimpleNumber(y)) {
    let yBig = BI.makeWrappedInt32(untagSimple(y))
    let ret = if (isDivide) {
      reducedFractionBigInt(x, yBig, false)
    } else {
      reducedBigInteger(BI.mul(x, yBig))
    }
    Memory.decRef(yBig)
    ret
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let yBoxedVal = boxedInt64Number(y)
        let yBig = BI.makeWrappedInt64(yBoxedVal)
        let ret = if (isDivide) {
          reducedFractionBigInt(x, yBig, false)
        } else {
          reducedBigInteger(BI.mul(x, yBig))
        }
        Memory.decRef(yBig)
        ret
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        if (isDivide) {
          reducedFractionBigInt(x, y, false)
        } else {
          reducedBigInteger(BI.mul(x, y))
        }
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        let yNumerator = boxedRationalNumerator(y)
        let yDenominator = boxedRationalDenominator(y)
        if (isDivide) {
          // x / (a / b) == (x * b) / a
          let numerator = BI.mul(x, yDenominator)
          let ret = reducedFractionBigInt(numerator, yNumerator, false)
          Memory.decRef(numerator)
          ret
        } else {
          // x * (a / b) == (x * a) / b
          let numerator = BI.mul(x, yNumerator)
          let ret = reducedFractionBigInt(numerator, yDenominator, false)
          Memory.decRef(numerator)
          ret
        }
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (/), (*) }
        let xval = BI.toFloat64(x)
        let yBoxedVal = boxedFloat64Number(y)
        if (isDivide) {
          newFloat64(xval / yBoxedVal)
        } else {
          newFloat64(xval * yBoxedVal)
        }
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberTimesDivideSimpleHelp = (x, y, isDivide) => {
  // PRECONDITION: x is a "simple" number (value tag is 0) and isNumber(y)
  let xval = untagSimple(x) // <- actual int value of x
  numberTimesDivideInt64Help(WasmI64.extendI32S(xval), y, isDivide)
}

@unsafe
let numberTimesDivideRationalHelp = (x, y, isDivide) => {
  use WasmI32.{ (!=) }
  // Division isn't commutative, so we actually need to do the work
  let xNumerator = boxedRationalNumerator(x)
  let xDenominator = boxedRationalDenominator(x)
  if (isSimpleNumber(y)) {
    let yBig = BI.makeWrappedInt32(untagSimple(y))
    let ret = if (isDivide) {
      // (a / b) / y == a / (b * y)
      let denominator = BI.mul(xDenominator, yBig)
      let ret = reducedFractionBigInt(xNumerator, denominator, false)
      Memory.decRef(denominator)
      ret
    } else {
      // (a / b) * y == (a * y) / b
      let numerator = BI.mul(xNumerator, yBig)
      let ret = reducedFractionBigInt(numerator, xDenominator, false)
      Memory.decRef(numerator)
      ret
    }
    if (yBig != ret) {
      Memory.decRef(yBig)
      void
    }
    ret
  } else {
    let ytag = boxedNumberTag(y)
    match (ytag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        // Same idea as above
        let yBig = BI.makeWrappedInt64(boxedInt64Number(y))
        let ret = if (isDivide) {
          // (a / b) / y == a / (b * y)
          let denominator = BI.mul(xDenominator, yBig)
          let ret = reducedFractionBigInt(xNumerator, denominator, false)
          Memory.decRef(denominator)
          ret
        } else {
          // (a / b) * y == (a * y) / b
          let numerator = BI.mul(xNumerator, yBig)
          let ret = reducedFractionBigInt(numerator, xDenominator, false)
          Memory.decRef(numerator)
          ret
        }
        Memory.decRef(yBig)
        ret
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        if (isDivide) {
          // (a / b) / y == a / (b * y)
          let denominator = BI.mul(xDenominator, y)
          let ret = reducedFractionBigInt(xNumerator, denominator, false)
          Memory.decRef(denominator)
          ret
        } else {
          // (a / b) * y == (a * y) / b
          let numerator = BI.mul(xNumerator, y)
          let ret = reducedFractionBigInt(numerator, xDenominator, false)
          Memory.decRef(numerator)
          ret
        }
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        timesDivideRational(x, y, isDivide, false)
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        use WasmF64.{ (/), (*) }
        // TODO(#1190): We should probably use something more accurate if possible here
        let asFloat = BI.toFloat64(xNumerator) / BI.toFloat64(xDenominator)
        if (isDivide) {
          newFloat64(asFloat / boxedFloat64Number(y))
        } else {
          newFloat64(asFloat * boxedFloat64Number(y))
        }
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberTimesDivideFloat64Help = (x, y, isDivide) => {
  use WasmF64.{ (/), (*) }
  // incRef y to reuse it via WasmI32.toGrain
  Memory.incRef(y)
  let yAsFloat = coerceNumberToWasmF64(WasmI32.toGrain(y): Number)
  if (isDivide) {
    newFloat64(x / yAsFloat)
  } else {
    newFloat64(x * yAsFloat)
  }
}

@unsafe
let numberTimesDivideHelp = (x, y, isDivide) => {
  if (isSimpleNumber(x)) {
    numberTimesDivideSimpleHelp(x, y, isDivide)
  } else {
    let xtag = boxedNumberTag(x)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        numberTimesDivideInt64Help(boxedInt64Number(x), y, isDivide)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        numberTimesDivideBigIntHelp(x, y, isDivide)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        numberTimesDivideRationalHelp(x, y, isDivide)
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        numberTimesDivideFloat64Help(boxedFloat64Number(x), y, isDivide)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let numberTimes = (x, y) => {
  WasmI32.toGrain(numberTimesDivideHelp(x, y, false)): Number
}

@unsafe
let numberDivide = (x, y) => {
  WasmI32.toGrain(numberTimesDivideHelp(x, y, true)): Number
}

/*
 * ===== MODULO =====
 * (same schema as equal())
 */

@unsafe
let i64abs = x => {
  use WasmI64.{ (-), (>=) }
  if (x >= 0N) x else 0N - x
}

@unsafe
let numberMod = (x, y) => {
  use WasmI64.{ (!=), (-), (*), (<), (>), (^) }
  // incRef x and y to reuse them via WasmI32.toGrain
  Memory.incRef(x)
  Memory.incRef(y)
  if (isFloat(x) || isFloat(y) || isRational(x) || isRational(y)) {
    use WasmF64.{ (==), (/), (*), (-) }
    let xval = coerceNumberToWasmF64(WasmI32.toGrain(x): Number)
    let yval = coerceNumberToWasmF64(WasmI32.toGrain(y): Number)
    let yInfinite = yval == InfinityW || yval == -InfinityW
    if (yval == 0.0W || yInfinite && (xval == InfinityW || xval == -InfinityW)) {
      newFloat64(NaNW)
    } else if (yInfinite) {
      newFloat64(xval)
    } else {
      newFloat64(xval - WasmF64.trunc(xval / yval) * yval)
    }
  } else {
    let xval = coerceNumberToWasmI64(WasmI32.toGrain(x): Number)
    let yval = coerceNumberToWasmI64(WasmI32.toGrain(y): Number)
    if (WasmI64.eqz(yval)) {
      throw Exception.ModuloByZero
    }
    // We implement true modulo
    if ((xval ^ yval) < 0N) {
      let xabs = i64abs(xval)
      let yabs = i64abs(yval)
      let mval = WasmI64.remS(xabs, yabs)
      let mres = yabs - mval
      reducedInteger(
        if (mval != 0N) (if (yval < 0N) 0N - mres else mres) else 0N
      )
    } else {
      reducedInteger(WasmI64.remS(xval, yval))
    }
  }
}

/*
 * ===== COMPARISONS =====
 * Int/int and float/float comparisons are always accurate.
 * Rational/rational comparisons are approximations with the exception of
 * equality, which is always accurate.
 *
 * Values compared to floats or rationals are first converted to floats.
 *
 * All comparison operators consider NaN not equal to, less than, or greater
 * than NaN, with the exception of `compare`, which considers NaN equal to
 * itself and otherwise smaller than any other float value. This provides a
 * total order (https://en.wikipedia.org/wiki/Total_order) over all numerical
 * values, making `compare` suitable for sorting or ordering.
 */

@unsafe
let cmpBigInt = (x: WasmI32, y: WasmI32) => {
  if (isSimpleNumber(y)) {
    BI.cmpI64(x, WasmI64.extendI32S(untagSimple(y)))
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        BI.cmpI64(x, boxedInt64Number(y))
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.cmp(x, y)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        let tmp = BI.mul(x, boxedRationalDenominator(y))
        let ret = BI.cmp(tmp, boxedRationalNumerator(y))
        Memory.decRef(tmp)
        ret
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        BI.cmpF64(x, boxedFloat64Number(y))
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

// cmpFloat applies a total ordering relation:
// unlike regular float logic, NaN is considered equal to itself and
// smaller than any other number
@unsafe
let cmpFloat = (x: WasmI32, y: WasmI32) => {
  use WasmF64.{ (!=), (<), (>), (/) }
  use WasmI32.{ (-) }
  let xf = boxedFloat64Number(x)
  if (isSimpleNumber(y)) {
    let yf = WasmF64.convertI32S(untagSimple(y))
    // special NaN cases
    if (xf != xf) {
      if (yf != yf) {
        0n
      } else {
        -1n
      }
    } else if (yf != yf) {
      if (xf != xf) {
        0n
      } else {
        1n
      }
    } else {
      if (xf < yf) {
        -1n
      } else if (xf > yf) {
        1n
      } else {
        0n
      }
    }
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    if (yBoxedNumberTag == Tags._GRAIN_BIGINT_BOXED_NUM_TAG) {
      0n - cmpBigInt(y, x)
    } else {
      let yf = match (yBoxedNumberTag) {
        t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
          WasmF64.convertI64S(boxedInt64Number(y))
        },
        t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
          throw InvariantViolation
        },
        t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
          BI.toFloat64(boxedRationalNumerator(y))
            / BI.toFloat64(boxedRationalDenominator(y))
        },
        t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
          boxedFloat64Number(y)
        },
        _ => {
          throw UnknownNumberTag
        },
      }
      // special NaN cases
      if (xf != xf) {
        if (yf != yf) {
          0n
        } else {
          -1n
        }
      } else if (yf != yf) {
        if (xf != xf) {
          0n
        } else {
          1n
        }
      } else {
        if (xf < yf) {
          -1n
        } else if (xf > yf) {
          1n
        } else {
          0n
        }
      }
    }
  }
}

@unsafe
let cmpSmallInt = (x: WasmI32, y: WasmI32) => {
  use WasmI64.{ (<), (>) }
  use WasmI32.{ (-) }
  let xi = boxedInt64Number(x)
  if (isSimpleNumber(y)) {
    let yi = WasmI64.extendI32S(untagSimple(y))
    if (xi < yi) {
      -1n
    } else if (xi > yi) {
      1n
    } else {
      0n
    }
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        let yi = boxedInt64Number(y)
        if (xi < yi) {
          -1n
        } else if (xi > yi) {
          1n
        } else {
          0n
        }
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        0n - cmpBigInt(y, x)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        use WasmF64.{ (<), (/) }
        // Rationals and ints are never considered equal
        if (
          WasmF64.convertI64S(xi)
          < BI.toFloat64(boxedRationalNumerator(y))
            / BI.toFloat64(boxedRationalDenominator(y))
        )
          -1n
        else
          1n
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        0n - cmpFloat(y, x)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
let cmpRational = (x: WasmI32, y: WasmI32) => {
  use WasmI32.{ (-) }
  if (isSimpleNumber(y)) {
    use WasmF64.{ (/), (<) }
    let xf = BI.toFloat64(boxedRationalNumerator(x))
      / BI.toFloat64(boxedRationalDenominator(x))
    // Rationals and ints are never considered equal
    if (xf < WasmF64.convertI32S(untagSimple(y))) -1n else 1n
  } else {
    let yBoxedNumberTag = boxedNumberTag(y)
    match (yBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        0n - cmpSmallInt(y, x)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        0n - cmpBigInt(y, x)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        cmpRationals(x, y)
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        0n - cmpFloat(y, x)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

@unsafe
provide let cmp = (x: WasmI32, y: WasmI32) => {
  use WasmI32.{ (-) }
  if (isSimpleNumber(x)) {
    if (isSimpleNumber(y)) {
      // fast comparison path for simple numbers
      x - y
    } else {
      let yBoxedNumberTag = boxedNumberTag(y)
      match (yBoxedNumberTag) {
        t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
          0n - cmpSmallInt(y, x)
        },
        t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
          0n - cmpBigInt(y, x)
        },
        t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
          0n - cmpRational(y, x)
        },
        t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
          0n - cmpFloat(y, x)
        },
        _ => {
          throw UnknownNumberTag
        },
      }
    }
  } else {
    let xBoxedNumberTag = boxedNumberTag(x)
    match (xBoxedNumberTag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        cmpSmallInt(x, y)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        cmpBigInt(x, y)
      },
      t when t == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG => {
        cmpRational(x, y)
      },
      t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
        cmpFloat(x, y)
      },
      _ => {
        throw UnknownNumberTag
      },
    }
  }
}

// In the comparison functions below, NaN is neither greater than, less than,
// or equal to any other number (including NaN), so any comparison involving
// NaN is always false. The only exception to this rule is `compare`, which
// applies a total ordering relation to allow numbers to be sortable (with
// NaN being considered equal to itself and less than all other numbers in
// this case).

/**
 * Checks if the first operand is less than the second operand.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns `true` if the first operand is less than the second operand or `false` otherwise
 *
 * @since v0.1.0
 */
@unsafe
provide let (<) = (num1: Number, num2: Number) => {
  use WasmI32.{ (<) }
  let x = WasmI32.fromGrain(num1)
  let y = WasmI32.fromGrain(num2)
  let result = !isNaN(x) && !isNaN(y) && cmp(x, y) < 0n
  ignore(num1)
  ignore(num2)
  result
}

/**
 * Checks if the first operand is greater than the second operand.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns `true` if the first operand is greater than the second operand or `false` otherwise
 *
 * @since v0.1.0
 */
@unsafe
provide let (>) = (num1: Number, num2: Number) => {
  use WasmI32.{ (>) }
  let x = WasmI32.fromGrain(num1)
  let y = WasmI32.fromGrain(num2)
  let result = !isNaN(x) && !isNaN(y) && cmp(x, y) > 0n
  ignore(num1)
  ignore(num2)
  result
}

/**
 * Checks if the first operand is less than or equal to the second operand.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns `true` if the first operand is less than or equal to the second operand or `false` otherwise
 *
 * @since v0.1.0
 */
@unsafe
provide let (<=) = (num1: Number, num2: Number) => {
  use WasmI32.{ (<=) }
  let x = WasmI32.fromGrain(num1)
  let y = WasmI32.fromGrain(num2)
  let result = !isNaN(x) && !isNaN(y) && cmp(x, y) <= 0n
  ignore(num1)
  ignore(num2)
  result
}

/**
 * Checks if the first operand is greater than or equal to the second operand.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns `true` if the first operand is greater than or equal to the second operand or `false` otherwise
 *
 * @since v0.1.0
 */
@unsafe
provide let (>=) = (num1: Number, num2: Number) => {
  use WasmI32.{ (>=) }
  let x = WasmI32.fromGrain(num1)
  let y = WasmI32.fromGrain(num2)
  let result = !isNaN(x) && !isNaN(y) && cmp(x, y) >= 0n
  ignore(num1)
  ignore(num2)
  result
}

@unsafe
provide let compare = (x: Number, y: Number) => {
  let xVal = WasmI32.fromGrain(x)
  let yVal = WasmI32.fromGrain(y)
  let result = WasmI32.toGrain(tagSimple(cmp(xVal, yVal))): Number
  ignore(x)
  ignore(y)
  result
}

/*
 * ===== EQUAL =====
 */

@unsafe
provide let numberEq = (x: Number, y: Number) => {
  let xVal = WasmI32.fromGrain(x)
  let yVal = WasmI32.fromGrain(y)
  let result = numberEqual(xVal, yVal)
  ignore(x)
  ignore(y)
  result
}

/*
 * ===== LOGICAL OPERATIONS =====
 * Only valid for int-like numbers. Coerce to i64/bigInt and do operations
 */
// TODO(#306): Semantics around when things should stay i32/i64

/**
 * Computes the bitwise NOT of the operand.
 *
 * @param value: The operand
 * @returns Containing the inverted bits of the operand
 *
 * @since v0.2.0
 */
@unsafe
provide let lnot = (value: Number) => {
  let xw32 = WasmI32.fromGrain(value)
  let result = if (isBigInt(xw32)) {
    WasmI32.toGrain(reducedBigInteger(BI.bitwiseNot(xw32))): Number
  } else {
    let xval = coerceNumberToWasmI64(value)
    WasmI32.toGrain(reducedInteger(i64not(xval))): Number
  }
  ignore(value)
  result
}

/**
 * Shifts the bits of the value left by the given number of bits.
 *
 * @param value: The value to shift
 * @param amount: The number of bits to shift by
 * @returns The shifted value
 *
 * @since v0.3.0
 * @history v0.2.0: Originally named `lsl`
 * @history v0.3.0: Renamed to `<<`
 */
@unsafe
provide let (<<) = (value: Number, amount: Number) => {
  use WasmI64.{ (-), (<<) }
  let xw32 = WasmI32.fromGrain(value)
  let result = if (isBigInt(xw32)) {
    let yval = coerceNumberToWasmI32(amount)
    WasmI32.toGrain(reducedBigInteger(BI.shl(xw32, yval))): Number
  } else {
    let xval = coerceNumberToWasmI64(value)
    let yval = coerceNumberToWasmI64(amount)
    // if the number will be shifted beyond the end of the i64 range, promote to BigInt
    // (note that we subtract one leading zero, since the leading bit is the sign bit)
    if (WasmI64.leU(WasmI64.clz(i64abs(xval)) - 1N, yval)) {
      let xbi = coerceNumberToBigInt(value)
      let yval = coerceNumberToWasmI32(amount)
      WasmI32.toGrain(reducedBigInteger(BI.shl(xbi, yval))): Number
    } else {
      WasmI32.toGrain(reducedInteger(xval << yval)): Number
    }
  }
  ignore(value)
  result
}

/**
 * Shifts the bits of the value right by the given number of bits, preserving the sign bit.
 *
 * @param value: The value to shift
 * @param amount: The amount to shift by
 * @returns The shifted value
 *
 * @since v0.3.0
 * @history v0.2.0: Originally named `lsr`
 * @history v0.3.0: Renamed to `>>>`
 */
@unsafe
provide let (>>>) = (value: Number, amount: Number) => {
  use WasmI64.{ (>>>) }
  let xw32 = WasmI32.fromGrain(value)
  let result = if (isBigInt(xw32)) {
    let yval = coerceNumberToWasmI32(amount)
    // [NOTE]: For BigInts, shrU is the same as shrS because there
    //         are an *infinite* number of leading ones
    WasmI32.toGrain(reducedBigInteger(BI.shrS(xw32, yval))): Number
  } else {
    let xval = coerceNumberToWasmI64(value)
    let yval = coerceNumberToWasmI64(amount)
    WasmI32.toGrain(reducedInteger(xval >>> yval)): Number
  }
  ignore(value)
  result
}

/**
 * Computes the bitwise AND (`&`) on the given operands.
 *
 * @param value1: The first operand
 * @param value2: The second operand
 * @returns Containing a `1` in each bit position for which the corresponding bits of both operands are `1`
 *
 * @since v0.3.0
 * @history v0.2.0: Originally named `land`
 * @history v0.3.0: Renamed to `&`
 */
@unsafe
provide let (&) = (value1: Number, value2: Number) => {
  use WasmI64.{ (&) }
  let xw32 = WasmI32.fromGrain(value1)
  let yw32 = WasmI32.fromGrain(value2)
  let result = if (isBigInt(xw32) || isBigInt(yw32)) {
    let xval = coerceNumberToBigInt(value1)
    let yval = coerceNumberToBigInt(value2)
    let ret = WasmI32.toGrain(reducedBigInteger(BI.bitwiseAnd(xval, yval))):
      Number
    if (!(xw32 == xval)) {
      Memory.decRef(xval)
      void
    }
    if (!(yw32 == yval)) {
      Memory.decRef(yval)
      void
    }
    ret
  } else {
    let xval = coerceNumberToWasmI64(value1)
    let yval = coerceNumberToWasmI64(value2)
    WasmI32.toGrain(reducedInteger(xval & yval)): Number
  }
  ignore(value1)
  ignore(value2)
  result
}

/**
 * Computes the bitwise OR (`|`) on the given operands.
 *
 * @param value1: The first operand
 * @param value2: The second operand
 * @returns Containing a `1` in each bit position for which the corresponding bits of either or both operands are `1`
 *
 * @since v0.3.0
 * @history v0.2.0: Originally named `lor`
 * @history v0.3.0: Renamed to `|`
 */
@unsafe
provide let (|) = (value1: Number, value2: Number) => {
  use WasmI64.{ (|) }
  let xw32 = WasmI32.fromGrain(value1)
  let yw32 = WasmI32.fromGrain(value2)
  let result = if (isBigInt(xw32) || isBigInt(yw32)) {
    let xval = coerceNumberToBigInt(value1)
    let yval = coerceNumberToBigInt(value2)
    let ret = WasmI32.toGrain(reducedBigInteger(BI.bitwiseOr(xval, yval))):
      Number
    if (!(xw32 == xval)) {
      Memory.decRef(xval)
      void
    }
    if (!(yw32 == yval)) {
      Memory.decRef(yval)
      void
    }
    ret
  } else {
    let xval = coerceNumberToWasmI64(value1)
    let yval = coerceNumberToWasmI64(value2)
    WasmI32.toGrain(reducedInteger(xval | yval)): Number
  }
  ignore(value1)
  ignore(value2)
  result
}

/**
 * Computes the bitwise XOR (`^`) on the given operands.
 *
 * @param value1: The first operand
 * @param value2: The second operand
 * @returns Containing a `1` in each bit position for which the corresponding bits of either but not both operands are `1`
 *
 * @since v0.3.0
 * @history v0.1.0: The `^` operator was originally an alias of `unbox`
 * @history v0.2.0: Originally named `lxor`
 * @history v0.3.0: Renamed to `^`
 */
@unsafe
provide let (^) = (value1: Number, value2: Number) => {
  use WasmI64.{ (^) }
  let xw32 = WasmI32.fromGrain(value1)
  let yw32 = WasmI32.fromGrain(value2)
  let result = if (isBigInt(xw32) || isBigInt(yw32)) {
    let xval = coerceNumberToBigInt(value1)
    let yval = coerceNumberToBigInt(value2)
    let ret = WasmI32.toGrain(reducedBigInteger(BI.bitwiseXor(xval, yval))):
      Number
    if (!(xw32 == xval)) {
      Memory.decRef(xval)
      void
    }
    if (!(yw32 == yval)) {
      Memory.decRef(yval)
      void
    }
    ret
  } else {
    let xval = coerceNumberToWasmI64(value1)
    let yval = coerceNumberToWasmI64(value2)
    WasmI32.toGrain(reducedInteger(xval ^ yval)): Number
  }
  ignore(value1)
  ignore(value2)
  result
}

/**
 * Shifts the bits of the value right by the given number of bits.
 *
 * @param value: The value to shift
 * @param amount: The amount to shift by
 * @returns The shifted value
 *
 * @since v0.3.0
 * @history v0.2.0: Originally named `asr`
 * @history v0.3.0: Renamed to `>>`
 */
@unsafe
provide let (>>) = (value: Number, amount: Number) => {
  use WasmI64.{ (>>) }
  let xw32 = WasmI32.fromGrain(value)
  let result = if (isBigInt(xw32)) {
    let yval = coerceNumberToWasmI32(amount)
    // [NOTE]: For BigInts, shrU is the same as shrS because there
    //         are an *infinite* number of leading ones
    WasmI32.toGrain(reducedBigInteger(BI.shrS(xw32, yval))): Number
  } else {
    let xval = coerceNumberToWasmI64(value)
    let yval = coerceNumberToWasmI64(amount)
    WasmI32.toGrain(reducedInteger(xval >> yval)): Number
  }
  ignore(value)
  result
}

/// USER-EXPOSED COERCION FUNCTIONS
//
// [NOTE]: Coercion is a *conservative* process! For example, even if a float is 1.0,
//         we will fail if attempting to coerce to an int!

@unsafe
let coerceNumberToShortUint = (x: Number, max32, max64, is8bit) => {
  use WasmI32.{ (&), (<), (>) }
  let xVal = WasmI32.fromGrain(x)
  let int32 = if (isSimpleNumber(xVal)) {
    untagSimple(xVal)
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmI64.{ (<), (>) }
        let int64 = boxedInt64Number(xVal)
        if (int64 > max64 || int64 < 0N) {
          throw Exception.Overflow
        }
        WasmI32.wrapI64(int64)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toInt32(xVal)
      },
      _ => {
        // rationals are never integral, and we refuse to coerce floats to ints
        throw Exception.NumberNotIntlike
      },
    }
  }
  ignore(x)
  if (int32 > max32 || int32 < 0n) {
    throw Exception.Overflow
  }
  if (is8bit) int32 & 0xffffn else int32 & 0xffffn
}

@unsafe
let coerceNumberToShortInt = (x: Number, min32, max32, min64, max64, is8bit) => {
  use WasmI32.{ (<), (>) }
  let xVal = WasmI32.fromGrain(x)
  let int32 = if (isSimpleNumber(xVal)) {
    untagSimple(xVal)
  } else {
    let xtag = boxedNumberTag(xVal)
    match (xtag) {
      t when t == Tags._GRAIN_INT64_BOXED_NUM_TAG => {
        use WasmI64.{ (<), (>) }
        let int64 = boxedInt64Number(xVal)
        if (int64 > max64 || int64 < min64) {
          throw Exception.Overflow
        }
        WasmI32.wrapI64(int64)
      },
      t when t == Tags._GRAIN_BIGINT_BOXED_NUM_TAG => {
        BI.toInt32(xVal)
      },
      _ => {
        // rationals are never integral, and we refuse to coerce floats to ints
        throw Exception.NumberNotIntlike
      },
    }
  }
  ignore(x)
  if (int32 > max32 || int32 < min32) {
    throw Exception.Overflow
  }
  if (is8bit) WasmI32.extendS8(int32) else WasmI32.extendS16(int32)
}

/**
 * Converts a Number to an Int8.
 *
 * @param number: The value to convert
 * @returns The Number represented as an Int8
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceNumberToInt8 = (number: Number) => {
  let val = coerceNumberToShortInt(
    number,
    _SMIN8_I32,
    _SMAX8_I32,
    _SMIN8_I64,
    _SMAX8_I64,
    true
  )
  tagInt8(val)
}

/**
 * Converts a Number to an Int16.
 *
 * @param number: The value to convert
 * @returns The Number represented as an Int16
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceNumberToInt16 = (number: Number) => {
  let val = coerceNumberToShortInt(
    number,
    _SMIN16_I32,
    _SMAX16_I32,
    _SMIN16_I64,
    _SMAX16_I64,
    false
  )
  tagInt16(val)
}

/**
 * Converts a Number to a Uint8.
 *
 * @param number: The value to convert
 * @returns The Number represented as a Uint8
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceNumberToUint8 = (number: Number) => {
  let val = coerceNumberToShortUint(number, _UMAX8_I32, _UMAX8_I64, true)
  tagUint8(val)
}

/**
 * Converts a Number to a Uint16.
 *
 * @param number: The value to convert
 * @returns The Number represented as a Uint16
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceNumberToUint16 = (number: Number) => {
  let val = coerceNumberToShortUint(number, _UMAX16_I32, _UMAX16_I64, false)
  tagUint16(val)
}

/**
 * Converts a Number to an Int32.
 *
 * @param number: The value to convert
 * @returns The Number represented as an Int32
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceNumberToInt32 = (number: Number) => {
  let result = newInt32(coerceNumberToWasmI32(number))
  WasmI32.toGrain(result): Int32
}

/**
 * Converts a Number to an Int64.
 *
 * @param number: The value to convert
 * @returns The Number represented as an Int64
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceNumberToInt64 = (number: Number) => {
  let x = WasmI32.fromGrain(number)
  let result = if (
    !isSimpleNumber(x)
    && boxedNumberTag(x) == Tags._GRAIN_INT64_BOXED_NUM_TAG
  ) {
    // avoid extra malloc and prevent x from being freed
    Memory.incRef(x)
    x
  } else {
    // incRef x to reuse it via WasmI32.toGrain
    Memory.incRef(x)
    newInt64(coerceNumberToWasmI64(WasmI32.toGrain(x): Number))
  }
  ignore(number)
  WasmI32.toGrain(result): Int64
}

/**
 * Converts a Number to a BigInt.
 *
 * @param number: The value to convert
 * @returns The Number represented as a BigInt
 *
 * @since v0.5.0
 */
@unsafe
provide let coerceNumberToBigInt = (number: Number) => {
  WasmI32.toGrain(coerceNumberToBigInt(number)): BigInt
}

/**
 * Converts a Number to a Rational.
 *
 * @param number: The value to convert
 * @returns The Number represented as a Rational
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceNumberToRational = (number: Number) => {
  let x = WasmI32.fromGrain(number)
  let result = if (isSimpleNumber(x)) {
    newRational(BI.makeWrappedInt32(untagSimple(x)), BI.makeWrappedInt32(1n))
  } else {
    let tag = boxedNumberTag(x)
    if (tag == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG) {
      // avoid extra malloc and prevent x from being freed
      Memory.incRef(x)
      x
    } else if (tag == Tags._GRAIN_INT64_BOXED_NUM_TAG) {
      // incRef x to reuse it via WasmI32.toGrain
      Memory.incRef(x)
      newRational(
        BI.makeWrappedInt32(coerceNumberToWasmI32(WasmI32.toGrain(x): Number)),
        BI.makeWrappedInt32(1n)
      )
    } else {
      throw Exception.NumberNotRational
    }
  }
  ignore(number)
  WasmI32.toGrain(result): Rational
}

/**
 * Converts a Number to a Float32.
 *
 * @param number: The value to convert
 * @returns The Number represented as a Float32
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceNumberToFloat32 = (number: Number) => {
  let result = newFloat32(coerceNumberToWasmF32(number))
  WasmI32.toGrain(result): Float32
}

/**
 * Converts a Number to a Float64.
 *
 * @param number: The value to convert
 * @returns The Number represented as a Float64
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceNumberToFloat64 = (number: Number) => {
  let x = WasmI32.fromGrain(number)
  let result = if (
    !isSimpleNumber(x)
    && boxedNumberTag(x) == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG
  ) {
    // avoid extra malloc and prevent x from being freed
    Memory.incRef(x)
    x
  } else {
    // incRef x to reuse it via WasmI32.toGrain
    Memory.incRef(x)
    newFloat64(coerceNumberToWasmF64(WasmI32.toGrain(x): Number))
  }
  ignore(number)
  WasmI32.toGrain(result): Float64
}

/**
 * Converts an Int8 to a Number.
 *
 * @param value: The value to convert
 * @returns The Int8 represented as a Number
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceInt8ToNumber = (value: Int8) => {
  let num = untagInt8(value)
  WasmI32.toGrain(tagSimple(num)): Number
}

/**
 * Converts an Int16 to a Number.
 *
 * @param value: The value to convert
 * @returns The Int16 represented as a Number
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceInt16ToNumber = (value: Int16) => {
  let num = untagInt16(value)
  WasmI32.toGrain(tagSimple(num)): Number
}

/**
 * Converts a Uint8 to a Number.
 *
 * @param value: The value to convert
 * @returns The Uint8 represented as a Number
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceUint8ToNumber = (value: Uint8) => {
  let num = untagUint8(value)
  WasmI32.toGrain(tagSimple(num)): Number
}

/**
 * Converts a Uint16 to a Number.
 *
 * @param value: The value to convert
 * @returns The Uint16 represented as a Number
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceUint16ToNumber = (value: Uint16) => {
  let num = untagUint16(value)
  WasmI32.toGrain(tagSimple(num)): Number
}

/**
 * Converts an Int32 to a Number.
 *
 * @param value: The value to convert
 * @returns The Int32 represented as a Number
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceInt32ToNumber = (value: Int32) => {
  let x = WasmI32.load(WasmI32.fromGrain(value), 4n)
  let result = reducedInteger(WasmI64.extendI32S(x))
  ignore(value)
  WasmI32.toGrain(result): Number
}

/**
 * Converts an Int64 to a Number.
 *
 * @param value: The value to convert
 * @returns The Int64 represented as a Number
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceInt64ToNumber = (value: Int64) => {
  let x = WasmI32.fromGrain(value)
  let result = WasmI32.toGrain(reducedInteger(boxedInt64Number(x))): Number
  ignore(value)
  result
}

/**
 * Converts a BigInt to a Number.
 *
 * @param num: The value to convert
 * @returns The BigInt represented as a Number
 *
 * @since v0.5.0
 */
@unsafe
provide let coerceBigIntToNumber = (num: BigInt) => {
  let x = WasmI32.fromGrain(num)
  // reducedBigInteger assumes that the bigint is dead,
  // but in our case, it is not
  Memory.incRef(x)
  let result = WasmI32.toGrain(reducedBigInteger(x)): Number
  ignore(num)
  result
}

/**
 * Converts a Rational to a Number.
 *
 * @param rational: The value to convert
 * @returns The Rational represented as a Number
 *
 * @since v0.6.0
 */
@unsafe
provide let coerceRationalToNumber = (rational: Rational) => {
  let x = WasmI32.fromGrain(rational)
  let denom = boxedRationalDenominator(x)
  let x = if (BI.eq(denom, BI.makeWrappedInt32(1n))) {
    boxedRationalNumerator(x)
  } else {
    x
  }
  // incRef x to reuse it via WasmI32.toGrain
  Memory.incRef(x)
  let result = WasmI32.toGrain(x): Number
  ignore(rational)
  result
}

/**
 * Converts a Float32 to a Number.
 *
 * @param float: The value to convert
 * @returns The Float32 represented as a Number
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceFloat32ToNumber = (float: Float32) => {
  let x = WasmF32.load(WasmI32.fromGrain(float), 4n)
  let x64 = WasmF64.promoteF32(x)
  let result = WasmI32.toGrain(newFloat64(x64)): Number
  ignore(float)
  result
}

/**
 * Converts a Float64 to a Number.
 *
 * @param float: The value to convert
 * @returns The Float64 represented as a Number
 *
 * @since v0.2.0
 */
@unsafe
provide let coerceFloat64ToNumber = (float: Float64) => {
  let x = WasmI32.fromGrain(float)
  // incRef x to reuse it via WasmI32.toGrain
  Memory.incRef(x)
  let result = WasmI32.toGrain(x): Number
  ignore(float)
  result
}

/// USER-EXPOSED CONVERSION FUNCTIONS

@unsafe
provide let convertExactToInexact = (x: Number) => {
  x
}

@unsafe
let convertInexactToExactHelp = x => {
  if (isSimpleNumber(x)) {
    x
  } else {
    let tag = boxedNumberTag(x)
    if (
      tag == Tags._GRAIN_INT64_BOXED_NUM_TAG
      || tag == Tags._GRAIN_BIGINT_BOXED_NUM_TAG
      || tag == Tags._GRAIN_RATIONAL_BOXED_NUM_TAG
    ) {
      Memory.incRef(x)
      x
    } else {
      match (tag) {
        t when t == Tags._GRAIN_FLOAT64_BOXED_NUM_TAG => {
          // TODO(#1191): Investigate if BigInt is more accurate
          reducedInteger(
            WasmI64.truncF64S(WasmF64.nearest(boxedFloat64Number(x)))
          )
        },
        _ => {
          throw UnknownNumberTag
        },
      }
    }
  }
}

@unsafe
provide let convertInexactToExact = (x: Number) => {
  let xVal = WasmI32.fromGrain(x)
  let result = WasmI32.toGrain(convertInexactToExactHelp(xVal)): Number
  ignore(x)
  result
}

/**
 * Computes the sum of its operands.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns The sum of the two operands
 *
 * @since v0.1.0
 */
@unsafe
provide let (+) = (num1: Number, num2: Number) => {
  let ret = numberAdd(WasmI32.fromGrain(num1), WasmI32.fromGrain(num2))
  ignore(num1)
  ignore(num2)
  ret
}

/**
 * Computes the difference of its operands.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns The difference of the two operands
 *
 * @since v0.1.0
 */
@unsafe
provide let (-) = (num1: Number, num2: Number) => {
  let ret = numberSub(WasmI32.fromGrain(num1), WasmI32.fromGrain(num2))
  ignore(num1)
  ignore(num2)
  ret
}

/**
 * Computes the product of its operands.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns The product of the two operands
 *
 * @since v0.1.0
 */
@unsafe
provide let (*) = (num1: Number, num2: Number) => {
  let ret = numberTimes(WasmI32.fromGrain(num1), WasmI32.fromGrain(num2))
  ignore(num1)
  ignore(num2)
  ret
}

/**
 * Computes the quotient of its operands.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns The quotient of the two operands
 *
 * @since v0.1.0
 */
@unsafe
provide let (/) = (num1: Number, num2: Number) => {
  let ret = numberDivide(WasmI32.fromGrain(num1), WasmI32.fromGrain(num2))
  ignore(num1)
  ignore(num2)
  ret
}

/**
 * Computes the remainder of the division of the first operand by the second.
 * The result will have the sign of the second operand.
 *
 * @param num1: The first operand
 * @param num2: The second operand
 * @returns The modulus of its operands
 *
 * @since v0.1.0
 */
@unsafe
provide let (%) = (num1: Number, num2: Number) => {
  let x = WasmI32.fromGrain(num1)
  let y = WasmI32.fromGrain(num2)
  let result = WasmI32.toGrain(numberMod(x, y)): Number
  ignore(num1)
  ignore(num2)
  result
}

// inc/dec

/**
 * Increments the value by one.
 *
 * @param value: The value to increment
 * @returns The incremented value
 *
 * @since v0.1.0
 */
provide let incr = value => {
  value + 1
}

/**
 * Decrements the value by one.
 *
 * @param value: The value to decrement
 * @returns The decremented value
 *
 * @since v0.1.0
 */
provide let decr = value => {
  value - 1
}

@unsafe
provide let isBigInt = x => {
  let xVal = WasmI32.fromGrain(x)
  let result = isBigInt(xVal)
  ignore(x)
  result
}

// Scalbn is based on https://git.musl-libc.org/cgit/musl/tree/src/math/scalbn.c
/*
 * ====================================================
 * Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved.
 *
 * Permission to use, copy, modify, and distribute this
 * software is freely granted, provided that this notice
 * is preserved.
 * ====================================================
 */
/**
 * Multiplies a floating-point number by an integral power of 2.
 *
 * @param x: The floating-point value
 * @param n: The Integer exponent
 * @returns The result of x * 2^n
 *
 * @since v0.5.4
 */
@unsafe
provide let scalbn = (x, n) => {
  use WasmI32.{ (>), (<), (-), (+) }
  use WasmF64.{ (*) }
  use WasmI64.{ (<<) }
  // Constants
  let mut n = n
  let mut y = x
  if (n > 1023n) {
    y *= 0x1p1023W
    n -= 1023n
    if (n > 1023n) {
      y *= 0x1p1023W
      n -= 1023n
      if (n > 1023n) n = 1023n
    } else if (n < -1023n) {
      /* make sure final n < -53 to avoid double rounding in the subnormal range */
      y *= 0x1p-1022W * 0x1p53W
      n += 1022n - 53n
      if (n < -1022n) {
        y *= 0x1p-1022W * 0x1p53W
        n += 1022n - 53n
        if (n < -1022n) n = -1022n
      }
    }
  }
  y * WasmF64.reinterpretI64(WasmI64.extendI32S(0x3FFn + n) << 52N)
}

// Exponentiation by squaring https://en.wikipedia.org/wiki/Exponentiation_by_squaring special path for int^int
let rec expBySquaring = (y, x, n) => {
  let (==) = numberEq
  if (n == 0) {
    1
  } else if (n == 1) {
    x * y
  } else if (n % 2 == 0) {
    expBySquaring(y, x * x, n / 2)
  } else {
    expBySquaring(x * y, x * x, (n - 1) / 2)
  }
}

// Math.pow for floats
@unsafe
provide let powf = (x: WasmF64, y: WasmF64) => {
  // Based on https://git.musl-libc.org/cgit/musl/tree/src/math/pow.c
  use WasmF64.{ (==), (!=), (<=), (/), (*), (+) }
  // Fast paths
  if (WasmF64.abs(y) <= 2.0W) {
    if (y == 2.0W) {
      return x * x
    } else if (y == 0.5W) {
      if (x != InfinityW) {
        return WasmF64.abs(WasmF64.sqrt(x))
      } else {
        return InfinityW
      }
    } else if (y == -1.0W) {
      return 1.0W / x
    } else if (y == 1.0W) {
      return x
    } else if (y == 0.0W) {
      return NaNW
    }
  }
  // Full calculation
  let dp_h1 = WasmF64.reinterpretI64(0x3FE2B80340000000N)
  let dp_l1 = WasmF64.reinterpretI64(0x3E4CFDEB43CFD006N)
  let two53 = WasmF64.reinterpretI64(0x4340000000000000N)
  let huge = WasmF64.reinterpretI64(0x7E37E43C8800759CN)
  let tiny = WasmF64.reinterpretI64(0x01A56E1FC2F8F359N)
  let l1 = WasmF64.reinterpretI64(0x3FE3333333333303N)
  let l2 = WasmF64.reinterpretI64(0x3FDB6DB6DB6FABFFN)
  let l3 = WasmF64.reinterpretI64(0x3FD55555518F264DN)
  let l4 = WasmF64.reinterpretI64(0x3FD17460A91D4101N)
  let l5 = WasmF64.reinterpretI64(0x3FCD864A93C9DB65N)
  let l6 = WasmF64.reinterpretI64(0x3FCA7E284A454EEFN)
  let p1 = WasmF64.reinterpretI64(0x3FC555555555553EN)
  let p2 = WasmF64.reinterpretI64(0xBF66C16C16BEBD93N)
  let p3 = WasmF64.reinterpretI64(0x3F11566AAF25DE2CN)
  let p4 = WasmF64.reinterpretI64(0xBEBBBD41C5D26BF1N)
  let p5 = WasmF64.reinterpretI64(0x3E66376972BEA4D0N)
  let lg2 = WasmF64.reinterpretI64(0x3FE62E42FEFA39EFN)
  let lg2_h = WasmF64.reinterpretI64(0x3FE62E4300000000N)
  let lg2_l = WasmF64.reinterpretI64(0xBE205C610CA86C39N)
  let ovt = WasmF64.reinterpretI64(0x3C971547652B82FEN)
  let cp = WasmF64.reinterpretI64(0x3FEEC709DC3A03FDN)
  let cp_h = WasmF64.reinterpretI64(0x3FEEC709E0000000N)
  let cp_l = WasmF64.reinterpretI64(0xBE3E2FE0145B01F5N)
  let ivln2 = WasmF64.reinterpretI64(0x3FF71547652B82FEN)
  let ivln2_h = WasmF64.reinterpretI64(0x3FF7154760000000N)
  let ivln2_l = WasmF64.reinterpretI64(0x3E54AE0BF85DDF44N)
  let inv3 = WasmF64.reinterpretI64(0x3FD5555555555555N)
  use WasmI32.{
    (==),
    (!=),
    (>=),
    (<=),
    (&),
    (|),
    (>),
    (<),
    (<<),
    (>>),
    (-),
    (+),
  }
  use WasmI64.{ (>>) as shrSWasmI64 }
  let u_ = WasmI64.reinterpretF64(x)
  let hx = WasmI32.wrapI64(shrSWasmI64(u_, 32N))
  let lx = WasmI32.wrapI64(u_)
  let u_ = WasmI64.reinterpretF64(y)
  let hy = WasmI32.wrapI64(shrSWasmI64(u_, 32N))
  let ly = WasmI32.wrapI64(u_)
  let mut ix = hx & 0x7FFFFFFFn
  let iy = hy & 0x7FFFFFFFn
  if ((iy | ly) == 0n) { // x**0 = 1, even if x is NaN
    return 1.0W
  } else if (
    // Either Argument is Nan
    ix > 0x7FF00000n
    || ix == 0x7FF00000n && lx != 0n
    || iy > 0x7FF00000n
    || iy == 0x7FF00000n && ly != 0n
  ) {
    use WasmF64.{ (+) }
    return x + y
  }
  let mut yisint = 0n
  let mut k = 0n
  if (hx < 0n) {
    if (iy >= 0x43400000n) {
      yisint = 2n
    } else if (iy >= 0x3FF00000n) {
      k = (iy >> 20n) - 0x3FFn
      let mut offset = 0n
      let mut _ly = 0n
      if (k > 20n) {
        offset = 52n - k
        _ly = ly
      } else {
        offset = 20n - k
        _ly = iy
      }
      let jj = _ly >> offset
      if (jj << offset == _ly) yisint = 2n - (jj & 1n)
    }
  }
  if (ly == 0n) {
    if (iy == 0x7FF00000n) { // y is +- inf
      if ((ix - 0x3FF00000n | lx) == 0n) { // C: (-1)**+-inf is 1, JS: NaN
        return NaNW
      } else if (ix >= 0x3FF00000n) { // (|x|>1)**+-inf = inf,0
        return if (hy >= 0n) y else 0.0W
      } else { // (|x|<1)**+-inf = 0,inf
        return if (hy >= 0n) 0.0W else y * -1.0W
      }
    } else if (iy == 0x3FF00000n) {
      return if (hy >= 0n) x else 1.0W / x
    } else if (hy == 0x3FE00000n) {
      return x * x
    } else if (hy == 0x3FE00000n) {
      if (hx >= 0n) {
        return WasmF64.sqrt(x)
      }
    }
  }
  let mut ax = WasmF64.abs(x)
  let mut z = 0.0W
  if (lx == 0n && (ix == 0n || ix == 0x7FF00000n || ix == 0x3FF00000n)) {
    z = ax
    if (hy < 0n) z = 1.0W / z
    if (hx < 0n) {
      if ((ix - 0x3FF00000n | yisint) == 0n) {
        use WasmF64.{ (-) }
        let d = z - z
        z = d / d
      } else if (yisint == 1n) {
        z *= -1.0W
      }
    }
    return z
  }
  let mut s = 1.0W
  if (hx < 0n) {
    if (yisint == 0n) {
      return NaNW
    } else if (yisint == 1n) {
      s = -1.0W
    }
  }
  let mut t1 = 0.0W
  and t2 = 0.0W
  and p_h = 0.0W
  and p_l = 0.0W
  and r = 0.0W
  and t = 0.0W
  and u = 0.0W
  and v = 0.0W
  and w = 0.0W
  let mut j = 0n
  and n = 0n
  if (iy > 0x41E00000n) {
    if (iy > 0x43F00000n) {
      if (ix <= 0x3FEFFFFFn) {
        let output = if (hy < 0n) huge * huge else tiny * tiny
        return output
      } else if (ix >= 0x3FF00000n) {
        let output = if (hy > 0n) huge * huge else tiny * tiny
        return output
      }
    }
    if (ix < 0x3FEFFFFFn) {
      if (hy < 0n) {
        return s * huge * huge
      } else {
        return s * tiny * tiny
      }
    } else if (ix > 0x3FF00000n) {
      if (hy > 0n) {
        return s * huge * huge
      } else {
        return s * tiny * tiny
      }
    } else {
      use WasmF64.{ (-), (+) }
      use WasmI64.{ (&) }
      t = ax - 1.0W
      w = t * t * (0.5W - t * (inv3 - t * 0.25W))
      u = ivln2_h * t
      v = t * ivln2_l - w * ivln2
      t1 = u + v
      t1 = WasmF64.reinterpretI64(
        WasmI64.reinterpretF64(t1) & 0xFFFFFFFF00000000N
      )
      t2 = v - (t1 - u)
    }
  } else {
    let mut ss = 0.0W
    and s2 = 0.0W
    and s_h = 0.0W
    and s_l = 0.0W
    and t_h = 0.0W
    and t_l = 0.0W
    n = 0n
    if (ix < 0x00100000n) {
      use WasmI64.{ (>>>) }
      ax *= two53
      n -= 53n
      ix = WasmI32.wrapI64(WasmI64.reinterpretF64(ax) >>> 32N)
    }
    n += (ix >> 20n) - 0x3FFn
    j = ix & 0x000FFFFFn
    ix = j | 0x3FF00000n
    if (j <= 0x3988En) {
      k = 0n
    } else if (j < 0xBB67An) {
      k = 1n
    } else {
      k = 0n
      n += 1n
      ix -= 0x00100000n
    }
    use WasmI64.{ (&), (|), (<<) }
    ax = WasmF64.reinterpretI64(
      WasmI64.reinterpretF64(ax) & 0xFFFFFFFFN | WasmI64.extendI32S(ix) << 32N
    )
    let bp = if (k != 0n) 1.5W else 1.0W
    use WasmF64.{ (+), (-) }
    u = ax - bp
    v = 1.0W / (ax + bp)
    ss = u * v
    s_h = ss
    s_h = WasmF64.reinterpretI64(
      WasmI64.reinterpretF64(s_h) & 0xFFFFFFFF00000000N
    )
    use WasmI32.{ (+), (|), (<<) as shlWasmI64 }
    t_h = WasmF64.reinterpretI64(
      WasmI64.extendI32S(
        (ix >> 1n | 0x20000000n) + 0x00080000n + shlWasmI64(k, 18n)
      )
        << 32N
    )
    use WasmF64.{ (+) }
    t_l = ax - (t_h - bp)
    s_l = v * (u - s_h * t_h - s_h * t_l)
    s2 = ss * ss
    //formatter-ignore
    r = s2 * s2 * (l1 + s2 * (l2 + s2 * (l3 + s2 * (l4 + s2 * (l5 + s2 * l6)))))
    r += s_l * (s_h + ss)
    s2 = s_h * s_h
    t_h = 3.0W + s2 + r
    t_h = WasmF64.reinterpretI64(
      WasmI64.reinterpretF64(t_h) & 0xFFFFFFFF00000000N
    )
    t_l = r - (t_h - 3.0W - s2)
    u = s_h * t_h
    v = s_l * t_h + t_l * ss
    p_h = u + v
    p_h = WasmF64.reinterpretI64(
      WasmI64.reinterpretF64(p_h) & 0xFFFFFFFF00000000N
    )
    p_l = v - (p_h - u)
    let z_h = cp_h * p_h
    let dp_l = if (k != 0n) dp_l1 else 0.0W
    let z_l = cp_l * p_h + p_l * cp + dp_l
    t = WasmF64.convertI32S(n)
    let dp_h = if (k != 0n) dp_h1 else 0.0W
    t1 = z_h + z_l + dp_h + t
    t1 = WasmF64.reinterpretI64(
      WasmI64.reinterpretF64(t1) & 0xFFFFFFFF00000000N
    )
    t2 = z_l - (t1 - t - dp_h - z_h)
  }
  use WasmF64.{ (>), (-), (+) }
  use WasmI64.{ (&), (>>), (<<) }
  let y1 = WasmF64.reinterpretI64(
    WasmI64.reinterpretF64(y) & 0xFFFFFFFF00000000N
  )
  p_l = (y - y1) * t1 + y * t2
  p_h = y1 * t1
  z = p_l + p_h
  let u_ = WasmI64.reinterpretF64(z)
  let j = WasmI32.wrapI64(u_ >> 32N)
  let i = WasmI32.wrapI64(u_)
  use WasmI32.{ (-) as addWasmI32, (&) }
  if (j >= 0x40900000n) {
    if ((addWasmI32(j, 0x40900000n) | i) != 0n || p_l + ovt > z - p_h) {
      return s * huge * huge
    }
  } else if ((j & 0x7FFFFFFFn) >= 0x4090CC00n) {
    use WasmF64.{ (<=) }
    if (addWasmI32(j, 0xC090CC00n | i) != 0n || p_l <= z - p_h) {
      return s * tiny * tiny
    }
  }
  use WasmI32.{ (&), (>>), (-), (+), (>), (*), (<<), (^) }
  let i = j & 0x7FFFFFFFn
  k = (i >> 20n) - 0x3FFn
  n = 0n
  if (i > 0x3FE00000n) {
    use WasmI64.{ (<<) }
    n = j + (0x00100000n >> (k + 1n))
    k = ((n & 0x7FFFFFFFn) >> 20n) - 0x3FFn
    t = 0.0W
    t = WasmF64.reinterpretI64(
      WasmI64.extendI32S(n & (0x000FFFFFn >> k ^ -1n)) << 32N
    )
    n = (n & 0x000FFFFFn | 0x00100000n) >> (20n - k)
    if (j < 0n) n *= -1n
    use WasmF64.{ (-) }
    p_h -= t
  }
  use WasmI64.{ (&), (|) }
  use WasmF64.{ (*), (+), (-) }
  t = p_l + p_h
  t = WasmF64.reinterpretI64(WasmI64.reinterpretF64(t) & 0xFFFFFFFF00000000N)
  u = t * lg2_h
  v = (p_l - (t - p_h)) * lg2 + t * lg2_l
  z = u + v
  w = v - (z - u)
  t = z * z
  t1 = z - t * (p1 + t * (p2 + t * (p3 + t * (p4 + t * p5))))
  r = z * t1 / (t1 - 2.0W) - (w + z * w)
  z = 1.0W - (r - z)
  use WasmI32.{ (+) }
  let j = WasmI32.wrapI64(shrSWasmI64(WasmI64.reinterpretF64(z), 32N))
    + (n << 20n)
  if (j >> 20n <= 0n) {
    z = scalbn(z, n)
  } else {
    use WasmI64.{ (<<) }
    z = WasmF64.reinterpretI64(
      WasmI64.reinterpretF64(z) & 0xFFFFFFFFN | WasmI64.extendI32S(j) << 32N
    )
  }
  return s * z
}

// Math.pow is largely based on https://git.musl-libc.org/cgit/musl/tree/src/math/pow.c
/*
 * ====================================================
 * Copyright (C) 2004 by Sun Microsystems, Inc. All rights reserved.
 *
 * Permission to use, copy, modify, and distribute this
 * software is freely granted, provided that this notice
 * is preserved.
 * ====================================================
 */
/**
 * Computes the exponentiation of the given base and power.
 *
 * @param base: The base number
 * @param power: The exponent number
 * @returns The base raised to the given power
 *
 * @since v0.6.0
 * @history v0.5.4: Originally existed in Number module
 */
@unsafe
provide let (**) = (base, power) => {
  let (==) = numberEq
  let (!=) = (x, y) => !numberEq(x, y)
  let basePtr = WasmI32.fromGrain(base)
  let powerPtr = WasmI32.fromGrain(power)
  let result = if (base == 1 && power != 0) {
    1
  } else if (isInteger(basePtr) && isInteger(powerPtr)) {
    if (power < 0)
      expBySquaring(1, 1 / base, power * -1)
    else
      expBySquaring(1, base, power)
  } else if (isRational(basePtr) && isInteger(powerPtr)) {
    // Apply expBySquaring to numerator and denominator
    let numerator = WasmI32.fromGrain(base)
    Memory.incRef(numerator)
    let numerator = WasmI32.toGrain(numerator): Rational
    let numerator = rationalNumerator(numerator)
    let denominator = WasmI32.fromGrain(base)
    Memory.incRef(denominator)
    let denominator = WasmI32.toGrain(denominator): Rational
    let denominator = rationalDenominator(denominator)
    let numerator = if (power < 0)
      expBySquaring(1, 1 / numerator, power * -1)
    else
      expBySquaring(1, numerator, power)
    let denominator = if (power < 0)
      expBySquaring(1, 1 / denominator, power * -1)
    else
      expBySquaring(1, denominator, power)
    numerator / denominator
  } else {
    let x = coerceNumberToWasmF64(base)
    let y = coerceNumberToWasmF64(power)
    WasmI32.toGrain(newFloat64(powf(x, y))): Number
  }

  ignore(base)
  ignore(power)

  // This return is never hit but is here because we need to hold on to references
  return result
}
