///|
/// math module builtins.

///|
fn builtin_math_sqrt(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.sqrt", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.sqrt() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.pow(x, 0.5)))
}

///|
fn builtin_math_pow(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.pow", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 2 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.pow() takes exactly two arguments".to_string(),
      ),
    )
  }
  let (_, a) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let (_, b) = match number_value(positional[1]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.pow(a, b)))
}

///|
fn builtin_math_log(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.log", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 && positional.length() != 2 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.log() takes 1 or 2 arguments".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let mut result = @math.ln(x)
  if positional.length() == 2 {
    let (_, base) = match number_value(positional[1]) {
      Ok(v) => v
      Err(err) => return Err(err)
    }
    result = result / @math.ln(base)
  }
  Ok(Value::Float(result))
}

///|
fn builtin_math_log2(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.log2", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.log2() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.log2(x)))
}

///|
fn builtin_math_exp(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.exp", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.exp() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.exp(x)))
}

///|
fn builtin_math_floor(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.floor", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.floor() takes exactly one argument".to_string(),
      ),
    )
  }
  match positional[0] {
    Value::Int(v) => Ok(Value::Int(v))
    Value::Bool(v) => Ok(Value::Int(if v { 1N } else { 0N }))
    Value::Float(v) =>
      Ok(Value::Int(@bigint.BigInt::from_int64(@math.floor(v).to_int64())))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.floor() expects a number".to_string(),
        ),
      )
  }
}

///|
fn builtin_math_trunc(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = match ensure_no_keywords("math.trunc", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.trunc() takes exactly one argument".to_string(),
      ),
    )
  }
  match positional[0] {
    Value::Int(v) => Ok(Value::Int(v))
    Value::Bool(v) => Ok(Value::Int(if v { 1N } else { 0N }))
    Value::Float(v) => Ok(Value::Int(@bigint.BigInt::from_int64(v.to_int64())))
    Value::Instance(_) => {
      let trunc_fn = match
        get_attr_from_value(positional[0], "__trunc__", globals, builtins, io) {
        Ok(v) => v
        Err(err) => return Err(err)
      }
      let value = match
        call_callable_with_env(trunc_fn, [], [], globals, builtins, io) {
        Ok(v) => v
        Err(err) => return Err(err)
      }
      match value {
        Value::Int(v) => Ok(Value::Int(v))
        Value::Bool(v) => Ok(Value::Int(if v { 1N } else { 0N }))
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "__trunc__ returned non-int".to_string(),
            ),
          )
      }
    }
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.trunc() expects a real number".to_string(),
        ),
      )
  }
}

///|
fn builtin_math_modf(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.modf", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.modf() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if x.is_nan() {
    return Ok(Value::Tuple([Value::Float(x), Value::Float(x)]))
  }
  if x.is_inf() {
    let frac = if x < 0.0 { -0.0 } else { 0.0 }
    return Ok(Value::Tuple([Value::Float(frac), Value::Float(x)]))
  }
  let int_part = @math.trunc(x)
  let mut frac = x - int_part
  // CPython preserves the sign of zero in the fractional part.
  if frac == 0.0 {
    let sign_is_neg = x < 0.0 ||
      (x == 0.0 && (1.0 / x).is_inf() && 1.0 / x < 0.0)
    frac = if sign_is_neg { -0.0 } else { 0.0 }
  }
  Ok(Value::Tuple([Value::Float(frac), Value::Float(int_part)]))
}

