///|
/// The operators: `⊙̂(v, v')` and `⊖̂(v)`.
///
/// Arithmetic is Python's, with three things this port has to supply that
/// MoonBit does not:
///
/// * **`//` and `%` floor.** MoonBit's `/` and `%` on `BigInt` and `Int64`
/// truncate toward zero; Python's floor toward negative infinity, so the
/// remainder takes the sign of the DIVISOR: `-7 // 2` is `-4` and
/// `7 % -2` is `-1`.
/// * **`bool` is not a number.** `True + 1` and `True < 2` are undefined,
/// even though Python accepts both. That is the same rule that makes
/// `True == 1` undefined, and the conformance suite has a test for it.
/// * **An integer and a float compare exactly**, not by converting the
/// integer, so a large integer is not made equal to the double nearest it.
///
/// The spec leaves arithmetic as "as in Python" with one sentence added:
/// TypeError where an operand is not a number. So `"a" + "b"` aborts here
/// rather than concatenating. Nothing in the conformance suite reaches it,
/// and the decision is written down rather than guessed at each call site.
pub fn binop(op : @ast.Operator, a : Value, b : Value) -> Outcome {
match (as_number(a), as_number(b)) {
(Some(x), Some(y)) => arith(op, x, y)
_ => Aborts(TypeError)
}
}
///|
/// A number, as one of the two things arithmetic works on. A `bool` is not
/// one, which is the point.
priv enum Num {
I(BigInt)
F(Double)
}
///|
fn as_number(v : Value) -> Num? {
match v {
Int(n) => Some(I(n))
Float(d) => Some(F(d))
_ => None
}
}
///|
/// A BigInt as a double, correctly rounded -- which is what Python's
/// `float(n)` gives. MoonBit's `BigInt` has no conversion, so it goes through
/// the decimal text, which `parse_double` rounds correctly.
pub fn big_to_double(n : BigInt) -> Double {
@string.parse_double(n.to_string()) catch {
// Out of a double's range: Python raises OverflowError, which is not a
// termination kind, so the caller treats it as undefined.
_ => if n < 0N { @double.neg_infinity } else { @double.infinity }
}
}
///|
fn to_double(x : Num) -> Double {
match x {
I(n) => big_to_double(n)
F(d) => d
}
}
///|
fn arith(op : @ast.Operator, a : Num, b : Num) -> Outcome {
match (a, b) {
(I(x), I(y)) => int_arith(op, x, y)
_ => float_arith(op, to_double(a), to_double(b))
}
}
///|
fn int_arith(op : @ast.Operator, x : BigInt, y : BigInt) -> Outcome {
match op {
Add => Val(Int(x + y))
Sub => Val(Int(x - y))
Mult => Val(Int(x * y))
Div =>
if y == 0N {
Aborts(ZeroDivisionError)
} else {
Val(Float(big_to_double(x) / big_to_double(y)))
}
FloorDiv =>
if y == 0N {
Aborts(ZeroDivisionError)
} else {
Val(Int(floor_div(x, y)))
}
Mod =>
if y == 0N {
Aborts(ZeroDivisionError)
} else {
Val(Int(floor_mod(x, y)))
}
Pow =>
if y < 0N {
// A negative exponent gives a float, and `0 ** -1` divides by zero.
if x == 0N {
Aborts(ZeroDivisionError)
} else {
Val(Float(@math.pow(big_to_double(x), big_to_double(y))))
}
} else {
Val(Int(x.pow(y)))
}
_ => Stuck("binary operator '" + op.symbol() + "'")
}
}
///|
/// Python's `//` on integers: floor, not truncation.
pub fn floor_div(x : BigInt, y : BigInt) -> BigInt {
let q = x / y
// MoonBit truncates toward zero. When the signs differ and the division was
// not exact, the floor is one lower.
if x % y != 0N && (x < 0N) != (y < 0N) {
q - 1N
} else {
q
}
}
///|
/// Python's `%` on integers: the remainder takes the sign of the divisor.
pub fn floor_mod(x : BigInt, y : BigInt) -> BigInt {
let r = x % y
if r != 0N && (r < 0N) != (y < 0N) {
r + y
} else {
r
}
}
///|
fn float_arith(op : @ast.Operator, x : Double, y : Double) -> Outcome {
match op {
Add => Val(Float(x + y))
Sub => Val(Float(x - y))
Mult => Val(Float(x * y))
Div => if y == 0.0 { Aborts(ZeroDivisionError) } else { Val(Float(x / y)) }
FloorDiv =>
if y == 0.0 {
Aborts(ZeroDivisionError)
} else {
Val(Float((x / y).floor()))
}
Mod =>
if y == 0.0 {
Aborts(ZeroDivisionError)
} else {
Val(Float(float_mod(x, y)))
}
Pow =>
// A negative base with a fractional exponent is a complex number in
// mathematics and a ValueError in Python; neither is a termination
// kind, so it has no rule here.
if x < 0.0 && y != y.floor() {
Stuck("a negative number raised to a fractional power")
} else {
Val(Float(@math.pow(x, y)))
}
_ => Stuck("binary operator '" + op.symbol() + "'")
}
}
///|
/// Python's `%` on floats: the result takes the sign of the divisor.
pub fn float_mod(x : Double, y : Double) -> Double {
let r = x - (x / y).floor() * y
// The subtraction can land exactly on the divisor through rounding; Python
// never returns a remainder equal to the divisor.
if r != 0.0 && (r < 0.0) != (y < 0.0) {
r + y
} else {
r
}
}
// ---------------------------------------------------------------------------
///|
/// `not`, unary `+` and unary `-`.
pub fn unop(op : @ast.UnaryOp, v : Value) -> Outcome {
match op {
Not =>
match v {
Bool(b) => Val(Bool(!b))
// Truthiness is Python's, not PurePy's: `not 1` has no rule.
_ => Stuck("'not' applied to " + v.kind_name())
}
UAdd =>
match v {
Int(_) | Float(_) => Val(v)
_ => Aborts(TypeError)
}
USub =>
match v {
Int(n) => Val(Int(-n))
Float(d) => Val(Float(-d))
_ => Aborts(TypeError)
}
Invert => Stuck("unary operator '~'")
}
}
// ---------------------------------------------------------------------------
///|
/// `==`, `!=`, `in`, `not in` and the four orderings.
pub fn compare(op : @ast.CmpOp, a : Value, b : Value) -> Outcome {
match op {
// The `==` operator has a rule where `eq` has none: two nans compare
// False rather than being undefined.
Eq =>
if both_nan(a, b) {
Val(Bool(false))
} else {
match eq(a, b) {
Some(r) => Val(Bool(r))
None => Stuck("== between " + a.kind_name() + " and " + b.kind_name())
}
}
NotEq =>
if both_nan(a, b) {
Val(Bool(true))
} else {
match eq(a, b) {
Some(r) => Val(Bool(!r))
None => Stuck("!= between " + a.kind_name() + " and " + b.kind_name())
}
}
In =>
match contains(b, a) {
Some(r) => Val(Bool(r))
None => Stuck("'in' over " + b.kind_name())
}
NotIn =>
match contains(b, a) {
Some(r) => Val(Bool(!r))
None => Stuck("'not in' over " + b.kind_name())
}
Lt | LtE | Gt | GtE =>
match order(a, b) {
Some(c) =>
Val(
Bool(
match op {
Lt => c < 0
LtE => c <= 0
Gt => c > 0
_ => c >= 0
},
),
)
None =>
Stuck(
"'" +
op.symbol() +
"' between " +
a.kind_name() +
" and " +
b.kind_name(),
)
}
// The sieve rejects `is` and `is not` as not yet supported (#81).
Is | IsNot => Stuck("identity operator '" + op.symbol() + "'")
}
}
///|
fn both_nan(a : Value, b : Value) -> Bool {
match (a, b) {
(Float(x), Float(y)) => x.is_nan() && y.is_nan()
_ => false
}
}
///|
/// Three-way comparison, `None` where the two values have no order.
///
/// Numbers with numbers, strings by code point, lists with lists and tuples
/// with tuples lexicographically. A `nan` anywhere makes the pair unordered,
/// which is how Python's comparisons all come out False.
pub fn order(a : Value, b : Value) -> Int? {
match (a, b) {
(Int(x), Int(y)) => Some(x.compare(y))
(Float(x), Float(y)) =>
if x.is_nan() || y.is_nan() {
None
} else if x < y {
Some(-1)
} else if x > y {
Some(1)
} else {
Some(0)
}
(Int(x), Float(y)) => int_float_order(x, y)
(Float(x), Int(y)) =>
match int_float_order(y, x) {
Some(c) => Some(-c)
None => None
}
(Str(x), Str(y)) => Some(@basic.lexical_compare(x, y))
(List(xs), List(ys)) | (Tuple(xs), Tuple(ys)) => order_elems(xs, ys)
_ => None
}
}
///|
/// An integer against a float, exactly.
fn int_float_order(n : BigInt, d : Double) -> Int? {
if d.is_nan() {
return None
}
if d.is_pos_inf() {
return Some(-1)
}
if d.is_neg_inf() {
return Some(1)
}
let f = d.floor()
match exact_integer(f) {
None => None
Some(m) => {
let c = n.compare(m)
if c != 0 {
Some(c)
} else if d > f {
// The integer equals the floor, and the float has a fraction above it.
Some(-1)
} else {
Some(0)
}
}
}
}
///|
/// Lexicographic order over two sequences, as Python compares them: the first
/// unequal pair decides, and otherwise the shorter is smaller.
fn order_elems(xs : Array[Value], ys : Array[Value]) -> Int? {
let n = if xs.length() < ys.length() { xs.length() } else { ys.length() }
for i in 0.. return None
Some(true) => ()
Some(false) =>
return match order(xs[i], ys[i]) {
Some(c) => Some(c)
None => None
}
}
}
if xs.length() == ys.length() {
Some(0)
} else if xs.length() < ys.length() {
Some(-1)
} else {
Some(1)
}
}