Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions crates/vm/src/protocol/sequence.rs
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,12 @@ impl PySequence<'_> {

// if both arguments appear to be sequences, try fallback to __iadd__
if self.check() && other.sequence_unchecked().check() {
let ret = vm._iadd(self.obj, other)?;
let ret = vm.binary_iop1(
self.obj,
other,
PyNumberBinaryOp::InplaceAdd,
PyNumberBinaryOp::Add,
)?;
if let PyArithmeticValue::Implemented(ret) = PyArithmeticValue::from_object(vm, ret) {
return Ok(ret);
}
Expand All @@ -249,7 +254,12 @@ impl PySequence<'_> {
}

if self.check() {
let ret = vm._imul(self.obj, &n.to_pyobject(vm))?;
let ret = vm.binary_iop1(
self.obj,
&n.to_pyobject(vm),
PyNumberBinaryOp::InplaceMultiply,
PyNumberBinaryOp::Multiply,
)?;
if let PyArithmeticValue::Implemented(ret) = PyArithmeticValue::from_object(vm, ret) {
return Ok(ret);
}
Expand Down Expand Up @@ -504,6 +514,34 @@ mod tests {
}
}

#[test]
fn unsupported_inplace_operations_keep_sequence_errors() {
Interpreter::without_stdlib(Default::default()).enter(|vm| {
let range = PyRange {
start: vm.ctx.new_int(0),
stop: vm.ctx.new_int(3),
step: vm.ctx.new_int(1),
}
.into_ref(&vm.ctx);
let sequence = range.as_object().sequence_unchecked();
for (result, message) in [
(
sequence.inplace_repeat(2, vm),
"'range' object can't be repeated",
),
(
sequence.inplace_concat(range.as_object(), vm),
"'range' object can't be concatenated",
),
] {
let error = result.unwrap_err();
assert!(error.fast_isinstance(vm.ctx.exceptions.type_error));
let actual: String = error.args().as_slice()[0].try_to_value(vm).unwrap();
assert_eq!(actual, message);
}
});
}