///|
fn builtin_math_frexp(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = match ensure_no_keywords("math.frexp", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.frexp() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if x == 0.0 {
    return Ok(Value::Tuple([Value::Float(0.0), Value::Int(0N)]))
  }
  if x.is_nan() || x.is_inf() {
    return Ok(Value::Tuple([Value::Float(x), Value::Int(0N)]))
  }
  let ax = if x < 0.0 { -x } else { x }
  let mut exp : Int64 = @math.floor(@math.log2(ax)).to_int64() + 1L
  let mut mant = x / @math.pow(2.0, exp.to_double())
  let mut abs_mant = if mant < 0.0 { -mant } else { mant }
  if abs_mant < 0.5 {
    mant = mant * 2.0
    exp = exp - 1L
  } else if abs_mant >= 1.0 {
    mant = mant / 2.0
    exp = exp + 1L
  }
  abs_mant = if mant < 0.0 { -mant } else { mant }
  if abs_mant < 0.5 {
    mant = mant * 2.0
    exp = exp - 1L
  } else if abs_mant >= 1.0 {
    mant = mant / 2.0
    exp = exp + 1L
  }
  Ok(
    Value::Tuple([
      Value::Float(mant),
      Value::Int(@bigint.BigInt::from_int64(exp)),
    ]),
  )
}

///|
fn builtin_math_ldexp(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = match ensure_no_keywords("math.ldexp", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 2 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.ldexp() takes exactly two arguments".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let exp_i64 = match positional[1] {
    Value::Int(v) => v.to_int64()
    Value::Bool(v) => if v { 1L } else { 0L }
    _ =>
      return Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.ldexp() expects an integer exponent".to_string(),
        ),
      )
  }
  Ok(Value::Float(x * @math.pow(2.0, exp_i64.to_double())))
}

///|
fn builtin_math_ceil(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.ceil", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.ceil() takes exactly one argument".to_string(),
      ),
    )
  }
  match positional[0] {
    Value::Int(v) => Ok(Value::Int(v))
    Value::Bool(v) => Ok(Value::Int(if v { 1N } else { 0N }))
    Value::Float(v) =>
      Ok(Value::Int(@bigint.BigInt::from_int64(@math.ceil(v).to_int64())))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.ceil() expects a number".to_string(),
        ),
      )
  }
}

///|
fn builtin_math_acos(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.acos", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.acos() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.acos(x)))
}

///|
fn builtin_math_cos(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.cos", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.cos() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.cos(x)))
}

///|
fn builtin_math_sin(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.sin", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.sin() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.sin(x)))
}

///|
fn builtin_math_hypot(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.hypot", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 2 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.hypot() takes exactly two arguments".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let (_, y) = match number_value(positional[1]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(@math.hypot(x, y)))
}

///|
fn builtin_math_fabs(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.fabs", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.fabs() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let value = if x < 0.0 { -x } else { x }
  Ok(Value::Float(value))
}

///|
fn erf_approx(x : Double) -> Double {
  let p = 0.3275911
  let a1 = 0.254829592
  let a2 = -0.284496736
  let a3 = 1.421413741
  let a4 = -1.453152027
  let a5 = 1.061405429
  let sign = if x < 0.0 { -1.0 } else { 1.0 }
  let ax = if x < 0.0 { -x } else { x }
  let t = 1.0 / (1.0 + p * ax)
  let y = 1.0 -
    ((((a5 * t + a4) * t + a3) * t + a2) * t + a1) * t * @math.exp(-ax * ax)
  sign * y
}

///|
fn lgamma_lanczos(x : Double) -> Double {
  let c1 = 76.18009172947146
  let c2 = -86.50532032941677
  let c3 = 24.01409824083091
  let c4 = -1.231739572450155
  let c5 = 0.001208650973866179
  let c6 = -0.000005395239384953
  let mut y = x
  let mut ser = 1.000000000190015
  y = y + 1.0
  ser = ser + c1 / y
  y = y + 1.0
  ser = ser + c2 / y
  y = y + 1.0
  ser = ser + c3 / y
  y = y + 1.0
  ser = ser + c4 / y
  y = y + 1.0
  ser = ser + c5 / y
  y = y + 1.0
  ser = ser + c6 / y
  let tmp = x + 5.5
  let log_tmp = (x + 0.5) * @math.ln(tmp) - tmp
  log_tmp + @math.ln(2.5066282746310005 * ser / x)
}

///|
fn lgamma_approx(x : Double) -> Double {
  let pi = 3.141592653589793
  if x < 0.5 {
    let sinpi = @math.sin(pi * x)
    let abs_sinpi = if sinpi < 0.0 { -sinpi } else { sinpi }
    if abs_sinpi == 0.0 {
      return 1.0 / 0.0
    }
    @math.ln(pi) - @math.ln(abs_sinpi) - lgamma_lanczos(1.0 - x)
  } else {
    lgamma_lanczos(x)
  }
}

