Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CU-86drpncat - Wrong tuple type hint is compiled on Neo3-boa #1232

Merged
merged 2 commits into from
Apr 9, 2024
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
12 changes: 5 additions & 7 deletions boa3/internal/analyser/moduleanalyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ def __include_variable(self,
if isinstance(source_node, ast.Global):
var = outer_symbol
else:
if isinstance(var_type, SequenceType):
if isinstance(var_type, SequenceType) and not Type.tuple.is_type_of(var_type):
luc10921 marked this conversation as resolved.
Show resolved Hide resolved
var_type = var_type.build_collection(var_enumerate_type)
var = Variable(var_type, origin_node=source_node)

Expand Down Expand Up @@ -1208,8 +1208,7 @@ def visit_Subscript(self, subscript: ast.Subscript) -> str | IType:
if (isinstance(symbol, (Collection, MetaType))
and isinstance(subscript.value, (ast.Name, ast.NameConstant, ast.Attribute))):
# for evaluating names like list[str], dict[int, bool], etc
value = subscript.slice.value if isinstance(subscript.slice, ast.Index) else subscript.slice
values_type: Iterable[IType] = self.get_values_type(value)
values_type: Iterable[IType] = self.get_values_type(subscript.slice)
if isinstance(symbol, Collection):
return symbol.build_collection(*values_type)
else:
Expand All @@ -1222,11 +1221,10 @@ def visit_Subscript(self, subscript: ast.Subscript) -> str | IType:
if isinstance(symbol, UnionType) or isinstance(symbol_type, UnionType):
if not isinstance(symbol_type, UnionType):
symbol_type = symbol
index = subscript.slice.value if isinstance(subscript.slice, ast.Index) else subscript.slice
if isinstance(index, ast.Tuple):
union_types = [self.get_type(value) for value in index.elts]
if isinstance(subscript.slice, ast.Tuple):
union_types = [self.get_type(value) for value in subscript.slice.elts]
else:
union_types = self.get_type(index)
union_types = self.get_type(subscript.slice)
return symbol_type.build(union_types)

if isinstance(symbol_type, Collection):
Expand Down
4 changes: 2 additions & 2 deletions boa3/internal/analyser/typeanalyser.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,7 +583,7 @@ def validate_get_or_set(self, subscript: ast.Subscript, index_node: ast.AST) ->
type_id=symbol_type.identifier,
operation_id=Operator.Subscript)
)
return symbol_type.item_type
return symbol_type.get_item_type(index)

def validate_slice(self, subscript: ast.Subscript, slice_node: ast.Slice) -> IType:
"""
Expand Down Expand Up @@ -1932,7 +1932,7 @@ def visit_Starred(self, node: ast.Starred) -> ast.AST:
actual_type_id=value_type.identifier)
)

return Type.tuple.build_collection(value_type.value_type)
return Type.tuple.build_any_length(value_type.value_type)

def visit_Index(self, index: ast.Index) -> Any:
"""
Expand Down
4 changes: 2 additions & 2 deletions boa3/internal/compiler/codegenerator/codegeneratorvisitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ def visit_Subscript_Index(self, subscript: ast.Subscript) -> GeneratorData:
if isinstance(subscript.ctx, ast.Load):
# get item
value_data = self.visit_to_generate(subscript.value)
slice = subscript.slice.value if isinstance(subscript.slice, ast.Index) else subscript.slice
slice = subscript.slice
self.visit_to_generate(slice)

index_is_constant_number = isinstance(slice, ast.Num) and isinstance(slice.n, int)
Expand All @@ -530,7 +530,7 @@ def visit_Subscript_Index(self, subscript: ast.Subscript) -> GeneratorData:
# set item
var_data = self.visit(subscript.value)

index = subscript.slice.value if isinstance(subscript.slice, ast.Index) else subscript.slice
index = subscript.slice
symbol_id = var_data.symbol_id
value_type = var_data.type

