diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md index 15330e4006..acaa115035 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/bytes.md @@ -1,6 +1,6 @@ -# Bytes subscript +# Bytes subscripts -## Simple +## Indexing ```py b = b"\x00abc\xff" @@ -21,11 +21,7 @@ reveal_type(x) # revealed: Unknown y = b[-6] # error: [index-out-of-bounds] "Index -6 is out of bounds for bytes literal `Literal[b"\x00abc\xff"]` with length 5" reveal_type(y) # revealed: Unknown -``` -## Function return - -```py def int_instance() -> int: return 42 @@ -33,3 +29,29 @@ a = b"abcde"[int_instance()] # TODO: Support overloads... Should be `bytes` reveal_type(a) # revealed: @Todo ``` + +## Slices + +```py +b = b"\x00abc\xff" + +reveal_type(b[0:2]) # revealed: Literal[b"\x00a"] +reveal_type(b[-3:]) # revealed: Literal[b"bc\xff"] + +b[0:4:0] # error: [zero-stepsize-in-slice] +b[:4:0] # error: [zero-stepsize-in-slice] +b[0::0] # error: [zero-stepsize-in-slice] +b[::0] # error: [zero-stepsize-in-slice] + +def int_instance() -> int: ... + +byte_slice1 = b[int_instance() : int_instance()] +# TODO: Support overloads... Should be `bytes` +reveal_type(byte_slice1) # revealed: @Todo + +def bytes_instance() -> bytes: ... + +byte_slice2 = bytes_instance()[0:5] +# TODO: Support overloads... Should be `bytes` +reveal_type(byte_slice2) # revealed: @Todo +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/stepsize_zero.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/stepsize_zero.md new file mode 100644 index 0000000000..06eace2d94 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/stepsize_zero.md @@ -0,0 +1,14 @@ +# Stepsize zero in slices + +We raise a `zero-stepsize-in-slice` diagnostic when trying to slice a literal +string, bytes, or tuple with a step size of zero (see tests in `string.md`, +`bytes.md` and `tuple.md`). But we don't want to raise this diagnostic when +slicing a custom type: + +```py +class MySequence: + def __getitem__(self, s: slice) -> int: + return 0 + +MySequence()[0:1:0] # No error +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md index 6987a95a70..bd47d5b358 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/string.md @@ -1,6 +1,6 @@ -# Subscript on strings +# String subscripts -## Simple +## Indexing ```py s = "abcde" @@ -18,15 +18,71 @@ reveal_type(a) # revealed: Unknown b = s[-8] # error: [index-out-of-bounds] "Index -8 is out of bounds for string `Literal["abcde"]` with length 5" reveal_type(b) # revealed: Unknown -``` -## Function return - -```py -def int_instance() -> int: - return 42 +def int_instance() -> int: ... a = "abcde"[int_instance()] # TODO: Support overloads... Should be `str` reveal_type(a) # revealed: @Todo ``` + +## Slices + +```py +s = "abcde" + +reveal_type(s[0:0]) # revealed: Literal[""] +reveal_type(s[0:1]) # revealed: Literal["a"] +reveal_type(s[0:2]) # revealed: Literal["ab"] +reveal_type(s[0:5]) # revealed: Literal["abcde"] +reveal_type(s[0:6]) # revealed: Literal["abcde"] +reveal_type(s[1:3]) # revealed: Literal["bc"] + +reveal_type(s[-3:5]) # revealed: Literal["cde"] +reveal_type(s[-4:-2]) # revealed: Literal["bc"] +reveal_type(s[-10:10]) # revealed: Literal["abcde"] + +reveal_type(s[0:]) # revealed: Literal["abcde"] +reveal_type(s[2:]) # revealed: Literal["cde"] +reveal_type(s[5:]) # revealed: Literal[""] +reveal_type(s[:2]) # revealed: Literal["ab"] +reveal_type(s[:0]) # revealed: Literal[""] +reveal_type(s[:2]) # revealed: Literal["ab"] +reveal_type(s[:10]) # revealed: Literal["abcde"] +reveal_type(s[:]) # revealed: Literal["abcde"] + +reveal_type(s[::-1]) # revealed: Literal["edcba"] +reveal_type(s[::2]) # revealed: Literal["ace"] +reveal_type(s[-2:-5:-1]) # revealed: Literal["dcb"] +reveal_type(s[::-2]) # revealed: Literal["eca"] +reveal_type(s[-1::-3]) # revealed: Literal["eb"] + +reveal_type(s[None:2:None]) # revealed: Literal["ab"] +reveal_type(s[1:None:1]) # revealed: Literal["bcde"] +reveal_type(s[None:None:None]) # revealed: Literal["abcde"] + +start = 1 +stop = None +step = 2 +reveal_type(s[start:stop:step]) # revealed: Literal["bd"] + +reveal_type(s[False:True]) # revealed: Literal["a"] +reveal_type(s[True:3]) # revealed: Literal["bc"] + +s[0:4:0] # error: [zero-stepsize-in-slice] +s[:4:0] # error: [zero-stepsize-in-slice] +s[0::0] # error: [zero-stepsize-in-slice] +s[::0] # error: [zero-stepsize-in-slice] + +def int_instance() -> int: ... + +substring1 = s[int_instance() : int_instance()] +# TODO: Support overloads... Should be `LiteralString` +reveal_type(substring1) # revealed: @Todo + +def str_instance() -> str: ... + +substring2 = str_instance()[0:5] +# TODO: Support overloads... Should be `str` +reveal_type(substring2) # revealed: @Todo +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md index 7545e8d1c5..38dea938eb 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md +++ b/crates/red_knot_python_semantic/resources/mdtest/subscript/tuple.md @@ -1,6 +1,6 @@ # Tuple subscripts -## Basic +## Indexing ```py t = (1, "a", "b") @@ -10,9 +10,66 @@ reveal_type(t[1]) # revealed: Literal["a"] reveal_type(t[-1]) # revealed: Literal["b"] reveal_type(t[-2]) # revealed: Literal["a"] +reveal_type(t[False]) # revealed: Literal[1] +reveal_type(t[True]) # revealed: Literal["a"] + a = t[4] # error: [index-out-of-bounds] reveal_type(a) # revealed: Unknown b = t[-4] # error: [index-out-of-bounds] reveal_type(b) # revealed: Unknown ``` + +## Slices + +```py +t = (1, "a", None, b"b") + +reveal_type(t[0:0]) # revealed: tuple[()] +reveal_type(t[0:1]) # revealed: tuple[Literal[1]] +reveal_type(t[0:2]) # revealed: tuple[Literal[1], Literal["a"]] +reveal_type(t[0:4]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] +reveal_type(t[0:5]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] +reveal_type(t[1:3]) # revealed: tuple[Literal["a"], None] + +reveal_type(t[-2:4]) # revealed: tuple[None, Literal[b"b"]] +reveal_type(t[-3:-1]) # revealed: tuple[Literal["a"], None] +reveal_type(t[-10:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + +reveal_type(t[0:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] +reveal_type(t[2:]) # revealed: tuple[None, Literal[b"b"]] +reveal_type(t[4:]) # revealed: tuple[()] +reveal_type(t[:0]) # revealed: tuple[()] +reveal_type(t[:2]) # revealed: tuple[Literal[1], Literal["a"]] +reveal_type(t[:10]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] +reveal_type(t[:]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + +reveal_type(t[::-1]) # revealed: tuple[Literal[b"b"], None, Literal["a"], Literal[1]] +reveal_type(t[::2]) # revealed: tuple[Literal[1], None] +reveal_type(t[-2:-5:-1]) # revealed: tuple[None, Literal["a"], Literal[1]] +reveal_type(t[::-2]) # revealed: tuple[Literal[b"b"], Literal["a"]] +reveal_type(t[-1::-3]) # revealed: tuple[Literal[b"b"], Literal[1]] + +reveal_type(t[None:2:None]) # revealed: tuple[Literal[1], Literal["a"]] +reveal_type(t[1:None:1]) # revealed: tuple[Literal["a"], None, Literal[b"b"]] +reveal_type(t[None:None:None]) # revealed: tuple[Literal[1], Literal["a"], None, Literal[b"b"]] + +start = 1 +stop = None +step = 2 +reveal_type(t[start:stop:step]) # revealed: tuple[Literal["a"], Literal[b"b"]] + +reveal_type(t[False:True]) # revealed: tuple[Literal[1]] +reveal_type(t[True:3]) # revealed: tuple[Literal["a"], None] + +t[0:4:0] # error: [zero-stepsize-in-slice] +t[:4:0] # error: [zero-stepsize-in-slice] +t[0::0] # error: [zero-stepsize-in-slice] +t[::0] # error: [zero-stepsize-in-slice] + +def int_instance() -> int: ... + +tuple_slice = t[int_instance() : int_instance()] +# TODO: Support overloads... Should be `tuple[Literal[1, 'a', b"b"] | None, ...]` +reveal_type(tuple_slice) # revealed: @Todo +``` diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 092699c9a2..4a2603e200 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -283,6 +283,8 @@ pub enum Type<'db> { LiteralString, /// A bytes literal BytesLiteral(BytesLiteralType<'db>), + /// A slice literal, e.g. `1:5`, `10:0:-1` or `:` + SliceLiteral(SliceLiteralType<'db>), /// A heterogeneous tuple type, with elements of the given types in source order. // TODO: Support variable length homogeneous tuple type like `tuple[int, ...]`. Tuple(TupleType<'db>), @@ -553,6 +555,7 @@ impl<'db> Type<'db> { | Type::IntLiteral(..) | Type::StringLiteral(..) | Type::BytesLiteral(..) + | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..)), @@ -561,6 +564,7 @@ impl<'db> Type<'db> { | Type::IntLiteral(..) | Type::StringLiteral(..) | Type::BytesLiteral(..) + | Type::SliceLiteral(..) | Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..)), @@ -612,6 +616,13 @@ impl<'db> Type<'db> { ), (Type::BytesLiteral(..), _) | (_, Type::BytesLiteral(..)) => true, + (Type::SliceLiteral(..), Type::Instance(class_type)) + | (Type::Instance(class_type), Type::SliceLiteral(..)) => !matches!( + class_type.known(db), + Some(KnownClass::Slice | KnownClass::Object) + ), + (Type::SliceLiteral(..), _) | (_, Type::SliceLiteral(..)) => true, + ( Type::FunctionLiteral(..) | Type::ModuleLiteral(..) | Type::ClassLiteral(..), Type::Instance(class_type), @@ -673,6 +684,7 @@ impl<'db> Type<'db> { | Type::IntLiteral(..) | Type::StringLiteral(..) | Type::BytesLiteral(..) + | Type::SliceLiteral(..) | Type::LiteralString => { // Note: The literal types included in this pattern are not true singletons. // There can be multiple Python objects (at different memory locations) that @@ -717,7 +729,8 @@ impl<'db> Type<'db> { | Type::IntLiteral(..) | Type::BooleanLiteral(..) | Type::StringLiteral(..) - | Type::BytesLiteral(..) => true, + | Type::BytesLiteral(..) + | Type::SliceLiteral(..) => true, Type::Tuple(tuple) => tuple .elements(db) @@ -738,6 +751,7 @@ impl<'db> Type<'db> { | KnownClass::Tuple | KnownClass::Set | KnownClass::Dict + | KnownClass::Slice | KnownClass::GenericAlias | KnownClass::ModuleType | KnownClass::FunctionType, @@ -818,6 +832,10 @@ impl<'db> Type<'db> { // TODO defer to Type::Instance().member Type::Todo } + Type::SliceLiteral(_) => { + // TODO defer to `builtins.slice` methods + Type::Todo + } Type::Tuple(_) => { // TODO: implement tuple methods Type::Todo @@ -872,6 +890,7 @@ impl<'db> Type<'db> { Type::StringLiteral(str) => Truthiness::from(!str.value(db).is_empty()), Type::LiteralString => Truthiness::Ambiguous, Type::BytesLiteral(bytes) => Truthiness::from(!bytes.value(db).is_empty()), + Type::SliceLiteral(_) => Truthiness::AlwaysTrue, Type::Tuple(items) => Truthiness::from(!items.elements(db).is_empty()), } } @@ -1028,6 +1047,7 @@ impl<'db> Type<'db> { | Type::ModuleLiteral(_) | Type::IntLiteral(_) | Type::StringLiteral(_) + | Type::SliceLiteral(_) | Type::Tuple(_) | Type::LiteralString | Type::None => Type::Unknown, @@ -1045,6 +1065,7 @@ impl<'db> Type<'db> { Type::Union(union) => union.map(db, |ty| ty.to_meta_type(db)), Type::BooleanLiteral(_) => KnownClass::Bool.to_class(db), Type::BytesLiteral(_) => KnownClass::Bytes.to_class(db), + Type::SliceLiteral(_) => KnownClass::Slice.to_class(db), Type::IntLiteral(_) => KnownClass::Int.to_class(db), Type::FunctionLiteral(_) => KnownClass::FunctionType.to_class(db), Type::ModuleLiteral(_) => KnownClass::ModuleType.to_class(db), @@ -1128,6 +1149,7 @@ pub enum KnownClass { Tuple, Set, Dict, + Slice, // Types GenericAlias, ModuleType, @@ -1150,6 +1172,7 @@ impl<'db> KnownClass { Self::Dict => "dict", Self::List => "list", Self::Type => "type", + Self::Slice => "slice", Self::GenericAlias => "GenericAlias", Self::ModuleType => "ModuleType", Self::FunctionType => "FunctionType", @@ -1173,7 +1196,8 @@ impl<'db> KnownClass { | Self::List | Self::Tuple | Self::Set - | Self::Dict => builtins_symbol_ty(db, self.as_str()), + | Self::Dict + | Self::Slice => builtins_symbol_ty(db, self.as_str()), Self::GenericAlias | Self::ModuleType | Self::FunctionType => { types_symbol_ty(db, self.as_str()) } @@ -1207,6 +1231,7 @@ impl<'db> KnownClass { "set" => Some(Self::Set), "dict" => Some(Self::Dict), "list" => Some(Self::List), + "slice" => Some(Self::Slice), "GenericAlias" => Some(Self::GenericAlias), "NoneType" => Some(Self::NoneType), "ModuleType" => Some(Self::ModuleType), @@ -1231,7 +1256,8 @@ impl<'db> KnownClass { | Self::List | Self::Tuple | Self::Set - | Self::Dict => module.name() == "builtins", + | Self::Dict + | Self::Slice => module.name() == "builtins", Self::GenericAlias | Self::ModuleType | Self::FunctionType => module.name() == "types", Self::NoneType => matches!(module.name().as_str(), "_typeshed" | "types"), } @@ -1797,6 +1823,13 @@ pub struct BytesLiteralType<'db> { value: Box<[u8]>, } +#[salsa::interned] +pub struct SliceLiteralType<'db> { + start: Option, + stop: Option, + step: Option, +} + #[salsa::interned] pub struct TupleType<'db> { #[return_ref] diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 395b290ae0..3cd3ec7c99 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -90,6 +90,28 @@ impl Display for DisplayRepresentation<'_> { escape.bytes_repr().write(f) } + Type::SliceLiteral(slice) => { + f.write_str("slice[")?; + if let Some(start) = slice.start(self.db) { + write!(f, "Literal[{start}]")?; + } else { + f.write_str("None")?; + } + + f.write_str(", ")?; + + if let Some(stop) = slice.stop(self.db) { + write!(f, "Literal[{stop}]")?; + } else { + f.write_str("None")?; + } + + if let Some(step) = slice.step(self.db) { + write!(f, ", Literal[{step}]")?; + } + + f.write_str("]") + } Type::Tuple(tuple) => { f.write_str("tuple[")?; let elements = tuple.elements(self.db); @@ -301,7 +323,9 @@ mod tests { use ruff_db::system::{DbWithTestSystem, SystemPathBuf}; use crate::db::tests::TestDb; - use crate::types::{global_symbol_ty, BytesLiteralType, StringLiteralType, Type, UnionType}; + use crate::types::{ + global_symbol_ty, BytesLiteralType, SliceLiteralType, StringLiteralType, Type, UnionType, + }; use crate::{Program, ProgramSettings, PythonVersion, SearchPathSettings}; fn setup_db() -> TestDb { @@ -376,4 +400,46 @@ mod tests { ); Ok(()) } + + #[test] + fn test_slice_literal_display() { + let db = setup_db(); + + assert_eq!( + Type::SliceLiteral(SliceLiteralType::new(&db, None, None, None)) + .display(&db) + .to_string(), + "slice[None, None]" + ); + assert_eq!( + Type::SliceLiteral(SliceLiteralType::new(&db, Some(1), None, None)) + .display(&db) + .to_string(), + "slice[Literal[1], None]" + ); + assert_eq!( + Type::SliceLiteral(SliceLiteralType::new(&db, None, Some(2), None)) + .display(&db) + .to_string(), + "slice[None, Literal[2]]" + ); + assert_eq!( + Type::SliceLiteral(SliceLiteralType::new(&db, Some(1), Some(5), None)) + .display(&db) + .to_string(), + "slice[Literal[1], Literal[5]]" + ); + assert_eq!( + Type::SliceLiteral(SliceLiteralType::new(&db, Some(1), Some(5), Some(2))) + .display(&db) + .to_string(), + "slice[Literal[1], Literal[5], Literal[2]]" + ); + assert_eq!( + Type::SliceLiteral(SliceLiteralType::new(&db, None, None, Some(2))) + .display(&db) + .to_string(), + "slice[None, None, Literal[2]]" + ); + } } diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 7720bfcbeb..a60c78f512 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -52,14 +52,13 @@ use crate::stdlib::builtins_module_scope; use crate::types::diagnostic::{TypeCheckDiagnostic, TypeCheckDiagnostics}; use crate::types::{ bindings_ty, builtins_symbol_ty, declarations_ty, global_symbol_ty, symbol_ty, - typing_extensions_symbol_ty, BytesLiteralType, ClassType, FunctionType, KnownFunction, - StringLiteralType, Truthiness, TupleType, Type, TypeArrayDisplay, UnionType, + typing_extensions_symbol_ty, BytesLiteralType, ClassType, FunctionType, IterationOutcome, + KnownClass, KnownFunction, SliceLiteralType, StringLiteralType, Truthiness, TupleType, Type, + TypeArrayDisplay, UnionBuilder, UnionType, }; -use crate::util::subscript::PythonSubscript; +use crate::util::subscript::{PyIndex, PySlice}; use crate::Db; -use super::{IterationOutcome, KnownClass, UnionBuilder}; - /// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope. /// Use when checking a scope, or needing to provide a type for an arbitrary expression in the /// scope. @@ -1483,6 +1482,14 @@ impl<'db> TypeInferenceBuilder<'db> { ); } + pub(super) fn slice_step_size_zero_diagnostic(&mut self, node: AnyNodeRef) { + self.add_diagnostic( + node, + "zero-stepsize-in-slice", + format_args!("Slice step size can not be zero"), + ); + } + /// Emit a diagnostic declaring that a type does not support subscripting. pub(super) fn non_subscriptable_diagnostic( &mut self, @@ -3199,13 +3206,13 @@ impl<'db> TypeInferenceBuilder<'db> { ) -> Type<'db> { match (value_ty, slice_ty) { // Ex) Given `("a", "b", "c", "d")[1]`, return `"b"` - (Type::Tuple(tuple_ty), Type::IntLiteral(int)) => { + (Type::Tuple(tuple_ty), Type::IntLiteral(int)) if i32::try_from(int).is_ok() => { let elements = tuple_ty.elements(self.db); elements .iter() - .python_subscript(int) + .py_index(i32::try_from(int).expect("checked in branch arm")) .copied() - .unwrap_or_else(|| { + .unwrap_or_else(|_| { self.index_out_of_bounds_diagnostic( "tuple", value_node.into(), @@ -3216,25 +3223,36 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown }) } - // Ex) Given `("a", "b", "c", "d")[True]`, return `"b"` - (Type::Tuple(_), Type::BooleanLiteral(bool)) => self.infer_subscript_expression_types( - value_node, - value_ty, - Type::IntLiteral(i64::from(bool)), - ), + // Ex) Given `("a", 1, Null)[0:2]`, return `("a", 1)` + (Type::Tuple(tuple_ty), Type::SliceLiteral(slice_ty)) => { + let elements = tuple_ty.elements(self.db); + let start = slice_ty.start(self.db); + let stop = slice_ty.stop(self.db); + let step = slice_ty.step(self.db); + + if let Ok(new_elements) = elements.as_ref().py_slice(start, stop, step) { + let new_elements: Vec<_> = new_elements.copied().collect(); + Type::Tuple(TupleType::new(self.db, new_elements.into_boxed_slice())) + } else { + self.slice_step_size_zero_diagnostic(value_node.into()); + Type::Unknown + } + } // Ex) Given `"value"[1]`, return `"a"` - (Type::StringLiteral(literal_ty), Type::IntLiteral(int)) => { + (Type::StringLiteral(literal_ty), Type::IntLiteral(int)) + if i32::try_from(int).is_ok() => + { let literal_value = literal_ty.value(self.db); literal_value .chars() - .python_subscript(int) + .py_index(i32::try_from(int).expect("checked in branch arm")) .map(|ch| { Type::StringLiteral(StringLiteralType::new( self.db, ch.to_string().into_boxed_str(), )) }) - .unwrap_or_else(|| { + .unwrap_or_else(|_| { self.index_out_of_bounds_diagnostic( "string", value_node.into(), @@ -3245,16 +3263,35 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown }) } + // Ex) Given `"value"[1:3]`, return `"al"` + (Type::StringLiteral(literal_ty), Type::SliceLiteral(slice_ty)) => { + let literal_value = literal_ty.value(self.db); + let start = slice_ty.start(self.db); + let stop = slice_ty.stop(self.db); + let step = slice_ty.step(self.db); + + let chars: Vec<_> = literal_value.chars().collect(); + let result = if let Ok(new_chars) = chars.as_slice().py_slice(start, stop, step) { + let literal: String = new_chars.collect(); + Type::StringLiteral(StringLiteralType::new(self.db, literal.into_boxed_str())) + } else { + self.slice_step_size_zero_diagnostic(value_node.into()); + Type::Unknown + }; + result + } // Ex) Given `b"value"[1]`, return `b"a"` - (Type::BytesLiteral(literal_ty), Type::IntLiteral(int)) => { + (Type::BytesLiteral(literal_ty), Type::IntLiteral(int)) + if i32::try_from(int).is_ok() => + { let literal_value = literal_ty.value(self.db); literal_value .iter() - .python_subscript(int) + .py_index(i32::try_from(int).expect("checked in branch arm")) .map(|byte| { Type::BytesLiteral(BytesLiteralType::new(self.db, [*byte].as_slice())) }) - .unwrap_or_else(|| { + .unwrap_or_else(|_| { self.index_out_of_bounds_diagnostic( "bytes literal", value_node.into(), @@ -3265,13 +3302,30 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Unknown }) } + // Ex) Given `b"value"[1:3]`, return `b"al"` + (Type::BytesLiteral(literal_ty), Type::SliceLiteral(slice_ty)) => { + let literal_value = literal_ty.value(self.db); + let start = slice_ty.start(self.db); + let stop = slice_ty.stop(self.db); + let step = slice_ty.step(self.db); + + if let Ok(new_bytes) = literal_value.as_ref().py_slice(start, stop, step) { + let new_bytes: Vec = new_bytes.copied().collect(); + Type::BytesLiteral(BytesLiteralType::new(self.db, new_bytes.into_boxed_slice())) + } else { + self.slice_step_size_zero_diagnostic(value_node.into()); + Type::Unknown + } + } // Ex) Given `"value"[True]`, return `"a"` - (Type::StringLiteral(_) | Type::BytesLiteral(_), Type::BooleanLiteral(bool)) => self - .infer_subscript_expression_types( - value_node, - value_ty, - Type::IntLiteral(i64::from(bool)), - ), + ( + Type::Tuple(_) | Type::StringLiteral(_) | Type::BytesLiteral(_), + Type::BooleanLiteral(bool), + ) => self.infer_subscript_expression_types( + value_node, + value_ty, + Type::IntLiteral(i64::from(bool)), + ), (value_ty, slice_ty) => { // Resolve the value to its class. let value_meta_ty = value_ty.to_meta_type(self.db); @@ -3347,6 +3401,11 @@ impl<'db> TypeInferenceBuilder<'db> { } fn infer_slice_expression(&mut self, slice: &ast::ExprSlice) -> Type<'db> { + enum SliceArg { + Arg(Option), + Unsupported, + } + let ast::ExprSlice { range: _, lower, @@ -3354,12 +3413,33 @@ impl<'db> TypeInferenceBuilder<'db> { step, } = slice; - self.infer_optional_expression(lower.as_deref()); - self.infer_optional_expression(upper.as_deref()); - self.infer_optional_expression(step.as_deref()); + let ty_lower = self.infer_optional_expression(lower.as_deref()); + let ty_upper = self.infer_optional_expression(upper.as_deref()); + let ty_step = self.infer_optional_expression(step.as_deref()); - // TODO slice - Type::Todo + let type_to_slice_argument = |ty: Option>| match ty { + Some(Type::IntLiteral(n)) if i32::try_from(n).is_ok() => { + SliceArg::Arg(Some(i32::try_from(n).expect("checked in branch arm"))) + } + Some(Type::BooleanLiteral(b)) => SliceArg::Arg(Some(i32::from(b))), + Some(Type::None) => SliceArg::Arg(None), + Some(Type::Instance(class)) if class.is_known(self.db, KnownClass::NoneType) => { + SliceArg::Arg(None) + } + None => SliceArg::Arg(None), + _ => SliceArg::Unsupported, + }; + + match ( + type_to_slice_argument(ty_lower), + type_to_slice_argument(ty_upper), + type_to_slice_argument(ty_step), + ) { + (SliceArg::Arg(lower), SliceArg::Arg(upper), SliceArg::Arg(step)) => { + Type::SliceLiteral(SliceLiteralType::new(self.db, lower, upper, step)) + } + _ => KnownClass::Slice.to_instance(self.db), + } } fn infer_type_parameters(&mut self, type_parameters: &ast::TypeParams) { diff --git a/crates/red_knot_python_semantic/src/util/subscript.rs b/crates/red_knot_python_semantic/src/util/subscript.rs index a88276e4b4..dfbc891940 100644 --- a/crates/red_knot_python_semantic/src/util/subscript.rs +++ b/crates/red_knot_python_semantic/src/util/subscript.rs @@ -1,18 +1,192 @@ -pub(crate) trait PythonSubscript { +//! This module provides utility functions for indexing (`PyIndex`) and slicing +//! operations (`PySlice`) on iterators, following the semantics of equivalent +//! operations in Python. + +use itertools::Either; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct OutOfBoundsError; + +pub(crate) trait PyIndex { type Item; - fn python_subscript(&mut self, index: i64) -> Option; + fn py_index(&mut self, index: i32) -> Result; } -impl> PythonSubscript for T { +fn from_nonnegative_i32(index: i32) -> usize { + static_assertions::const_assert!(usize::BITS >= 32); + debug_assert!(index >= 0); + + // SAFETY: `index` is non-negative, and `usize` is at least 32 bits. + usize::try_from(index).unwrap() +} + +fn from_negative_i32(index: i32) -> usize { + static_assertions::const_assert!(usize::BITS >= 32); + + index.checked_neg().map(from_nonnegative_i32).unwrap_or({ + // 'checked_neg' only fails for i32::MIN. We can not + // represent -i32::MIN as a i32, but we can represent + // it as a usize, since usize is at least 32 bits. + from_nonnegative_i32(i32::MAX) + 1 + }) +} + +#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)] +enum Position { + BeforeStart, + AtIndex(usize), + AfterEnd, +} + +enum Nth { + FromStart(usize), + FromEnd(usize), +} + +impl Nth { + fn from_index(index: i32) -> Self { + if index >= 0 { + Nth::FromStart(from_nonnegative_i32(index)) + } else { + Nth::FromEnd(from_negative_i32(index) - 1) + } + } + + fn to_position(&self, len: usize) -> Position { + debug_assert!(len > 0); + + match self { + Nth::FromStart(nth) => { + if *nth < len { + Position::AtIndex(*nth) + } else { + Position::AfterEnd + } + } + Nth::FromEnd(nth_rev) => { + if *nth_rev < len { + Position::AtIndex(len - 1 - *nth_rev) + } else { + Position::BeforeStart + } + } + } + } +} + +impl PyIndex for T +where + T: DoubleEndedIterator, +{ type Item = I; - fn python_subscript(&mut self, index: i64) -> Option { - if index >= 0 { - self.nth(usize::try_from(index).ok()?) + fn py_index(&mut self, index: i32) -> Result { + match Nth::from_index(index) { + Nth::FromStart(nth) => self.nth(nth).ok_or(OutOfBoundsError), + Nth::FromEnd(nth_rev) => self.nth_back(nth_rev).ok_or(OutOfBoundsError), + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) struct StepSizeZeroError; + +pub(crate) trait PySlice { + type Item; + + fn py_slice( + &self, + start: Option, + stop: Option, + step: Option, + ) -> Result< + Either, impl Iterator>, + StepSizeZeroError, + >; +} + +impl PySlice for &[T] { + type Item = T; + + fn py_slice( + &self, + start: Option, + stop: Option, + step_int: Option, + ) -> Result< + Either, impl Iterator>, + StepSizeZeroError, + > { + let step_int = step_int.unwrap_or(1); + if step_int == 0 { + return Err(StepSizeZeroError); + } + + let len = self.len(); + if len == 0 { + // The iterator needs to have the same type as the step>0 case below, + // so we need to use `.skip(0)`. + #[allow(clippy::iter_skip_zero)] + return Ok(Either::Left(self.iter().skip(0).take(0).step_by(1))); + } + + let to_position = |index| Nth::from_index(index).to_position(len); + + if step_int.is_positive() { + let step = from_nonnegative_i32(step_int); + + let start = start.map(to_position).unwrap_or(Position::BeforeStart); + let stop = stop.map(to_position).unwrap_or(Position::AfterEnd); + + let (skip, take, step) = if start < stop { + let skip = match start { + Position::BeforeStart => 0, + Position::AtIndex(start_index) => start_index, + Position::AfterEnd => len, + }; + + let take = match stop { + Position::BeforeStart => 0, + Position::AtIndex(stop_index) => stop_index - skip, + Position::AfterEnd => len - skip, + }; + + (skip, take, step) + } else { + (0, 0, step) + }; + + Ok(Either::Left( + self.iter().skip(skip).take(take).step_by(step), + )) } else { - let nth_rev = usize::try_from(index.checked_neg()?).ok()?.checked_sub(1)?; - self.rev().nth(nth_rev) + let step = from_negative_i32(step_int); + + let start = start.map(to_position).unwrap_or(Position::AfterEnd); + let stop = stop.map(to_position).unwrap_or(Position::BeforeStart); + + let (skip, take, step) = if start <= stop { + (0, 0, step) + } else { + let skip = match start { + Position::BeforeStart => len, + Position::AtIndex(start_index) => len - 1 - start_index, + Position::AfterEnd => 0, + }; + + let take = match stop { + Position::BeforeStart => len - skip, + Position::AtIndex(stop_index) => (len - 1) - skip - stop_index, + Position::AfterEnd => 0, + }; + + (skip, take, step) + }; + + Ok(Either::Right( + self.iter().rev().skip(skip).take(take).step_by(step), + )) } } } @@ -20,64 +194,312 @@ impl> PythonSubscript for T { #[cfg(test)] #[allow(clippy::redundant_clone)] mod tests { - use super::PythonSubscript; + use crate::util::subscript::{OutOfBoundsError, StepSizeZeroError}; + + use super::{PyIndex, PySlice}; + use itertools::assert_equal; #[test] - fn python_subscript_basic() { - let iter = 'a'..='e'; + fn py_index_empty() { + let iter = std::iter::empty::(); - assert_eq!(iter.clone().python_subscript(0), Some('a')); - assert_eq!(iter.clone().python_subscript(1), Some('b')); - assert_eq!(iter.clone().python_subscript(4), Some('e')); - assert_eq!(iter.clone().python_subscript(5), None); - - assert_eq!(iter.clone().python_subscript(-1), Some('e')); - assert_eq!(iter.clone().python_subscript(-2), Some('d')); - assert_eq!(iter.clone().python_subscript(-5), Some('a')); - assert_eq!(iter.clone().python_subscript(-6), None); + assert_eq!(iter.clone().py_index(0), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(-1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(i32::MIN), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(i32::MAX), Err(OutOfBoundsError)); } #[test] - fn python_subscript_empty() { - let iter = 'a'..'a'; + fn py_index_single_element() { + let iter = ['a'].into_iter(); - assert_eq!(iter.clone().python_subscript(0), None); - assert_eq!(iter.clone().python_subscript(1), None); - assert_eq!(iter.clone().python_subscript(-1), None); + assert_eq!(iter.clone().py_index(0), Ok('a')); + assert_eq!(iter.clone().py_index(1), Err(OutOfBoundsError)); + assert_eq!(iter.clone().py_index(-1), Ok('a')); + assert_eq!(iter.clone().py_index(-2), Err(OutOfBoundsError)); } #[test] - fn python_subscript_single_element() { - let iter = 'a'..='a'; + fn py_index_more_elements() { + let iter = ['a', 'b', 'c', 'd', 'e'].into_iter(); - assert_eq!(iter.clone().python_subscript(0), Some('a')); - assert_eq!(iter.clone().python_subscript(1), None); - assert_eq!(iter.clone().python_subscript(-1), Some('a')); - assert_eq!(iter.clone().python_subscript(-2), None); + assert_eq!(iter.clone().py_index(0), Ok('a')); + assert_eq!(iter.clone().py_index(1), Ok('b')); + assert_eq!(iter.clone().py_index(4), Ok('e')); + assert_eq!(iter.clone().py_index(5), Err(OutOfBoundsError)); + + assert_eq!(iter.clone().py_index(-1), Ok('e')); + assert_eq!(iter.clone().py_index(-2), Ok('d')); + assert_eq!(iter.clone().py_index(-5), Ok('a')); + assert_eq!(iter.clone().py_index(-6), Err(OutOfBoundsError)); } #[test] - fn python_subscript_uses_full_index_range() { - let iter = 0..=u64::MAX; + fn py_index_uses_full_index_range() { + let iter = 0..=u32::MAX; - assert_eq!(iter.clone().python_subscript(0), Some(0)); - assert_eq!(iter.clone().python_subscript(1), Some(1)); - assert_eq!( - iter.clone().python_subscript(i64::MAX), - Some(i64::MAX as u64) + // u32::MAX - |i32::MIN| + 1 = 2^32 - 1 - 2^31 + 1 = 2^31 + assert_eq!(iter.clone().py_index(i32::MIN), Ok(2u32.pow(31))); + assert_eq!(iter.clone().py_index(-2), Ok(u32::MAX - 2 + 1)); + assert_eq!(iter.clone().py_index(-1), Ok(u32::MAX - 1 + 1)); + + assert_eq!(iter.clone().py_index(0), Ok(0)); + assert_eq!(iter.clone().py_index(1), Ok(1)); + assert_eq!(iter.clone().py_index(i32::MAX), Ok(i32::MAX as u32)); + } + + #[track_caller] + fn assert_eq_slice( + input: &[char; N], + start: Option, + stop: Option, + step: Option, + expected: &[char; M], + ) { + assert_equal( + input.as_slice().py_slice(start, stop, step).unwrap(), + expected.iter(), ); + } - assert_eq!(iter.clone().python_subscript(-1), Some(u64::MAX)); - assert_eq!(iter.clone().python_subscript(-2), Some(u64::MAX - 1)); + #[test] + fn py_slice_empty_input() { + let input = []; - // i64::MIN is not representable as a positive number, so it is not - // a valid index: - assert_eq!(iter.clone().python_subscript(i64::MIN), None); + assert_eq_slice(&input, None, None, None, &[]); + assert_eq_slice(&input, Some(0), None, None, &[]); + assert_eq_slice(&input, None, Some(0), None, &[]); + assert_eq_slice(&input, Some(0), Some(0), None, &[]); + assert_eq_slice(&input, Some(-5), Some(-5), None, &[]); + assert_eq_slice(&input, None, None, Some(-1), &[]); + assert_eq_slice(&input, None, None, Some(2), &[]); + } - // but i64::MIN +1 is: - assert_eq!( - iter.clone().python_subscript(i64::MIN + 1), - Some(2u64.pow(63) + 1) - ); + #[test] + fn py_slice_single_element_input() { + let input = ['a']; + + assert_eq_slice(&input, None, None, None, &['a']); + + assert_eq_slice(&input, Some(0), None, None, &['a']); + assert_eq_slice(&input, None, Some(0), None, &[]); + assert_eq_slice(&input, Some(0), Some(0), None, &[]); + assert_eq_slice(&input, Some(0), Some(1), None, &['a']); + assert_eq_slice(&input, Some(0), Some(2), None, &['a']); + + assert_eq_slice(&input, Some(-1), None, None, &['a']); + assert_eq_slice(&input, Some(-1), Some(-1), None, &[]); + assert_eq_slice(&input, Some(-1), Some(0), None, &[]); + assert_eq_slice(&input, Some(-1), Some(1), None, &['a']); + assert_eq_slice(&input, Some(-1), Some(2), None, &['a']); + assert_eq_slice(&input, None, Some(-1), None, &[]); + + assert_eq_slice(&input, Some(-2), None, None, &['a']); + assert_eq_slice(&input, Some(-2), Some(-1), None, &[]); + assert_eq_slice(&input, Some(-2), Some(0), None, &[]); + assert_eq_slice(&input, Some(-2), Some(1), None, &['a']); + assert_eq_slice(&input, Some(-2), Some(2), None, &['a']); + } + + #[test] + fn py_slice_nonnegative_indices() { + let input = ['a', 'b', 'c', 'd', 'e']; + + assert_eq_slice(&input, None, Some(0), None, &[]); + assert_eq_slice(&input, None, Some(1), None, &['a']); + assert_eq_slice(&input, None, Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, None, Some(5), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, None, Some(6), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, None, None, None, &['a', 'b', 'c', 'd', 'e']); + + assert_eq_slice(&input, Some(0), Some(0), None, &[]); + assert_eq_slice(&input, Some(0), Some(1), None, &['a']); + assert_eq_slice(&input, Some(0), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, Some(0), Some(5), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(0), Some(6), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(0), None, None, &['a', 'b', 'c', 'd', 'e']); + + assert_eq_slice(&input, Some(1), Some(0), None, &[]); + assert_eq_slice(&input, Some(1), Some(1), None, &[]); + assert_eq_slice(&input, Some(1), Some(2), None, &['b']); + assert_eq_slice(&input, Some(1), Some(4), None, &['b', 'c', 'd']); + assert_eq_slice(&input, Some(1), Some(5), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(1), Some(6), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(1), None, None, &['b', 'c', 'd', 'e']); + + assert_eq_slice(&input, Some(4), Some(0), None, &[]); + assert_eq_slice(&input, Some(4), Some(4), None, &[]); + assert_eq_slice(&input, Some(4), Some(5), None, &['e']); + assert_eq_slice(&input, Some(4), Some(6), None, &['e']); + assert_eq_slice(&input, Some(4), None, None, &['e']); + + assert_eq_slice(&input, Some(5), Some(0), None, &[]); + assert_eq_slice(&input, Some(5), Some(5), None, &[]); + assert_eq_slice(&input, Some(5), Some(6), None, &[]); + assert_eq_slice(&input, Some(5), None, None, &[]); + + assert_eq_slice(&input, Some(6), Some(0), None, &[]); + assert_eq_slice(&input, Some(6), Some(6), None, &[]); + assert_eq_slice(&input, Some(6), None, None, &[]); + } + + #[test] + fn py_slice_negatice_indices() { + let input = ['a', 'b', 'c', 'd', 'e']; + + assert_eq_slice(&input, Some(-6), None, None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-6), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, Some(-6), Some(-4), None, &['a']); + assert_eq_slice(&input, Some(-6), Some(-5), None, &[]); + assert_eq_slice(&input, Some(-6), Some(-6), None, &[]); + assert_eq_slice(&input, Some(-6), Some(-10), None, &[]); + + assert_eq_slice(&input, Some(-5), None, None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-5), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, Some(-5), Some(-4), None, &['a']); + assert_eq_slice(&input, Some(-5), Some(-5), None, &[]); + assert_eq_slice(&input, Some(-5), Some(-6), None, &[]); + assert_eq_slice(&input, Some(-5), Some(-10), None, &[]); + + assert_eq_slice(&input, Some(-4), None, None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-4), Some(-1), None, &['b', 'c', 'd']); + assert_eq_slice(&input, Some(-4), Some(-3), None, &['b']); + assert_eq_slice(&input, Some(-4), Some(-4), None, &[]); + assert_eq_slice(&input, Some(-4), Some(-10), None, &[]); + + assert_eq_slice(&input, Some(-1), None, None, &['e']); + assert_eq_slice(&input, Some(-1), Some(-1), None, &[]); + assert_eq_slice(&input, Some(-1), Some(-10), None, &[]); + + assert_eq_slice(&input, None, Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, None, Some(-4), None, &['a']); + assert_eq_slice(&input, None, Some(-5), None, &[]); + assert_eq_slice(&input, None, Some(-6), None, &[]); + } + + #[test] + fn py_slice_mixed_positive_negative_indices() { + let input = ['a', 'b', 'c', 'd', 'e']; + + assert_eq_slice(&input, Some(0), Some(-1), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, Some(1), Some(-1), None, &['b', 'c', 'd']); + assert_eq_slice(&input, Some(3), Some(-1), None, &['d']); + assert_eq_slice(&input, Some(4), Some(-1), None, &[]); + assert_eq_slice(&input, Some(5), Some(-1), None, &[]); + + assert_eq_slice(&input, Some(0), Some(-4), None, &['a']); + assert_eq_slice(&input, Some(1), Some(-4), None, &[]); + assert_eq_slice(&input, Some(3), Some(-4), None, &[]); + + assert_eq_slice(&input, Some(0), Some(-5), None, &[]); + assert_eq_slice(&input, Some(1), Some(-5), None, &[]); + assert_eq_slice(&input, Some(3), Some(-5), None, &[]); + + assert_eq_slice(&input, Some(0), Some(-6), None, &[]); + assert_eq_slice(&input, Some(1), Some(-6), None, &[]); + + assert_eq_slice(&input, Some(-6), Some(6), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-6), Some(5), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-6), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, Some(-6), Some(1), None, &['a']); + assert_eq_slice(&input, Some(-6), Some(0), None, &[]); + + assert_eq_slice(&input, Some(-5), Some(6), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-5), Some(5), None, &['a', 'b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-5), Some(4), None, &['a', 'b', 'c', 'd']); + assert_eq_slice(&input, Some(-5), Some(1), None, &['a']); + assert_eq_slice(&input, Some(-5), Some(0), None, &[]); + + assert_eq_slice(&input, Some(-4), Some(6), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-4), Some(5), None, &['b', 'c', 'd', 'e']); + assert_eq_slice(&input, Some(-4), Some(4), None, &['b', 'c', 'd']); + assert_eq_slice(&input, Some(-4), Some(2), None, &['b']); + assert_eq_slice(&input, Some(-4), Some(1), None, &[]); + assert_eq_slice(&input, Some(-4), Some(0), None, &[]); + + assert_eq_slice(&input, Some(-1), Some(6), None, &['e']); + assert_eq_slice(&input, Some(-1), Some(5), None, &['e']); + assert_eq_slice(&input, Some(-1), Some(4), None, &[]); + assert_eq_slice(&input, Some(-1), Some(1), None, &[]); + } + + #[test] + fn py_slice_step_forward() { + // indices: 0 1 2 3 4 5 6 + let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + + // Step size zero is invalid: + assert!(matches!( + input.as_slice().py_slice(None, None, Some(0)), + Err(StepSizeZeroError) + )); + assert!(matches!( + input.as_slice().py_slice(Some(0), Some(5), Some(0)), + Err(StepSizeZeroError) + )); + assert!(matches!( + input.as_slice().py_slice(Some(0), Some(0), Some(0)), + Err(StepSizeZeroError) + )); + + assert_eq_slice(&input, Some(0), Some(8), Some(2), &['a', 'c', 'e', 'g']); + assert_eq_slice(&input, Some(0), Some(7), Some(2), &['a', 'c', 'e', 'g']); + assert_eq_slice(&input, Some(0), Some(6), Some(2), &['a', 'c', 'e']); + assert_eq_slice(&input, Some(0), Some(5), Some(2), &['a', 'c', 'e']); + assert_eq_slice(&input, Some(0), Some(4), Some(2), &['a', 'c']); + assert_eq_slice(&input, Some(0), Some(3), Some(2), &['a', 'c']); + assert_eq_slice(&input, Some(0), Some(2), Some(2), &['a']); + assert_eq_slice(&input, Some(0), Some(1), Some(2), &['a']); + assert_eq_slice(&input, Some(0), Some(0), Some(2), &[]); + assert_eq_slice(&input, Some(1), Some(5), Some(2), &['b', 'd']); + + assert_eq_slice(&input, Some(0), Some(7), Some(3), &['a', 'd', 'g']); + assert_eq_slice(&input, Some(0), Some(6), Some(3), &['a', 'd']); + + assert_eq_slice(&input, Some(0), None, Some(10), &['a']); + } + + #[test] + fn py_slice_step_backward() { + // indices: 0 1 2 3 4 5 6 + let input = ['a', 'b', 'c', 'd', 'e', 'f', 'g']; + + assert_eq_slice(&input, Some(7), Some(0), Some(-2), &['g', 'e', 'c']); + assert_eq_slice(&input, Some(6), Some(0), Some(-2), &['g', 'e', 'c']); + assert_eq_slice(&input, Some(5), Some(0), Some(-2), &['f', 'd', 'b']); + assert_eq_slice(&input, Some(4), Some(0), Some(-2), &['e', 'c']); + assert_eq_slice(&input, Some(3), Some(0), Some(-2), &['d', 'b']); + assert_eq_slice(&input, Some(2), Some(0), Some(-2), &['c']); + assert_eq_slice(&input, Some(1), Some(0), Some(-2), &['b']); + assert_eq_slice(&input, Some(0), Some(0), Some(-2), &[]); + + assert_eq_slice(&input, Some(7), None, Some(-2), &['g', 'e', 'c', 'a']); + assert_eq_slice(&input, None, None, Some(-2), &['g', 'e', 'c', 'a']); + assert_eq_slice(&input, None, Some(0), Some(-2), &['g', 'e', 'c']); + + assert_eq_slice(&input, Some(5), Some(1), Some(-2), &['f', 'd']); + assert_eq_slice(&input, Some(5), Some(2), Some(-2), &['f', 'd']); + assert_eq_slice(&input, Some(5), Some(3), Some(-2), &['f']); + assert_eq_slice(&input, Some(5), Some(4), Some(-2), &['f']); + assert_eq_slice(&input, Some(5), Some(5), Some(-2), &[]); + + assert_eq_slice(&input, Some(6), None, Some(-3), &['g', 'd', 'a']); + assert_eq_slice(&input, Some(6), Some(0), Some(-3), &['g', 'd']); + + assert_eq_slice(&input, Some(7), None, Some(-10), &['g']); + + assert_eq_slice(&input, Some(-6), Some(-9), Some(-1), &['b', 'a']); + assert_eq_slice(&input, Some(-6), Some(-8), Some(-1), &['b', 'a']); + assert_eq_slice(&input, Some(-6), Some(-7), Some(-1), &['b']); + assert_eq_slice(&input, Some(-6), Some(-6), Some(-1), &[]); + + assert_eq_slice(&input, Some(-7), Some(-9), Some(-1), &['a']); + + assert_eq_slice(&input, Some(-8), Some(-9), Some(-1), &[]); + assert_eq_slice(&input, Some(-9), Some(-9), Some(-1), &[]); + + assert_eq_slice(&input, Some(-6), Some(-2), Some(-1), &[]); + assert_eq_slice(&input, Some(-9), Some(-6), Some(-1), &[]); } }