///|
fn builtin_math_erf(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.erf", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.erf() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(erf_approx(x)))
}

///|
fn builtin_math_lgamma(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.lgamma", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.lgamma() takes exactly one argument".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  Ok(Value::Float(lgamma_approx(x)))
}

///|
fn builtin_math_fsum(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = match ensure_no_keywords("math.fsum", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.fsum() takes exactly one argument".to_string(),
      ),
    )
  }
  let items = match iterable_values(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let mut total = 0.0
  for item in items {
    let (_, value) = match number_value(item) {
      Ok(v) => v
      Err(err) => return Err(err)
    }
    total = total + value
  }
  Ok(Value::Float(total))
}

///|
fn builtin_math_sumprod(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = match ensure_no_keywords("math.sumprod", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 2 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.sumprod() takes exactly two arguments".to_string(),
      ),
    )
  }
  let items_a = match iterable_values(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let items_b = match iterable_values(positional[1]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if items_a.length() != items_b.length() {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Runtime,
        "ValueError: sumprod() arguments have different lengths".to_string(),
      ),
    )
  }
  let mut total = 0.0
  for i = 0; i < items_a.length(); i = i + 1 {
    let (_, a) = match number_value(items_a[i]) {
      Ok(v) => v
      Err(err) => return Err(err)
    }
    let (_, b) = match number_value(items_b[i]) {
      Ok(v) => v
      Err(err) => return Err(err)
    }
    total = total + a * b
  }
  Ok(Value::Float(total))
}

///|
fn builtin_math_isfinite(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.isfinite", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.isfinite() takes exactly one argument".to_string(),
      ),
    )
  }
  match positional[0] {
    Value::Float(v) => Ok(Value::Bool(!v.is_nan() && !v.is_inf()))
    Value::Int(_) | Value::Bool(_) => Ok(Value::Bool(true))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.isfinite() expects a number".to_string(),
        ),
      )
  }
}

///|
fn builtin_math_isnan(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.isnan", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.isnan() takes exactly one argument".to_string(),
      ),
    )
  }
  match positional[0] {
    Value::Float(v) => Ok(Value::Bool(v.is_nan()))
    Value::Int(_) | Value::Bool(_) => Ok(Value::Bool(false))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.isnan() expects a number".to_string(),
        ),
      )
  }
}

///|
fn builtin_math_isinf(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.isinf", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.isinf() takes exactly one argument".to_string(),
      ),
    )
  }
  match positional[0] {
    Value::Float(v) => Ok(Value::Bool(v.is_inf()))
    Value::Int(_) | Value::Bool(_) => Ok(Value::Bool(false))
    _ =>
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.isinf() expects a number".to_string(),
        ),
      )
  }
}

///|
fn builtin_math_isclose(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io

  // Signature: isclose(a, b, *, rel_tol=1e-09, abs_tol=0.0)
  if positional.length() < 2 || positional.length() > 4 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.isclose() takes 2 positional arguments but " +
        positional.length().to_string() +
        " were given".to_string(),
      ),
    )
  }
  let (_, a) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let (_, b) = match number_value(positional[1]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let mut rel_tol : Double = 0.000000001
  let mut abs_tol : Double = 0.0
  let mut has_kw_rel = false
  let mut has_kw_abs = false
  if positional.length() >= 3 {
    let (_, v) = match number_value(positional[2]) {
      Ok(x) => x
      Err(err) => return Err(err)
    }
    rel_tol = v
  }
  if positional.length() >= 4 {
    let (_, v) = match number_value(positional[3]) {
      Ok(x) => x
      Err(err) => return Err(err)
    }
    abs_tol = v
  }
  for kv in keywords {
    match kv.0 {
      "rel_tol" => {
        if positional.length() >= 3 || has_kw_rel {
          return Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "math.isclose() got multiple values for argument 'rel_tol'".to_string(),
            ),
          )
        }
        let (_, v) = match number_value(kv.1) {
          Ok(x) => x
          Err(err) => return Err(err)
        }
        rel_tol = v
        has_kw_rel = true
      }
      "abs_tol" => {
        if positional.length() >= 4 || has_kw_abs {
          return Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "math.isclose() got multiple values for argument 'abs_tol'".to_string(),
            ),
          )
        }
        let (_, v) = match number_value(kv.1) {
          Ok(x) => x
          Err(err) => return Err(err)
        }
        abs_tol = v
        has_kw_abs = true
      }
      _ =>
        return Err(
          make_runtime_error(
            RuntimeErrorKind::Type,
            "math.isclose() got an unexpected keyword argument '" + kv.0 + "'",
          ),
        )
    }
  }
  if rel_tol < 0.0 || abs_tol < 0.0 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Runtime,
        "ValueError: tolerances must be non-negative".to_string(),
      ),
    )
  }

  // Per CPython semantics:
  // - NaN is never close to anything (including itself).
  // - infinities are only close if equal.
  if a.is_nan() || b.is_nan() {
    return Ok(Value::Bool(false))
  }
  if a == b {
    return Ok(Value::Bool(true))
  }
  if a.is_inf() || b.is_inf() {
    return Ok(Value::Bool(false))
  }
  let diff = if a > b { a - b } else { b - a }
  let abs_a = if a < 0.0 { -a } else { a }
  let abs_b = if b < 0.0 { -b } else { b }
  let max_ab = if abs_a > abs_b { abs_a } else { abs_b }
  let tol = if rel_tol * max_ab > abs_tol { rel_tol * max_ab } else { abs_tol }
  Ok(Value::Bool(diff <= tol))
}