Expand Down
2 changes: 1 addition & 1 deletion boa3/internal/model/callable.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def __init__(self, args: dict[str, Variable] = None,

default_value = set_internal_call(ast.parse(default_code).body[0].value)

self.args[vararg_id] = Variable(Type.tuple.build_collection([vararg_var.type]))
self.args[vararg_id] = Variable(Type.tuple.build_any_length(vararg_var.type))
self.defaults.append(default_value)
self._vararg = vararg

Expand Down
31 changes: 31 additions & 0 deletions boa3/internal/model/type/annotation/ellipsistype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
from typing import Any

from boa3.internal.model.type.itype import IType


class EllipsisType(IType):
"""
A class used to represent Python Ellipsis (...) annotation
"""

def __init__(self):
identifier = 'Ellipsis'
super().__init__(identifier)

@classmethod
def build(cls, value: Any) -> IType:
return ellipsisType

@classmethod
def _is_type_of(cls, value: Any):
return value is Ellipsis or value is ellipsisType

def union_type(self, other_type: IType) -> IType:
return other_type

def intersect_type(self, other_type: IType) -> IType:
from boa3.internal.model.type.type import Type
return Type.none


ellipsisType: IType = EllipsisType()
8 changes: 8 additions & 0 deletions boa3/internal/model/type/collection/icollection.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,9 @@ def get_types(cls, value: Any) -> set[IType]:
types: set[IType] = {val if isinstance(val, IType) else Type.get_type(val) for val in value}
return cls.filter_types(types)

def get_item_type(self, index: tuple):
return self.item_type

@classmethod
def filter_types(cls, values_type) -> set[IType]:
if values_type is None:
Expand All @@ -93,6 +96,11 @@ def filter_types(cls, values_type) -> set[IType]:
if any(t is Type.any or t is Type.none for t in values_type):
return {Type.any}

if Type.ellipsis in values_type:
values_type.remove(Type.ellipsis)
if len(values_type) == 1:
return values_type

actual_types = list(values_type)[:1]
for value in list(values_type)[1:]:
other = next((x for x in actual_types
Expand Down
109 changes: 105 additions & 4 deletions boa3/internal/model/type/collection/sequence/tupletype.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
from typing import Any
from __future__ import annotations

from typing import Any, Iterable

from boa3.internal.model.type.collection.sequence.sequencetype import SequenceType
from boa3.internal.model.type.itype import IType
Expand All @@ -9,11 +11,33 @@ class TupleType(SequenceType):
A class used to represent Python tuple type
"""

def __init__(self, values_type: set[IType] = None):
def __init__(self, values_type: list[IType] = None, any_length: bool = False):
identifier = 'tuple'
if values_type is None:
values_type = []
any_length = True

self._tuple_types = values_type
self._is_any_length = any_length

values_type = self.filter_types(values_type)
super().__init__(identifier, values_type)

@property
def identifier(self) -> str:
from boa3.internal.model.type.type import Type
if self.item_type == Type.any and self._is_any_length:
return self._identifier

if len(self._tuple_types) == 0:
tuple_types = [self.item_type.identifier]
else:
tuple_types = [type_.identifier for type_ in self._tuple_types]

if self._is_any_length:
tuple_types.append('...')
return f'{self._identifier}[{", ".join(tuple_types)}]'

@property
def default_value(self) -> Any:
return tuple()
Expand All @@ -29,8 +53,85 @@ def valid_key(self) -> IType:
@classmethod
def build(cls, value: Any) -> IType:
if cls._is_type_of(value):
values_types: set[IType] = cls.get_types(value)
return cls(values_types)
values_types: list[IType] = cls.get_types(value)
from boa3.internal.model.type.type import Type
if len(values_types) == 2 and values_types[-1] is Type.ellipsis:
has_ellipsis = True
values_types.pop()
else:
has_ellipsis = False
if Type.ellipsis in values_types:
# only tuple[<type>, ...] is accepted as tuple of any size typed as <type>
# all other cases where ... is used it has the same behavior as any
for index, value in enumerate(values_types):
if value is Type.ellipsis:
values_types[index] = Type.any

return cls(values_types, any_length=has_ellipsis)

def build_any_length(self, value: Any) -> IType:
result: TupleType = self.build((value,))
if len(result._tuple_types) == 1:
result._is_any_length = True
return result

@classmethod
def build_collection(cls, *value_type: IType | Iterable) -> IType:
params = []
for arg in value_type:
if isinstance(arg, Iterable):
argument = list(arg)
else:
argument = [arg]
params.extend(argument)
return cls.build(tuple(params))

@classmethod
def get_types(cls, value: Any) -> list[IType]:
from boa3.internal.model.type.type import Type
return [val if isinstance(val, IType) else Type.get_type(val) for val in value]

def get_item_type(self, index: tuple):
if len(index) > 0 and isinstance(index[0], int):
target_index = index[0]
if len(self._tuple_types) > target_index:
return self._tuple_types[target_index]

return super().get_item_type(index)

def is_type_of(self, value: Any) -> bool:
if self._is_type_of(value):
min_size = len(self._tuple_types)
if isinstance(value, TupleType):
types_to_check = value._tuple_types
any_length = value._is_any_length
else:
types_to_check = value
any_length = False

len_types_to_check = len(types_to_check)
if self._is_any_length and len_types_to_check == 0 and not any_length:
# tuples of any length are always type of empty tuple
luc10921 marked this conversation as resolved.
Show resolved Hide resolved
return True
if len_types_to_check < min_size:
return False
if not self._is_any_length:
if len_types_to_check > min_size:
return False
elif len_types_to_check == min_size and any_length:
return False

for index in range(min_size):
if not self._tuple_types[index].is_type_of(types_to_check[index]):
return False
if len_types_to_check > min_size:
last_tuple_type = self._tuple_types[-1] if len(self._tuple_types) else self.value_type
for index in range(min_size, len_types_to_check):
if not last_tuple_type.is_type_of(types_to_check[index]):
return False

return True
return False

@classmethod
def _is_type_of(cls, value: Any):
Expand Down
2 changes: 2 additions & 0 deletions boa3/internal/model/type/type.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import Any

from boa3.internal.model.type.annotation.ellipsistype import ellipsisType
from boa3.internal.model.type.annotation.optionaltype import OptionalType
from boa3.internal.model.type.annotation.uniontype import UnionType
from boa3.internal.model.type.anytype import anyType
Expand Down Expand Up @@ -122,4 +123,5 @@ def get_generic_type(cls, *types: IType) -> IType:
# Annotation types
union = UnionType()
optional = OptionalType()
ellipsis = ellipsisType
any = anyType
2 changes: 1 addition & 1 deletion boa3_test/test_sc/any_test/AnyTuple.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@

@public
def Main():
a: Tuple[Any] = (True, 1, 'ok')
a: Tuple[Any, Any, Any] = (True, 1, 'ok')
2 changes: 1 addition & 1 deletion boa3_test/test_sc/class_test/NotificationSetVariables.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def event_name(event: str) -> str:


@public
def state(obj: Tuple[Any]) -> Any:
def state(obj: Tuple[Any, ...]) -> Any:
x = Notification()
x.state = obj
return x.state
2 changes: 1 addition & 1 deletion boa3_test/test_sc/dict_test/MismatchedTypeKeysDict.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
@public
def Main() -> Sequence[str]:
a: Dict[str, int] = {'one': 1, 'two': 2, 'three': 3}
b: Tuple[str] = a.keys()
b: Tuple[str, ...] = a.keys()
return b
2 changes: 1 addition & 1 deletion boa3_test/test_sc/dict_test/MismatchedTypeValuesDict.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@
@public
def Main() -> Sequence[int]:
a: Dict[str, int] = {'one': 1, 'two': 2, 'three': 3}
b: Tuple[int] = a.values()
b: Tuple[int, ...] = a.values()
return b
2 changes: 1 addition & 1 deletion boa3_test/test_sc/for_test/ForElse.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@public
def Main() -> int:
a: int = 0
sequence: Tuple[int] = (3, 5, 15)
sequence: Tuple[int, int, int] = (3, 5, 15)

for x in sequence:
a = a + x
Expand Down
2 changes: 1 addition & 1 deletion boa3_test/test_sc/for_test/NestedFor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@public
def Main() -> int:
a: int = 0
sequence: Tuple[int] = (3, 5, 15)
sequence: Tuple[int, int, int] = (3, 5, 15)

for x in sequence:
for y in sequence:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from typing import Tuple


def Main(a: bool, b: Tuple[str]) -> bool:
def Main(a: bool, b: Tuple[str, ...]) -> bool:
return a | b
5 changes: 3 additions & 2 deletions boa3_test/test_sc/native_test/neo/GetCandidates.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from typing import Any, List, Tuple
from typing import List, Tuple

from boa3.builtin.compile_time import public
from boa3.builtin.nativecontract.neo import NEO
from boa3.builtin.type import ECPoint


@public
def main() -> List[Tuple[Any, Any]]:
def main() -> List[Tuple[ECPoint, int]]:
return NEO.get_candidates()
4 changes: 1 addition & 3 deletions boa3_test/test_sc/tuple_test/EmptyTupleAssignment.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from typing import Tuple

from boa3.builtin.compile_time import public


@public
def Main():
a: Tuple[int] = ()
a: tuple = ()
4 changes: 2 additions & 2 deletions boa3_test/test_sc/tuple_test/IndexTuple.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Any, Tuple
from typing import Any

from boa3.builtin.compile_time import public


@public
def main(a: Tuple[Any], value: Any, start: int, end: int) -> int:
def main(a: tuple, value: Any, start: int, end: int) -> int:
return a.index(value, start, end)
4 changes: 2 additions & 2 deletions boa3_test/test_sc/tuple_test/IndexTupleDefaults.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Any, Tuple
from typing import Any

from boa3.builtin.compile_time import public


@public
def main(a: Tuple[Any], value: Any) -> int:
def main(a: tuple, value: Any) -> int:
return a.index(value)
4 changes: 2 additions & 2 deletions boa3_test/test_sc/tuple_test/IndexTupleEndDefault.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
from typing import Any, Tuple
from typing import Any

from boa3.builtin.compile_time import public


@public
def main(a: Tuple[Any], value: Any, start: int) -> int:
def main(a: tuple, value: Any, start: int) -> int:
return a.index(value, start)
2 changes: 1 addition & 1 deletion boa3_test/test_sc/tuple_test/MultipleExpressionsInLine.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@


@public
def Main(items1: Tuple[int]) -> int:
def Main(items1: Tuple[int, ...]) -> int:
items2 = ('a', 'b', 'c', 'd'); value = items1[0]; count = value + len(items2)
return count
Loading
Loading