///|
/// The predefined functions of Figure 2.7.
///
/// The figure gives `len`, `range` and `sys.exit` as partial maps from
/// arguments to outcomes, and a call outside a map's domain has no
/// derivation. So `len` has an explicit "otherwise aborts TypeError" and
/// `range` does not: `range("x")` is undefined, while `len(1)` aborts. The
/// asymmetry is the figure's, not this port's.
///
/// The figure leaves `print` and the `math` functions without a definition.
/// They are implemented as Python implements them, since CPython is the
/// oracle for what a run prints.
async fn Interp::call_primitive(
  self : Interp,
  p : @value.Primitive,
  args : Array[Value],
) -> Outcome noraise {
  match p {
    Print => {
      let parts : Array[String] = []
      for a in args {
        match a.str() {
          Some(t) => parts.push(t)
          // A closure, a module or a class has no printable form: Python
          // prints an address, which no implementation can reproduce.
          None => return Stuck("printing " + a.kind_name())
        }
      }
      self.write(parts.join(" ") + "\n")
      Val(None)
    }
    Len =>
      if args.length() != 1 {
        Aborts(TypeError)
      } else {
        match @value.iter(args[0]) {
          Some(xs) => Val(Int(BigInt::from_int(xs.length())))
          None => Aborts(TypeError)
        }
      }
    Range =>
      if args.length() != 1 {
        Stuck("range with \{args.length()} arguments")
      } else {
        match args[0] {
          Int(n) => {
            let out : Array[Value] = []
            let mut i = 0N
            while i < n {
              out.push(Int(i))
              i = i + 1N
            }
            Val(List(out))
          }
          v => Stuck("range of " + v.kind_name())
        }
      }
    Exit =>
      if args.is_empty() {
        Aborts(SystemExit(0N))
      } else if args.length() == 1 {
        match args[0] {
          Int(n) => Aborts(SystemExit(n))
          v => Stuck("sys.exit of " + v.kind_name())
        }
      } else {
        Stuck("sys.exit with \{args.length()} arguments")
      }
    // `math.floor` and `math.ceil` return an integer in Python; the rest
    // return a float.
    MathFloor | MathCeil => {
      let x = match numeric(args) {
        Some(d) => d
        None => return Stuck("a math function of the wrong shape")
      }
      let r = if p is MathFloor { x.floor() } else { @math.ceil(x) }
      match @value.exact_integer(r) {
        Some(n) => Val(Int(n))
        None => Stuck("floor or ceil of a value with no integer")
      }
    }
    Sqrt | Exp | Log | Sin | Cos | Tan => {
      let x = match numeric(args) {
        Some(d) => d
        None => return Stuck("a math function of the wrong shape")
      }
      match p {
        // `sqrt` of a negative number and `log` of a non-positive one raise
        // ValueError in Python, which is not a termination kind.
        Sqrt =>
          if x < 0.0 {
            Stuck("the square root of a negative number")
          } else {
            Val(Float(x.sqrt()))
          }
        Log =>
          if x <= 0.0 {
            Stuck("the logarithm of a number that is not positive")
          } else {
            Val(Float(@math.ln(x)))
          }
        Exp => Val(Float(@math.exp(x)))
        Sin => Val(Float(@math.sin(x)))
        Cos => Val(Float(@math.cos(x)))
        _ => Val(Float(@math.tan(x)))
      }
    }
    // `typing.Any`, `typing.Callable` and `dataclasses.dataclass` are values
    // so that importing them binds something. They are never called.
    Opaque(name) => Stuck("calling " + name)
    // A function the host supplies. It answers by name, and what it cannot
    // answer is an operation the semantics does not cover -- the same as any
    // other undefined operation, and reported the same way.
    Foreign(name) => (self.host.call)(name, args)
  }
}

///|
/// The single numeric argument a `math` function takes, as a double. An
/// integer is converted, as Python converts it.
fn numeric(args : Array[Value]) -> Double? {
  if args.length() != 1 {
    return None
  }
  match args[0] {
    Float(d) => Some(d)
    Int(n) => Some(@value.big_to_double(n))
    _ => None
  }
}