#[test]
fn conversion_and_partial_result_cleanup() {
Interpreter::without_stdlib(Default::default()).enter(|vm| {
Expand Down
52 changes: 32 additions & 20 deletions crates/vm/src/types/slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,16 +492,6 @@ fn sequence_contains_wrapper(
contains_wrapper(seq.obj, needle, vm)
}

#[inline(never)]
fn sequence_repeat_wrapper(seq: PySequence<'_>, n: isize, vm: &VirtualMachine) -> PyResult {
vm.call_special_method(seq.obj, identifier!(vm, __mul__), (n,))
}

#[inline(never)]
fn sequence_inplace_repeat_wrapper(seq: PySequence<'_>, n: isize, vm: &VirtualMachine) -> PyResult {
vm.call_special_method(seq.obj, identifier!(vm, __imul__), (n,))
}

fn repr_wrapper(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<PyRef<PyStr>> {
let ret = vm.call_special_method(zelf, identifier!(vm, __repr__), ())?;
ret.downcast::<PyStr>().map_err(|obj| {
Expand Down Expand Up @@ -1581,16 +1571,38 @@ impl PyType {
// (handled by number protocol fallback)
accessor.inherit_from_mro(self);
}
SlotAccessor::SqRepeat => {
update_sub_slot!(as_sequence, repeat, sequence_repeat_wrapper, SeqRepeat)
}
SlotAccessor::SqInplaceRepeat => {
update_sub_slot!(
as_sequence,
inplace_repeat,
sequence_inplace_repeat_wrapper,
SeqRepeat
)
SlotAccessor::SqRepeat | SlotAccessor::SqInplaceRepeat => {
// Python `__mul__`/`__rmul__`/`__imul__` overrides use the number slots and
// leave no sequence repeat, as with `sq_concat`.
let (names, field) = if matches!(accessor, SlotAccessor::SqRepeat) {
(
&[identifier!(ctx, __mul__), identifier!(ctx, __rmul__)][..],
&self.slots.as_sequence.repeat,
)
} else {
(
&[identifier!(ctx, __imul__)][..],
&self.slots.as_sequence.inplace_repeat,
)
};
let mut repeat = None;
for &name in names {
match self.lookup_slot_in_mro(name, ctx, |sf| {
if let SlotFunc::SeqRepeat(f) = sf {
Some(*f)
} else {
None
}
}) {
SlotLookupResult::NativeSlot(func) => repeat = repeat.or(Some(func)),
SlotLookupResult::PythonMethod => {
repeat = None;
break;
}
SlotLookupResult::NotFound => {}
}
}
field.store(repeat);
}
SlotAccessor::SqItem => {
update_sub_slot!(as_sequence, item, sequence_getitem_wrapper, SeqItem)
Expand Down
78 changes: 45 additions & 33 deletions crates/vm/src/vm/vm_ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use crate::{
Py, PyRef,
builtins::{PyInt, PyStr, PyStrInterned, PyStrRef, PyType, PyUtf8Str},
object::{AsObject, PyObject, PyObjectRef, PyResult},
protocol::{PyNumberBinaryOp, PyNumberSlots, PyNumberTernaryOp, PyNumberUnaryFunc},
protocol::{PyNumberBinaryOp, PyNumberSlots, PyNumberTernaryOp, PyNumberUnaryFunc, PySequence},
types::PyComparisonOp,
};
use num_traits::ToPrimitive;
Expand Down Expand Up @@ -285,7 +285,7 @@ impl VirtualMachine {
///
/// - Otherwise, in-place modification is not supported. Handle it exactly as
/// a non in-place operation of the same kind.
fn binary_iop1(
pub(crate) fn binary_iop1(
&self,
a: &PyObject,
b: &PyObject,
Expand Down Expand Up @@ -492,20 +492,11 @@ impl VirtualMachine {
if !result.is(&self.ctx.not_implemented) {
return Ok(result);
}
if let Ok(seq_a) = a.try_sequence(self) {
let n = b
.try_index(self)?
.as_bigint()
.to_isize()
.ok_or_else(|| self.new_overflow_error("repeated bytes are too long"))?;
return seq_a.repeat(n, self);
} else if let Ok(seq_b) = b.try_sequence(self) {
let n = a
.try_index(self)?
.as_bigint()
.to_isize()
.ok_or_else(|| self.new_overflow_error("repeated bytes are too long"))?;
return seq_b.repeat(n, self);
if let Some(f) = a.sequence_unchecked().slots().repeat.load() {
return self.sequence_repeat(f, a, b);
}
if let Some(f) = b.sequence_unchecked().slots().repeat.load() {
return self.sequence_repeat(f, b, a);
}
Err(self.new_unsupported_bin_op_error(a, b, "*"))
}
Expand All @@ -520,27 +511,48 @@ impl VirtualMachine {
if !result.is(&self.ctx.not_implemented) {
return Ok(result);
}
if let Ok(seq_a) = a.try_sequence(self) {
let n = b
.try_index(self)?
.as_bigint()
.to_isize()
.ok_or_else(|| self.new_overflow_error("repeated bytes are too long"))?;
return seq_a.inplace_repeat(n, self);
} else if let Ok(seq_b) = b.try_sequence(self) {
let n = a
.try_index(self)?
.as_bigint()
.to_isize()
.ok_or_else(|| self.new_overflow_error("repeated bytes are too long"))?;
/* Note that the right hand operand should not be
* mutated in this case so inplace_repeat is not
* used. */
return seq_b.repeat(n, self);
let a_seq = a.sequence_unchecked();
let a_slots = a_seq.slots();
if let Some(f) = a_slots
.inplace_repeat
.load()
.or_else(|| a_slots.repeat.load())
{
return self.sequence_repeat(f, a, b);
}
// The right operand is only tried when the left type has no sequence table at all,
// and every heap type has one. It is repeated, never mutated in place.
if !a_slots.has_any()
&& a.class().heaptype_ext().is_none()
&& let Some(f) = b.sequence_unchecked().slots().repeat.load()
{
return self.sequence_repeat(f, b, a);
}
Err(self.new_unsupported_bin_op_error(a, b, "*="))
}

// sequence_repeat in CPython
fn sequence_repeat(
&self,
repeat: fn(PySequence<'_>, isize, &Self) -> PyResult,
seq: &PyObject,
n: &PyObject,
) -> PyResult {
let index = n.try_index_opt(self).ok_or_else(|| {
self.new_type_error(format!(
"can't multiply sequence by non-int of type '{}'",
n.class().slot_name()
))
})??;
let count = index.as_bigint().to_isize().ok_or_else(|| {
self.new_overflow_error(format!(
"cannot fit '{}' into an index-sized integer",
n.class().slot_name()
))
})?;
repeat(seq.sequence_unchecked(), count, self)
}

fn unary_op(
&self,
a: &PyObject,
Expand Down
118 changes: 118 additions & 0 deletions extra_tests/snippets/operator_arithmetic.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from operator import imul, mul

from testutils import assert_raises

assert -3 // 2 == -2
Expand Down Expand Up @@ -52,3 +54,119 @@
assert_raises(TypeError, lambda: 1.5 | True)
assert_raises(TypeError, lambda: 1.5 & True)
assert_raises(TypeError, lambda: 1.5 ^ True)


def check_repeat_error(error, message, operation, *args):
with assert_raises(error) as caught:
operation(*args)
assert str(caught.exception) == message, str(caught.exception)


class GetItemOnly:
def __getitem__(self, index):
raise IndexError


# An item slot does not imply support for repetition.
for value in (range(3), GetItemOnly()):
name = type(value).__name__
for operation, symbol in ((mul, "*"), (imul, "*=")):
check_repeat_error(
TypeError,
f"unsupported operand type(s) for {symbol}: '{name}' and 'int'",
operation,
value,
2,
)
check_repeat_error(
TypeError,
f"unsupported operand type(s) for *: 'int' and '{name}'",
mul,
2,
value,
)


# Native sequence slots use the same count conversion in both directions.
for value in ([1], (1,), "a", b"a", bytearray(b"a")):
for operation in (mul, lambda seq, count: count * seq, imul):
for count, error, message in (
(1.5, TypeError, "can't multiply sequence by non-int of type 'float'"),
(2**100, OverflowError, "cannot fit 'int' into an index-sized integer"),
):
check_repeat_error(error, message, operation, value, count)


class RepeatIndex:
def __init__(self, value):
self.value = value

def __index__(self):
return self.value


assert RepeatIndex(2) * [1] == [1, 1]
assert imul(2, [1]) == [1, 1]
check_repeat_error(
TypeError,
"unsupported operand type(s) for *=: 'RepeatIndex' and 'list'",
imul,
RepeatIndex(2),
[1],
)
check_repeat_error(
OverflowError,
"cannot fit 'RepeatIndex' into an index-sized integer",
mul,
"a",
RepeatIndex(-(2**100)),
)


class DeclineRepeat(GetItemOnly):
calls = []

def __mul__(self, other):
self.calls.append("mul")
return NotImplemented

def __rmul__(self, other):
self.calls.append("rmul")
return NotImplemented

def __imul__(self, other):
self.calls.append("imul")
return NotImplemented


# Numeric fallback must not invoke the same Python method through a sequence slot.
declining = DeclineRepeat()
for operation, args, expected in (
(mul, (declining, 2), ["mul"]),
(mul, (2, declining), ["rmul"]),
(imul, (declining, 2), ["imul", "mul"]),
):
DeclineRepeat.calls.clear()
assert_raises(TypeError, operation, *args)
assert DeclineRepeat.calls == expected


class RepeatList(list):
pass


class RepeatInt(int):
pass


assert "a" * RepeatInt(2) == RepeatInt(2) * "a" == "aa"
for method, operation in (
("__mul__", lambda value: value * 2),
("__rmul__", lambda value: 2 * value),
):
value = RepeatList([1])
assert operation(value) == [1, 1]
setattr(RepeatList, method, lambda self, other: NotImplemented)
assert_raises(TypeError, operation, value)
delattr(RepeatList, method)
assert operation(value) == [1, 1]
Loading