///|
fn builtin_math_copysign(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.copysign", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 2 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.copysign() takes exactly two arguments".to_string(),
      ),
    )
  }
  let (_, x) = match number_value(positional[0]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let (_, y) = match number_value(positional[1]) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let ax = if x == 0.0 { 0.0 } else if x < 0.0 { -x } else { x }
  let neg = if y == 0.0 { 1.0 / y < 0.0 } else { y < 0.0 }
  Ok(Value::Float(if neg { -ax } else { ax }))
}

///|
fn bigint_gcd(a : @bigint.BigInt, b : @bigint.BigInt) -> @bigint.BigInt {
  let mut x = if a < 0N { -a } else { a }
  let mut y = if b < 0N { -b } else { b }
  while y != 0N {
    let r = x % y
    x = y
    y = r
  }
  x
}

///|
fn builtin_math_gcd(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.gcd", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  let mut g = 0N
  for item in positional {
    let value = match item {
      Value::Int(v) => v
      Value::Bool(v) => if v { 1N } else { 0N }
      _ =>
        return Err(
          make_runtime_error(
            RuntimeErrorKind::Type,
            "math.gcd() expects ints".to_string(),
          ),
        )
    }
    g = bigint_gcd(g, value)
  }
  Ok(Value::Int(g))
}

///|
fn bigint_isqrt(value : @bigint.BigInt) -> @bigint.BigInt {
  if value < 2N {
    return value
  }
  let mut low = 1N
  let mut high = value
  let mut ans = 0N
  while low <= high {
    let mid = (low + high) / 2N
    let sq = mid * mid
    if sq == value {
      return mid
    }
    if sq < value {
      ans = mid
      low = mid + 1N
    } else {
      high = mid - 1N
    }
  }
  ans
}

///|
fn builtin_math_isqrt(
  positional : Array[Value],
  keywords : Array[(String, Value)],
  locals : Array[(String, Value)],
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  touch_env(locals, globals, builtins, io)
  let _ = locals
  let _ = globals
  let _ = builtins
  let _ = io
  let _ = match ensure_no_keywords("math.isqrt", keywords) {
    Ok(v) => v
    Err(err) => return Err(err)
  }
  if positional.length() != 1 {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Type,
        "math.isqrt() takes exactly one argument".to_string(),
      ),
    )
  }
  let value = match positional[0] {
    Value::Int(v) => v
    Value::Bool(v) => if v { 1N } else { 0N }
    _ =>
      return Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "math.isqrt() expects an int".to_string(),
        ),
      )
  }
  if value < 0N {
    return Err(
      make_runtime_error(
        RuntimeErrorKind::Runtime,
        "ValueError: isqrt() argument must be nonnegative".to_string(),
      ),
    )
  }
  Ok(Value::Int(bigint_isqrt(value)))
}