///|
/// Binary operator evaluation helpers.

///|
fn eval_binary_op_values(
  op : BinaryOp,
  left_val : Value,
  right_val : Value,
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  match op {
    BinaryOp::Add =>
      match (left_val, right_val) {
        (Value::Complex(ar, ai), Value::Complex(br, bi)) =>
          Ok(Value::Complex(ar + br, ai + bi))
        (Value::Complex(ar, ai), other) => {
          let (br, bi) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          Ok(Value::Complex(ar + br, ai + bi))
        }
        (other, Value::Complex(br, bi)) => {
          if other is Value::Instance(_) {
            match get_attr_from_value(other, "__add__", globals, builtins, io) {
              Ok(add_method) =>
                return call_callable_with_env(
                  add_method,
                  [Value::Complex(br, bi)],
                  [],
                  globals,
                  builtins,
                  io,
                )
              Err(_) => ()
            }
          }
          let (ar, ai) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          Ok(Value::Complex(ar + br, ai + bi))
        }
        (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a + b))
        (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b))
        (Value::Bool(a), Value::Bool(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) + (if b { 1N } else { 0N })))
        (Value::Bool(a), other) =>
          eval_binary_op_values(
            BinaryOp::Add,
            Value::Int(if a { 1N } else { 0N }),
            other,
            globals,
            builtins,
            io,
          )
        (other, Value::Bool(b)) =>
          eval_binary_op_values(
            BinaryOp::Add,
            other,
            Value::Int(if b { 1N } else { 0N }),
            globals,
            builtins,
            io,
          )
        (Value::Int(a), Value::Float(b)) => {
          let aa = match bigint_to_double_checked(a) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Float(aa + b))
        }
        (Value::Float(a), Value::Int(b)) => {
          let bb = match bigint_to_double_checked(b) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Float(a + bb))
        }
        (Value::Str(a), Value::Str(b)) => Ok(Value::Str(a + b))
        (Value::Bytes(a), Value::Bytes(b)) => {
          let merged : Array[Int32] = []
          for item in a {
            merged.push(item)
          }
          for item in b {
            merged.push(item)
          }
          Ok(Value::Bytes(merged))
        }
        (Value::ByteArray(a), Value::ByteArray(b)) => {
          let merged : Array[Int32] = []
          for item in a {
            merged.push(item)
          }
          for item in b {
            merged.push(item)
          }
          Ok(Value::ByteArray(merged))
        }
        (Value::ByteArray(a), Value::Bytes(b))
        | (Value::ByteArray(a), Value::MemoryView(b)) => {
          let merged : Array[Int32] = []
          for item in a {
            merged.push(item)
          }
          for item in b {
            merged.push(item)
          }
          Ok(Value::ByteArray(merged))
        }
        (Value::List(a), Value::List(b)) => {
          let merged : Array[Value] = []
          for item in a {
            merged.push(item)
          }
          for item in b {
            merged.push(item)
          }
          Ok(Value::List(merged))
        }
        (Value::Tuple(a), Value::Tuple(b)) => {
          let merged : Array[Value] = []
          for item in a {
            merged.push(item)
          }
          for item in b {
            merged.push(item)
          }
          Ok(Value::Tuple(merged))
        }
        _ => {
          if left_val is Value::Instance(_) {
            match
              get_attr_from_value(left_val, "__add__", globals, builtins, io) {
              Ok(add_method) =>
                return call_callable_with_env(
                  add_method,
                  [right_val],
                  [],
                  globals,
                  builtins,
                  io,
                )
              Err(_) => ()
            }
          }
          if right_val is Value::Instance(_) {
            match
              get_attr_from_value(right_val, "__radd__", globals, builtins, io) {
              Ok(radd_method) =>
                return call_callable_with_env(
                  radd_method,
                  [left_val],
                  [],
                  globals,
                  builtins,
                  io,
                )
              Err(_) => ()
            }
          }
          // Numeric subclasses (e.g. `class Temperature(int)`) may not inherit
          // `int.__add__` in the current builtins table. Fall back to treating
          // them as plain integers when they carry `$__int__`.
          match left_val {
            Value::Instance(inst) =>
              match get_named_value(inst.dict, int_storage_name) {
                Some(Value::Int(v)) =>
                  return eval_binary_op_values(
                    BinaryOp::Add,
                    Value::Int(v),
                    right_val,
                    globals,
                    builtins,
                    io,
                  )
                Some(Value::Bool(v)) =>
                  return eval_binary_op_values(
                    BinaryOp::Add,
                    Value::Int(if v { 1N } else { 0N }),
                    right_val,
                    globals,
                    builtins,
                    io,
                  )
                _ => ()
              }
            _ => ()
          }
          match right_val {
            Value::Instance(inst) =>
              match get_named_value(inst.dict, int_storage_name) {
                Some(Value::Int(v)) =>
                  return eval_binary_op_values(
                    BinaryOp::Add,
                    left_val,
                    Value::Int(v),
                    globals,
                    builtins,
                    io,
                  )
                Some(Value::Bool(v)) =>
                  return eval_binary_op_values(
                    BinaryOp::Add,
                    left_val,
                    Value::Int(if v { 1N } else { 0N }),
                    globals,
                    builtins,
                    io,
                  )
                _ => ()
              }
            _ => ()
          }
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "unsupported operand type for +".to_string(),
            ),
          )
        }
      }
    BinaryOp::Sub => {
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, "__sub__", globals, builtins, io) {
          Ok(sub_method) =>
            return call_callable_with_env(
              sub_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rsub__", globals, builtins, io) {
          Ok(rsub_method) =>
            return call_callable_with_env(
              rsub_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Complex(ar, ai), Value::Complex(br, bi)) =>
          Ok(Value::Complex(ar - br, ai - bi))
        (Value::Complex(ar, ai), other) => {
          let (br, bi) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          Ok(Value::Complex(ar - br, ai - bi))
        }
        (other, Value::Complex(br, bi)) => {
          let (ar, ai) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          Ok(Value::Complex(ar - br, ai - bi))
        }
        (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a - b))
        (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b))
        (Value::Int(a), Value::Float(b)) => {
          let aa = match bigint_to_double_checked(a) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Float(aa - b))
        }
        (Value::Float(a), Value::Int(b)) => {
          let bb = match bigint_to_double_checked(b) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Float(a - bb))
        }
        (Value::Bool(a), Value::Bool(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) - (if b { 1N } else { 0N })))
        (Value::Bool(a), Value::Int(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) - b))
        (Value::Int(a), Value::Bool(b)) =>
          Ok(Value::Int(a - (if b { 1N } else { 0N })))
        (Value::Bool(a), Value::Float(b)) =>
          Ok(Value::Float((if a { 1.0 } else { 0.0 }) - b))
        (Value::Float(a), Value::Bool(b)) =>
          Ok(Value::Float(a - (if b { 1.0 } else { 0.0 })))
        (Value::Set(left), Value::Set(right)) => {
          let out : Array[Value] = []
          for item in left {
            let idx = match set_find_index(right, item) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            if idx is None {
              out.push(item)
            }
          }
          Ok(Value::Set(out))
        }
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "unsupported operand type for -".to_string(),
            ),
          )
      }
    }
    BinaryOp::Mul => {
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, "__mul__", globals, builtins, io) {
          Ok(mul_method) =>
            return call_callable_with_env(
              mul_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rmul__", globals, builtins, io) {
          Ok(rmul_method) =>
            return call_callable_with_env(
              rmul_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Complex(ar, ai), Value::Complex(br, bi)) =>
          Ok(Value::Complex(ar * br - ai * bi, ar * bi + ai * br))
        (Value::Complex(ar, ai), other) => {
          let (br, bi) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          Ok(Value::Complex(ar * br - ai * bi, ar * bi + ai * br))
        }
        (other, Value::Complex(br, bi)) => {
          let (ar, ai) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          Ok(Value::Complex(ar * br - ai * bi, ar * bi + ai * br))
        }
        (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a * b))
        (Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b))
        (Value::Bool(a), Value::Bool(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) * (if b { 1N } else { 0N })))
        (Value::Bool(a), other) =>
          eval_binary_op_values(
            BinaryOp::Mul,
            Value::Int(if a { 1N } else { 0N }),
            other,
            globals,
            builtins,
            io,
          )
        (other, Value::Bool(b)) =>
          eval_binary_op_values(
            BinaryOp::Mul,
            other,
            Value::Int(if b { 1N } else { 0N }),
            globals,
            builtins,
            io,
          )
        (Value::Int(a), Value::Float(b)) => {
          let aa = match bigint_to_double_checked(a) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Float(aa * b))
        }
        (Value::Float(a), Value::Int(b)) => {
          let bb = match bigint_to_double_checked(b) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Float(a * bb))
        }
        (Value::Str(text), Value::Int(count))
        | (Value::Int(count), Value::Str(text)) =>
          if count <= 0N {
            Ok(Value::Str(""))
          } else {
            let buf = StringBuilder()
            let count_i = match bigint_to_int_checked(count) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            for i = 0; i < count_i; i = i + 1 {
              buf.write_string(text)
            }
            Ok(Value::Str(buf.to_string()))
          }
        (Value::Bytes(bytes), Value::Int(count))
        | (Value::Int(count), Value::Bytes(bytes)) =>
          if count <= 0N {
            Ok(Value::Bytes([]))
          } else {
            let count_i = match bigint_to_int_checked(count) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            let out : Array[Int32] = []
            for i = 0; i < count_i; i = i + 1 {
              for byte in bytes {
                out.push(byte)
              }
            }
            Ok(Value::Bytes(out))
          }
        (Value::ByteArray(bytes), Value::Int(count))
        | (Value::Int(count), Value::ByteArray(bytes)) =>
          if count <= 0N {
            Ok(Value::ByteArray([]))
          } else {
            let count_i = match bigint_to_int_checked(count) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            let out : Array[Int32] = []
            for i = 0; i < count_i; i = i + 1 {
              for byte in bytes {
                out.push(byte)
              }
            }
            Ok(Value::ByteArray(out))
          }
        (Value::List(values), Value::Int(count))
        | (Value::Int(count), Value::List(values)) =>
          if count <= 0N {
            Ok(Value::List([]))
          } else {
            let count_i = match bigint_to_int_checked(count) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            let out : Array[Value] = []
            for i = 0; i < count_i; i = i + 1 {
              for item in values {
                out.push(item)
              }
            }
            Ok(Value::List(out))
          }
        (Value::Tuple(values), Value::Int(count))
        | (Value::Int(count), Value::Tuple(values)) =>
          if count <= 0N {
            Ok(Value::Tuple([]))
          } else {
            let count_i = match bigint_to_int_checked(count) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            let out : Array[Value] = []
            for i = 0; i < count_i; i = i + 1 {
              for item in values {
                out.push(item)
              }
            }
            Ok(Value::Tuple(out))
          }
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "unsupported operand type for *".to_string(),
            ),
          )
      }
    }
    BinaryOp::Div => {
      if left_val is Value::Instance(_) {
        match
          get_attr_from_value(left_val, "__truediv__", globals, builtins, io) {
          Ok(truediv_method) =>
            return call_callable_with_env(
              truediv_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rtruediv__", globals, builtins, io) {
          Ok(rtruediv_method) =>
            return call_callable_with_env(
              rtruediv_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Complex(ar, ai), Value::Complex(br, bi)) => {
          let denom = br * br + bi * bi
          if denom == 0.0 {
            return Err(
              make_runtime_error(
                RuntimeErrorKind::ZeroDivision,
                "division by zero".to_string(),
              ),
            )
          }
          Ok(
            Value::Complex(
              (ar * br + ai * bi) / denom,
              (ai * br - ar * bi) / denom,
            ),
          )
        }
        (Value::Complex(ar, ai), other) => {
          let (br, bi) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          let denom = br * br + bi * bi
          if denom == 0.0 {
            return Err(
              make_runtime_error(
                RuntimeErrorKind::ZeroDivision,
                "division by zero".to_string(),
              ),
            )
          }
          Ok(
            Value::Complex(
              (ar * br + ai * bi) / denom,
              (ai * br - ar * bi) / denom,
            ),
          )
        }
        (other, Value::Complex(br, bi)) => {
          let (ar, ai) = match to_complex(other) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          let denom = br * br + bi * bi
          if denom == 0.0 {
            return Err(
              make_runtime_error(
                RuntimeErrorKind::ZeroDivision,
                "division by zero".to_string(),
              ),
            )
          }
          Ok(
            Value::Complex(
              (ar * br + ai * bi) / denom,
              (ai * br - ar * bi) / denom,
            ),
          )
        }
        _ => {
          let (_, left_num) = match number_value(left_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          let (_, right_num) = match number_value(right_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          if right_num == 0.0 {
            return Err(
              make_runtime_error(
                RuntimeErrorKind::ZeroDivision,
                "division by zero".to_string(),
              ),
            )
          }
          Ok(Value::Float(left_num / right_num))
        }
      }
    }
    BinaryOp::FloorDiv => {
      if left_val is Value::Instance(_) {
        match
          get_attr_from_value(left_val, "__floordiv__", globals, builtins, io) {
          Ok(floordiv_method) =>
            return call_callable_with_env(
              floordiv_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rfloordiv__", globals, builtins, io) {
          Ok(rfloordiv_method) =>
            return call_callable_with_env(
              rfloordiv_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Int(a), Value::Int(b)) => {
          if b == 0N {
            return Err(
              make_runtime_error(
                RuntimeErrorKind::ZeroDivision,
                "integer division or modulo by zero".to_string(),
              ),
            )
          }
          let q0 = a / b
          let r0 = a % b
          let q = if r0 != 0N && (r0 < 0N) != (b < 0N) { q0 - 1N } else { q0 }
          Ok(Value::Int(q))
        }
        (Value::Bool(a), Value::Bool(b)) =>
          eval_binary_op_values(
            BinaryOp::FloorDiv,
            Value::Int(if a { 1N } else { 0N }),
            Value::Int(if b { 1N } else { 0N }),
            globals,
            builtins,
            io,
          )
        (Value::Bool(a), Value::Int(b)) =>
          eval_binary_op_values(
            BinaryOp::FloorDiv,
            Value::Int(if a { 1N } else { 0N }),
            Value::Int(b),
            globals,
            builtins,
            io,
          )
        (Value::Int(a), Value::Bool(b)) =>
          eval_binary_op_values(
            BinaryOp::FloorDiv,
            Value::Int(a),
            Value::Int(if b { 1N } else { 0N }),
            globals,
            builtins,
            io,
          )
        _ => {
          let (left_is_float, left_num) = match number_value(left_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          let (right_is_float, right_num) = match number_value(right_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          if right_num == 0.0 {
            let msg = if left_is_float || right_is_float {
              "float floor division by zero".to_string()
            } else {
              "integer division or modulo by zero".to_string()
            }
            return Err(make_runtime_error(RuntimeErrorKind::ZeroDivision, msg))
          }
          Ok(Value::Float((left_num / right_num).floor()))
        }
      }
    }
    BinaryOp::Mod => {
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, "__mod__", globals, builtins, io) {
          Ok(mod_method) =>
            return call_callable_with_env(
              mod_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      // Don't bypass the '%' string-formatting protocol.
      if !(left_val is Value::Str(_)) && right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rmod__", globals, builtins, io) {
          Ok(rmod_method) =>
            return call_callable_with_env(
              rmod_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Str(format), value) => {
          let formatted = match
            format_percent_string(format, value, globals, builtins, io) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Str(formatted))
        }
        (Value::Int(a), Value::Int(b)) => {
          if b == 0N {
            return Err(
              make_runtime_error(
                RuntimeErrorKind::ZeroDivision,
                "integer modulo by zero".to_string(),
              ),
            )
          }
          let r0 = a % b
          let needs_adjust = r0 != 0N && (r0 < 0N) != (b < 0N)
          let r = if needs_adjust { r0 + b } else { r0 }
          Ok(Value::Int(r))
        }
        (Value::Bool(a), Value::Bool(b)) =>
          eval_binary_op_values(
            BinaryOp::Mod,
            Value::Int(if a { 1N } else { 0N }),
            Value::Int(if b { 1N } else { 0N }),
            globals,
            builtins,
            io,
          )
        (Value::Bool(a), Value::Int(b)) =>
          eval_binary_op_values(
            BinaryOp::Mod,
            Value::Int(if a { 1N } else { 0N }),
            Value::Int(b),
            globals,
            builtins,
            io,
          )
        (Value::Int(a), Value::Bool(b)) =>
          eval_binary_op_values(
            BinaryOp::Mod,
            Value::Int(a),
            Value::Int(if b { 1N } else { 0N }),
            globals,
            builtins,
            io,
          )
        _ => {
          let (left_is_float, left_num) = match number_value(left_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          let (right_is_float, right_num) = match number_value(right_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          if right_num == 0.0 {
            let msg = if left_is_float || right_is_float {
              "float modulo".to_string()
            } else {
              "integer modulo by zero".to_string()
            }
            return Err(make_runtime_error(RuntimeErrorKind::ZeroDivision, msg))
          }
          let r0 = left_num % right_num
          let needs_adjust = r0 != 0.0 && (r0 < 0.0) != (right_num < 0.0)
          let r = if needs_adjust { r0 + right_num } else { r0 }
          Ok(Value::Float(r))
        }
      }
    }
    BinaryOp::Pow => {
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, "__pow__", globals, builtins, io) {
          Ok(pow_method) =>
            return call_callable_with_env(
              pow_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rpow__", globals, builtins, io) {
          Ok(rpow_method) =>
            return call_callable_with_env(
              rpow_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Int(left), Value::Int(right)) =>
          if right < 0N {
            Ok(
              Value::Float(
                @math.pow(
                  match bigint_to_double_checked(left) {
                    Ok(v) => v
                    Err(err) => return Err(err)
                  },
                  match bigint_to_double_checked(right) {
                    Ok(v) => v
                    Err(err) => return Err(err)
                  },
                ),
              ),
            )
          } else {
            Ok(Value::Int(left.pow(right)))
          }
        _ => {
          let (_, left_num) = match number_value(left_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          let (_, right_num) = match number_value(right_val) {
            Ok(value) => value
            Err(err) => return Err(err)
          }
          Ok(Value::Float(@math.pow(left_num, right_num)))
        }
      }
    }
    BinaryOp::MatMul => {
      if left_val is Value::Instance(_) {
        match
          get_attr_from_value(left_val, "__matmul__", globals, builtins, io) {
          Ok(matmul_method) =>
            return call_callable_with_env(
              matmul_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rmatmul__", globals, builtins, io) {
          Ok(rmatmul_method) =>
            return call_callable_with_env(
              rmatmul_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      Err(
        make_runtime_error(
          RuntimeErrorKind::Type,
          "unsupported operand type for @".to_string(),
        ),
      )
    }
    BinaryOp::ShiftLeft => {
      if left_val is Value::Instance(_) {
        match
          get_attr_from_value(left_val, "__lshift__", globals, builtins, io) {
          Ok(lshift_method) =>
            return call_callable_with_env(
              lshift_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rlshift__", globals, builtins, io) {
          Ok(rlshift_method) =>
            return call_callable_with_env(
              rlshift_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Int(a), Value::Int(b)) => {
          let bb = match bigint_to_int_checked(b) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Int(a << bb))
        }
        (Value::Bool(a), Value::Int(b)) => {
          let bb = match bigint_to_int_checked(b) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Int((if a { 1N } else { 0N }) << bb))
        }
        (Value::Int(a), Value::Bool(b)) =>
          Ok(Value::Int(a << (if b { 1 } else { 0 })))
        (Value::Bool(a), Value::Bool(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) << (if b { 1 } else { 0 })))
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "unsupported operand type for <<".to_string(),
            ),
          )
      }
    }
    BinaryOp::ShiftRight => {
      if left_val is Value::Instance(_) {
        match
          get_attr_from_value(left_val, "__rshift__", globals, builtins, io) {
          Ok(rshift_method) =>
            return call_callable_with_env(
              rshift_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rrshift__", globals, builtins, io) {
          Ok(rrshift_method) =>
            return call_callable_with_env(
              rrshift_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Int(a), Value::Int(b)) => {
          let bb = match bigint_to_int_checked(b) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Int(a >> bb))
        }
        (Value::Bool(a), Value::Int(b)) => {
          let bb = match bigint_to_int_checked(b) {
            Ok(v) => v
            Err(err) => return Err(err)
          }
          Ok(Value::Int((if a { 1N } else { 0N }) >> bb))
        }
        (Value::Int(a), Value::Bool(b)) =>
          Ok(Value::Int(a >> (if b { 1 } else { 0 })))
        (Value::Bool(a), Value::Bool(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) >> (if b { 1 } else { 0 })))
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "unsupported operand type for >>".to_string(),
            ),
          )
      }
    }
    BinaryOp::BitAnd => {
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, "__and__", globals, builtins, io) {
          Ok(and_method) =>
            return call_callable_with_env(
              and_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rand__", globals, builtins, io) {
          Ok(rand_method) =>
            return call_callable_with_env(
              rand_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a & b))
        (Value::Bool(a), Value::Int(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) & b))
        (Value::Int(a), Value::Bool(b)) =>
          Ok(Value::Int(a & (if b { 1N } else { 0N })))
        (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a && b))
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "unsupported operand type for &".to_string(),
            ),
          )
      }
    }
    BinaryOp::BitXor => {
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, "__xor__", globals, builtins, io) {
          Ok(xor_method) =>
            return call_callable_with_env(
              xor_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match
          get_attr_from_value(right_val, "__rxor__", globals, builtins, io) {
          Ok(rxor_method) =>
            return call_callable_with_env(
              rxor_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a ^ b))
        (Value::Bool(a), Value::Int(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) ^ b))
        (Value::Int(a), Value::Bool(b)) =>
          Ok(Value::Int(a ^ (if b { 1N } else { 0N })))
        (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a != b))
        _ =>
          Err(
            make_runtime_error(
              RuntimeErrorKind::Type,
              "unsupported operand type for ^".to_string(),
            ),
          )
      }
    }
    BinaryOp::BitOr => {
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, "__or__", globals, builtins, io) {
          Ok(or_method) =>
            return call_callable_with_env(
              or_method,
              [right_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      if right_val is Value::Instance(_) {
        match get_attr_from_value(right_val, "__ror__", globals, builtins, io) {
          Ok(ror_method) =>
            return call_callable_with_env(
              ror_method,
              [left_val],
              [],
              globals,
              builtins,
              io,
            )
          Err(_) => ()
        }
      }
      match (left_val, right_val) {
        (Value::Int(a), Value::Int(b)) => Ok(Value::Int(a | b))
        (Value::Bool(a), Value::Int(b)) =>
          Ok(Value::Int((if a { 1N } else { 0N }) | b))
        (Value::Int(a), Value::Bool(b)) =>
          Ok(Value::Int(a | (if b { 1N } else { 0N })))
        (Value::Bool(a), Value::Bool(b)) => Ok(Value::Bool(a || b))
        (Value::Set(left), Value::Set(right)) => {
          let values : Array[Value] = []
          for item in left {
            let _ = match set_add_unique(values, item) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
          }
          for item in right {
            let _ = match set_add_unique(values, item) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
          }
          Ok(Value::Set(values))
        }
        // frozenset unions are common in the stdlib (e.g. typing.py uses
        // frozenset(...) | frozenset(...) | {...} at import time). moonpython
        // models `frozenset()` as an instance carrying set storage, so handle
        // unions involving that representation here.
        (Value::Instance(left_inst), right) => {
          let left_storage = match
            get_named_value(left_inst.dict, set_storage_name) {
            Some(Value::Set(values)) => Some(values)
            _ => None
          }
          let right_storage = match right {
            Value::Set(values) => Some(values)
            Value::Instance(right_inst) =>
              match get_named_value(right_inst.dict, set_storage_name) {
                Some(Value::Set(values)) => Some(values)
                _ => None
              }
            _ => None
          }
          match (left_storage, right_storage) {
            (Some(left_values), Some(right_values)) => {
              let values : Array[Value] = []
              for item in left_values {
                let _ = match set_add_unique(values, item) {
                  Ok(_) => ()
                  Err(err) => return Err(err)
                }
              }
              for item in right_values {
                let _ = match set_add_unique(values, item) {
                  Ok(_) => ()
                  Err(err) => return Err(err)
                }
              }
              if left_inst.class.name == "frozenset" {
                let klass = builtin_class_from_name("frozenset", builtins)
                let dict : Array[(String, Value)] = [
                  ("hashvalue", Value::Int(fresh_object_hashvalue())),
                  (set_storage_name, Value::Set(values)),
                ]
                Ok(Value::Instance(InstanceValue::{ class: klass, dict, }))
              } else {
                Ok(Value::Set(values))
              }
            }
            _ => {
              // Not a set/frozenset union; fall back to `X | Y` union types.
              let left_args = union_args_from_value(
                Value::Instance(left_inst),
                builtins,
              )
              let right_args = union_args_from_value(right, builtins)
              match (left_args, right_args) {
                (Some(left), Some(right)) => {
                  let args : Array[Value] = []
                  for value in left {
                    args.push(value)
                  }
                  for value in right {
                    args.push(value)
                  }
                  Ok(make_union_type(args))
                }
                _ =>
                  Err(
                    make_runtime_error(
                      RuntimeErrorKind::Type,
                      "unsupported operand type for |".to_string(),
                    ),
                  )
              }
            }
          }
        }
        (Value::Set(left), Value::Instance(right_inst)) =>
          match get_named_value(right_inst.dict, set_storage_name) {
            Some(Value::Set(right)) => {
              let values : Array[Value] = []
              for item in left {
                let _ = match set_add_unique(values, item) {
                  Ok(_) => ()
                  Err(err) => return Err(err)
                }
              }
              for item in right {
                let _ = match set_add_unique(values, item) {
                  Ok(_) => ()
                  Err(err) => return Err(err)
                }
              }
              Ok(Value::Set(values))
            }
            _ =>
              Err(
                make_runtime_error(
                  RuntimeErrorKind::Type,
                  "unsupported operand type for |".to_string(),
                ),
              )
          }
        (Value::Dict(left), Value::Dict(right)) => {
          // PEP 584: dict union.
          let out : Array[(Value, Value)] = []
          for pair in left {
            out.push(pair)
          }
          for pair in right {
            let _ = match dict_set_item(out, pair.0, pair.1) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
          }
          Ok(Value::Dict(out))
        }
        _ => {
          let left_args = union_args_from_value(left_val, builtins)
          let right_args = union_args_from_value(right_val, builtins)
          match (left_args, right_args) {
            (Some(left), Some(right)) => {
              let args : Array[Value] = []
              for value in left {
                args.push(value)
              }
              for value in right {
                args.push(value)
              }
              Ok(make_union_type(args))
            }
            _ =>
              Err(
                make_runtime_error(
                  RuntimeErrorKind::Type,
                  "unsupported operand type for |".to_string(),
                ),
              )
          }
        }
      }
    }
  }
}

///|
fn inplace_dunder_name(op : BinaryOp) -> String? {
  match op {
    BinaryOp::Add => Some("__iadd__")
    BinaryOp::Sub => Some("__isub__")
    BinaryOp::Mul => Some("__imul__")
    BinaryOp::MatMul => Some("__imatmul__")
    BinaryOp::Div => Some("__itruediv__")
    BinaryOp::FloorDiv => Some("__ifloordiv__")
    BinaryOp::Mod => Some("__imod__")
    BinaryOp::Pow => Some("__ipow__")
    BinaryOp::ShiftLeft => Some("__ilshift__")
    BinaryOp::ShiftRight => Some("__irshift__")
    BinaryOp::BitAnd => Some("__iand__")
    BinaryOp::BitXor => Some("__ixor__")
    BinaryOp::BitOr => Some("__ior__")
  }
}

///|
fn eval_augassign_op_values(
  op : BinaryOp,
  left_val : Value,
  right_val : Value,
  globals : Array[(String, Value)],
  builtins : Array[(String, Value)],
  io : MockIO,
) -> Result[Value, RuntimeError] {
  // Builtin list in-place ops are observable via aliasing (`a is b`).
  match (op, left_val) {
    (BinaryOp::Add, Value::List(values)) => {
      let iterator = match
        iter_value_to_iterator(right_val, globals, builtins, io) {
        Ok(v) => v
        Err(err) => return Err(err)
      }
      while true {
        match iterator_next(iterator, None, globals, builtins, io) {
          Ok(item) => values.push(item)
          Err(err) =>
            if err.exc_type == "StopIteration" {
              break
            } else {
              return Err(err)
            }
        }
      }
      return Ok(Value::List(values))
    }
    (BinaryOp::Mul, Value::List(values)) => {
      let count = match right_val {
        Value::Int(v) => v
        Value::Bool(v) => if v { 1N } else { 0N }
        _ =>
          return eval_binary_op_values(
            op,
            Value::List(values),
            right_val,
            globals,
            builtins,
            io,
          )
      }
      if count <= 0N {
        replace_array_values(values, [])
        return Ok(Value::List(values))
      }
      let count_i = match bigint_to_int_checked(count) {
        Ok(v) => v
        Err(err) => return Err(err)
      }
      let original : Array[Value] = []
      for item in values {
        original.push(item)
      }
      let next : Array[Value] = []
      for _i = 0; _i < count_i; _i = _i + 1 {
        for item in original {
          next.push(item)
        }
      }
      replace_array_values(values, next)
      return Ok(Value::List(values))
    }
    _ => ()
  }

  // User-defined in-place dunders (`__iadd__`, `__imul__`, ...) take precedence.
  match inplace_dunder_name(op) {
    Some(name) =>
      if left_val is Value::Instance(_) {
        match get_attr_from_value(left_val, name, globals, builtins, io) {
          Ok(inplace_method) => {
            let out = match
              call_callable_with_env(
                inplace_method,
                [right_val],
                [],
                globals,
                builtins,
                io,
              ) {
              Ok(v) => v
              Err(err) => return Err(err)
            }
            if !is_not_implemented_value(out) {
              return Ok(out)
            }
          }
          Err(err) =>
            match err.kind {
              RuntimeErrorKind::Attribute => ()
              _ => return Err(err)
            }
        }
      }
    None => ()
  }
  eval_binary_op_values(op, left_val, right_val, globals, builtins, io)
}