diff --git a/boa3/analyser/typeanalyser.py b/boa3/analyser/typeanalyser.py index edd353f52..e17d239df 100644 --- a/boa3/analyser/typeanalyser.py +++ b/boa3/analyser/typeanalyser.py @@ -335,7 +335,7 @@ def validate_get_or_set(self, subscript: ast.Subscript, index_node: ast.Index) - ) return symbol_type # the sequence can't use the given type as index - elif not symbol_type.is_valid_key(index_type): + if not symbol_type.is_valid_key(index_type): self._log_error( CompilerError.MismatchedTypes( subscript.lineno, subscript.col_offset, @@ -350,9 +350,7 @@ def validate_get_or_set(self, subscript: ast.Subscript, index_node: ast.Index) - type_id=symbol_type.identifier, operation_id=Operator.Subscript) ) - else: - return symbol_type.value_type - return Type.none + return symbol_type.value_type def validate_slice(self, subscript: ast.Subscript, slice_node: ast.Slice) -> IType: """ @@ -866,14 +864,23 @@ def visit_Num(self, num: ast.Num) -> int: ) return num.n - def visit_Str(self, str: ast.Str) -> str: + def visit_Str(self, string: ast.Str) -> str: """ Visitor of literal string node - :param str: the python ast string node + :param string: the python ast string node :return: the value of the string """ - return str.s + return string.s + + def visit_Bytes(self, bts: ast.Bytes) -> bytes: + """ + Visitor of literal bytes node + + :param bts: the python ast bytes node + :return: the value of the bytes + """ + return bts.s def visit_Tuple(self, tup_node: ast.Tuple) -> Tuple[Any, ...]: """ diff --git a/boa3/compiler/codegenerator.py b/boa3/compiler/codegenerator.py index 5d07ddb3b..771c3309a 100644 --- a/boa3/compiler/codegenerator.py +++ b/boa3/compiler/codegenerator.py @@ -16,7 +16,6 @@ from boa3.neo.vm.opcode.OpcodeInfo import OpcodeInfo from boa3.neo.vm.opcode.OpcodeInformation import OpcodeInformation from boa3.neo.vm.type.Integer import Integer -from boa3.neo.vm.type.StackItemType import StackItemType class CodeGenerator: @@ -301,6 +300,8 @@ def convert_literal(self, value: Any): self.convert_string_literal(value) elif value is None: self.insert_none() + elif isinstance(value, (bytes, bytearray)): + self.convert_byte_array(value) else: # TODO: convert other python literals as they are implemented raise NotImplementedError @@ -318,9 +319,9 @@ def convert_integer_literal(self, value: int): self.__insert1(op_info) else: array = Integer(value).to_byte_array() - self.convert_byte_array(array) + self.insert_push_data(array) # cast the value to integer - self.__insert1(OpcodeInfo.CONVERT, StackItemType.Integer) + self.convert_cast(Type.int) self._stack.append(Type.int) def convert_string_literal(self, value: str): @@ -330,9 +331,8 @@ def convert_string_literal(self, value: str): :param value: the value to be converted """ array = bytes(value, sys.getdefaultencoding()) - self.convert_byte_array(array) - self._stack.pop() - self._stack.append(Type.str) + self.insert_push_data(array) + self.convert_cast(Type.str) def convert_bool_literal(self, value: bool): """ @@ -352,7 +352,16 @@ def convert_byte_array(self, array: bytes): :param array: the value to be converted """ - data_len: int = len(array) + self.insert_push_data(array) + self.convert_cast(Type.bytes) + + def insert_push_data(self, data: bytes): + """ + Inserts a push data value + + :param data: the value to be converted + """ + data_len: int = len(data) if data_len <= ONE_BYTE_MAX_VALUE: op_info = OpcodeInfo.PUSHDATA1 elif data_len <= TWO_BYTES_MAX_VALUE: @@ -360,9 +369,9 @@ def convert_byte_array(self, array: bytes): else: op_info = OpcodeInfo.PUSHDATA4 - data = Integer(data_len).to_byte_array(min_length=op_info.data_len) + array + data = Integer(data_len).to_byte_array(min_length=op_info.data_len) + data self.__insert1(op_info, data) - self._stack.append(Type.none) # TODO: change to bytearray when implemented + self._stack.append(Type.str) # push data pushes a ByteString value in the stack def insert_none(self): """ @@ -371,6 +380,19 @@ def insert_none(self): self.__insert1(OpcodeInfo.PUSHNULL) self._stack.append(Type.none) + def convert_cast(self, value_type: IType): + """ + Converts casting types in Neo VM + """ + stack_top_type: IType = self._stack[-1] + if (not value_type.is_generic + and not stack_top_type.is_generic + and value_type.stack_item != stack_top_type.stack_item + and value_type.stack_item is not Type.any.stack_item): + self.__insert1(OpcodeInfo.CONVERT, value_type.stack_item) + self._stack.pop() + self._stack.append(value_type) + def convert_new_empty_array(self, length: int, array_type: IType): """ Converts the creation of a new empty array @@ -396,18 +418,11 @@ def convert_new_array(self, length: int, array_type: IType): self.convert_new_empty_array(length, array_type) else: self.__insert1(OpcodeInfo.PACK) + self._stack.pop() # array size + for x in range(length): + self._stack.pop() self._stack.append(array_type) - def convert_set_new_array_item_at(self, index: int): - """ - Converts the beginning of setting af a value in an array - - :param index: the index of the array that will be set - """ - self.duplicate_stack_top_item() - self.convert_literal_index(index) - self.convert_literal(index) - def convert_set_array_item(self): """ Converts the end of setting af a value in an array @@ -537,13 +552,14 @@ def convert_store_variable(self, var_id: str): index: int = scope.index(var_id) opcode = Opcode.get_store(index, local, is_arg) - op_info = OpcodeInfo.get_info(opcode) + if opcode is not None: + op_info = OpcodeInfo.get_info(opcode) - if op_info.data_len > 0: - self.__insert1(op_info, Integer(index).to_byte_array()) - else: - self.__insert1(op_info) - self._stack.pop() + if op_info.data_len > 0: + self.__insert1(op_info, Integer(index).to_byte_array()) + else: + self.__insert1(op_info) + self._stack.pop() def convert_builtin_method_call(self, function: IBuiltinMethod): """ diff --git a/boa3/compiler/codegeneratorvisitor.py b/boa3/compiler/codegeneratorvisitor.py index 791a43b85..899a0b648 100644 --- a/boa3/compiler/codegeneratorvisitor.py +++ b/boa3/compiler/codegeneratorvisitor.py @@ -400,13 +400,22 @@ def visit_Num(self, num: ast.Num): """ self.generator.convert_literal(num.n) - def visit_Str(self, str: ast.Str): + def visit_Str(self, string: ast.Str): """ Visitor of literal string node - :param str: the python ast string node + :param string: the python ast string node """ - self.generator.convert_literal(str.s) + self.generator.convert_literal(string.s) + + def visit_Bytes(self, bts: ast.Bytes): + """ + Visitor of literal bytes node + + :param bts: the python ast bytes node + :return: the value of the bytes + """ + self.generator.convert_literal(bts.s) def visit_Tuple(self, tup_node: ast.Tuple): """ diff --git a/boa3/model/type/itype.py b/boa3/model/type/itype.py index ade108205..52f45771b 100644 --- a/boa3/model/type/itype.py +++ b/boa3/model/type/itype.py @@ -3,6 +3,7 @@ from boa3.model.symbol import ISymbol from boa3.neo.vm.type.AbiType import AbiType +from boa3.neo.vm.type.StackItemType import StackItemType class IType(ISymbol): @@ -36,6 +37,24 @@ def abi_type(self) -> AbiType: """ return AbiType.Any + @property + def stack_item(self) -> StackItemType: + """ + Get the Neo VM stack item type representation for this type + + :return: the stack item type of this type. Any by default. + """ + return StackItemType.Any + + @property + def is_generic(self) -> bool: + """ + Verifies if this is a generic type + + :return: True if this is a generic type. False otherwise. + """ + return False + @classmethod @abstractmethod def _is_type_of(cls, value: Any) -> bool: diff --git a/boa3/model/type/primitive/booltype.py b/boa3/model/type/primitive/booltype.py index 8c9300a76..cf14d1ade 100644 --- a/boa3/model/type/primitive/booltype.py +++ b/boa3/model/type/primitive/booltype.py @@ -2,6 +2,7 @@ from boa3.model.type.itype import IType from boa3.neo.vm.type.AbiType import AbiType +from boa3.neo.vm.type.StackItemType import StackItemType class BoolType(IType): @@ -21,6 +22,10 @@ def default_value(self) -> Any: def abi_type(self) -> AbiType: return AbiType.Boolean + @property + def stack_item(self) -> StackItemType: + return StackItemType.Boolean + @classmethod def build(cls, value: Any): if cls._is_type_of(value): diff --git a/boa3/model/type/primitive/bytestype.py b/boa3/model/type/primitive/bytestype.py new file mode 100644 index 000000000..f1731b904 --- /dev/null +++ b/boa3/model/type/primitive/bytestype.py @@ -0,0 +1,60 @@ +from typing import Any + +from boa3.model.type.itype import IType +from boa3.model.type.sequence.sequencetype import SequenceType +from boa3.neo.vm.type.AbiType import AbiType +from boa3.neo.vm.type.StackItemType import StackItemType + + +class BytesType(SequenceType): + """ + A class used to represent Python bytes type + """ + + def __init__(self): + identifier = 'bytes' + from boa3.model.type.primitive.inttype import IntType + values_type = [IntType()] + super().__init__(identifier, values_type) + + @property + def identifier(self) -> str: + return self._identifier + + @property + def abi_type(self) -> AbiType: + return AbiType.ByteArray + + @property + def stack_item(self) -> StackItemType: + return StackItemType.Buffer + + @property + def default_value(self) -> Any: + return bytes() + + def is_valid_key(self, value_type: IType) -> bool: + return value_type == self.valid_key + + @property + def valid_key(self) -> IType: + from boa3.model.type.type import Type + return Type.int + + @classmethod + def build(cls, value: Any): + from boa3.model.type.type import Type + return Type.bytes + + @classmethod + def build_sequence(cls, value_type: IType): + from boa3.model.type.type import Type + return Type.bytes + + @classmethod + def _is_type_of(cls, value: Any): + return type(value) is bytes or isinstance(value, BytesType) + + @property + def can_reassign_values(self) -> bool: + return False diff --git a/boa3/model/type/primitive/inttype.py b/boa3/model/type/primitive/inttype.py index 5e7e835a3..d41e2bff8 100644 --- a/boa3/model/type/primitive/inttype.py +++ b/boa3/model/type/primitive/inttype.py @@ -2,6 +2,7 @@ from boa3.model.type.itype import IType from boa3.neo.vm.type.AbiType import AbiType +from boa3.neo.vm.type.StackItemType import StackItemType class IntType(IType): @@ -21,6 +22,10 @@ def default_value(self) -> Any: def abi_type(self) -> AbiType: return AbiType.Integer + @property + def stack_item(self) -> StackItemType: + return StackItemType.Integer + @classmethod def build(cls, value: Any): if cls._is_type_of(value): diff --git a/boa3/model/type/primitive/strtype.py b/boa3/model/type/primitive/strtype.py index f4307715a..4e8e91a16 100644 --- a/boa3/model/type/primitive/strtype.py +++ b/boa3/model/type/primitive/strtype.py @@ -3,6 +3,7 @@ from boa3.model.type.itype import IType from boa3.model.type.sequence.sequencetype import SequenceType from boa3.neo.vm.type.AbiType import AbiType +from boa3.neo.vm.type.StackItemType import StackItemType class StrType(SequenceType): @@ -26,6 +27,10 @@ def default_value(self) -> Any: def abi_type(self) -> AbiType: return AbiType.String + @property + def stack_item(self) -> StackItemType: + return StackItemType.ByteString + @classmethod def build(cls, value: Any): if cls._is_type_of(value): diff --git a/boa3/model/type/sequence/genericsequencetype.py b/boa3/model/type/sequence/genericsequencetype.py index 77b736320..de2beebdb 100644 --- a/boa3/model/type/sequence/genericsequencetype.py +++ b/boa3/model/type/sequence/genericsequencetype.py @@ -22,6 +22,10 @@ def valid_key(self) -> IType: from boa3.model.type.type import Type return Type.int + @property + def is_generic(self) -> bool: + return True + @classmethod def build(cls, value: Any): if cls._is_type_of(value): diff --git a/boa3/model/type/sequence/mutable/genericmutablesequencetype.py b/boa3/model/type/sequence/mutable/genericmutablesequencetype.py index 6619689d7..39abe2718 100644 --- a/boa3/model/type/sequence/mutable/genericmutablesequencetype.py +++ b/boa3/model/type/sequence/mutable/genericmutablesequencetype.py @@ -22,6 +22,10 @@ def valid_key(self) -> IType: from boa3.model.type.type import Type return Type.int + @property + def is_generic(self) -> bool: + return True + @classmethod def build(cls, value: Any): if cls._is_type_of(value): diff --git a/boa3/model/type/sequence/sequencetype.py b/boa3/model/type/sequence/sequencetype.py index d397f465e..386aea2fe 100644 --- a/boa3/model/type/sequence/sequencetype.py +++ b/boa3/model/type/sequence/sequencetype.py @@ -3,6 +3,7 @@ from boa3.model.type.itype import IType from boa3.neo.vm.type.AbiType import AbiType +from boa3.neo.vm.type.StackItemType import StackItemType class SequenceType(IType, ABC): @@ -44,7 +45,11 @@ def get_types(cls, value: Any) -> List[IType]: @property def abi_type(self) -> AbiType: - return AbiType.Array # TODO: change when 'bytes' is implemented + return AbiType.Array + + @property + def stack_item(self) -> StackItemType: + return StackItemType.Array @abstractmethod def is_valid_key(self, value_type: IType) -> bool: diff --git a/boa3/model/type/type.py b/boa3/model/type/type.py index 0e2471a3b..0a8dc68d9 100644 --- a/boa3/model/type/type.py +++ b/boa3/model/type/type.py @@ -3,6 +3,7 @@ from boa3.model.type.anytype import anyType from boa3.model.type.itype import IType from boa3.model.type.primitive.booltype import BoolType +from boa3.model.type.primitive.bytestype import BytesType from boa3.model.type.primitive.inttype import IntType from boa3.model.type.primitive.nonetype import NoneType from boa3.model.type.primitive.strtype import StrType @@ -64,6 +65,7 @@ def get_generic_type(cls, type1: IType, type2: IType) -> IType: bool = BoolType() str = StrType() none = NoneType() + bytes = BytesType() tuple = TupleType() list = ListType() diff --git a/boa3_test/example/built_in_methods_test/LenBytes.py b/boa3_test/example/built_in_methods_test/LenBytes.py new file mode 100644 index 000000000..68cb92f87 --- /dev/null +++ b/boa3_test/example/built_in_methods_test/LenBytes.py @@ -0,0 +1,4 @@ +def Main() -> int: + a = b'\x01\x02\x03' + b = len(a) + return b diff --git a/boa3_test/example/bytes_test/GetValue.py b/boa3_test/example/bytes_test/GetValue.py new file mode 100644 index 000000000..761dac207 --- /dev/null +++ b/boa3_test/example/bytes_test/GetValue.py @@ -0,0 +1,2 @@ +def Main(a: bytes) -> int: + return a[0] diff --git a/boa3_test/example/bytes_test/GetValueNegativeIndex.py b/boa3_test/example/bytes_test/GetValueNegativeIndex.py new file mode 100644 index 000000000..3a976cb0f --- /dev/null +++ b/boa3_test/example/bytes_test/GetValueNegativeIndex.py @@ -0,0 +1,2 @@ +def Main(a: bytes) -> int: + return a[-1] diff --git a/boa3_test/example/bytes_test/LiteralBytes.py b/boa3_test/example/bytes_test/LiteralBytes.py new file mode 100644 index 000000000..077558869 --- /dev/null +++ b/boa3_test/example/bytes_test/LiteralBytes.py @@ -0,0 +1,2 @@ +def Main(): + a: bytes = b'\x01\x02\x03' diff --git a/boa3_test/example/bytes_test/SetValue.py b/boa3_test/example/bytes_test/SetValue.py new file mode 100644 index 000000000..b583227fa --- /dev/null +++ b/boa3_test/example/bytes_test/SetValue.py @@ -0,0 +1,3 @@ +def Main(a: bytes) -> bytes: + a[0] = 0x01 + return a diff --git a/boa3_test/tests/test_builtin_method.py b/boa3_test/tests/test_builtin_method.py index bcac6aa9d..0dd97c3fe 100644 --- a/boa3_test/tests/test_builtin_method.py +++ b/boa3_test/tests/test_builtin_method.py @@ -1,5 +1,6 @@ from boa3.boa3 import Boa3 from boa3.exception.CompilerError import MismatchedTypes, UnexpectedArgument, UnfilledArgument +from boa3.model.type.type import Type from boa3.neo.vm.opcode.Opcode import Opcode from boa3.neo.vm.type.Integer import Integer from boa3.neo.vm.type.String import String @@ -74,6 +75,29 @@ def test_len_of_str(self): output = Boa3.compile(path) self.assertEqual(expected_output, output) + def test_len_of_bytes(self): + byte_input = b'\x01\x02\x03' + expected_output = ( + Opcode.INITSLOT + + b'\x02' + + b'\x00' + + Opcode.PUSHDATA1 # push the bytes + + Integer(len(byte_input)).to_byte_array() + + byte_input + + Opcode.CONVERT + + Type.bytes.stack_item + + Opcode.STLOC0 + + Opcode.LDLOC0 + + Opcode.SIZE + + Opcode.STLOC1 + + Opcode.LDLOC1 + + Opcode.RET + ) + path = '%s/boa3_test/example/built_in_methods_test/LenBytes.py' % self.dirname + + output = Boa3.compile(path) + self.assertEqual(expected_output, output) + def test_len_of_no_collection(self): path = '%s/boa3_test/example/built_in_methods_test/LenMismatchedType.py' % self.dirname self.assertCompilerLogs(MismatchedTypes, path) diff --git a/boa3_test/tests/test_bytes.py b/boa3_test/tests/test_bytes.py new file mode 100644 index 000000000..d70d207db --- /dev/null +++ b/boa3_test/tests/test_bytes.py @@ -0,0 +1,80 @@ +from boa3.boa3 import Boa3 +from boa3.exception.CompilerError import UnresolvedOperation +from boa3.model.type.type import Type +from boa3.neo.vm.opcode.Opcode import Opcode +from boa3.neo.vm.type.Integer import Integer +from boa3_test.tests.boa_test import BoaTest + + +class TestBytes(BoaTest): + + def test_bytes_literal_value(self): + data = b'\x01\x02\x03' + expected_output = ( + Opcode.INITSLOT # function signature + + b'\x01' + + b'\x00' + + Opcode.PUSHDATA1 # a = b'\x01\x02\x03' + + Integer(len(data)).to_byte_array(min_length=1) + + data + + Opcode.CONVERT + + Type.bytes.stack_item + + Opcode.STLOC0 + + Opcode.PUSHNULL + + Opcode.RET # return + ) + + path = '%s/boa3_test/example/bytes_test/LiteralBytes.py' % self.dirname + output = Boa3.compile(path) + self.assertEqual(expected_output, output) + + def test_bytes_get_value(self): + expected_output = ( + Opcode.INITSLOT # function signature + + b'\x00' + + b'\x01' + + Opcode.LDARG0 # arg[0] + + Opcode.PUSH0 + + Opcode.DUP + + Opcode.SIGN + + Opcode.PUSHM1 + + Opcode.JMPNE + + Integer(5).to_byte_array(min_length=1, signed=True) + + Opcode.OVER + + Opcode.SIZE + + Opcode.ADD + + Opcode.PICKITEM + + Opcode.RET # return + ) + + path = '%s/boa3_test/example/bytes_test/GetValue.py' % self.dirname + output = Boa3.compile(path) + self.assertEqual(expected_output, output) + + def test_bytes_get_value_negative_index(self): + expected_output = ( + Opcode.INITSLOT # function signature + + b'\x00' + + b'\x01' + + Opcode.LDARG0 # arg[0] + + Opcode.PUSH1 + + Opcode.NEGATE + + Opcode.DUP + + Opcode.SIGN + + Opcode.PUSHM1 + + Opcode.JMPNE + + Integer(5).to_byte_array(min_length=1, signed=True) + + Opcode.OVER + + Opcode.SIZE + + Opcode.ADD + + Opcode.PICKITEM + + Opcode.RET # return + ) + + path = '%s/boa3_test/example/bytes_test/GetValueNegativeIndex.py' % self.dirname + output = Boa3.compile(path) + self.assertEqual(expected_output, output) + + def test_bytes_set_value(self): + path = '%s/boa3_test/example/bytes_test/SetValue.py' % self.dirname + self.assertCompilerLogs(UnresolvedOperation, path) diff --git a/boa3_test/tests/test_constant.py b/boa3_test/tests/test_constant.py index ed4a85e2a..287dbd224 100644 --- a/boa3_test/tests/test_constant.py +++ b/boa3_test/tests/test_constant.py @@ -3,9 +3,9 @@ from boa3.analyser.analyser import Analyser from boa3.compiler.codegenerator import CodeGenerator +from boa3.model.type.type import Type from boa3.neo.vm.opcode.Opcode import Opcode from boa3.neo.vm.type.Integer import Integer -from boa3.neo.vm.type.StackItemType import StackItemType from boa3.neo.vm.type.String import String from boa3_test.tests.boa_test import BoaTest @@ -30,7 +30,7 @@ def test_negative_integer_constant(self): + Integer(len(byte_input)).to_byte_array() + byte_input + Opcode.CONVERT # convert to integer - + StackItemType.Integer + + Type.int.stack_item ) generator = CodeGenerator({}) @@ -47,7 +47,7 @@ def test_one_byte_integer_constant(self): + Integer(len(byte_input)).to_byte_array(min_length=1) + byte_input + Opcode.CONVERT # convert to integer - + StackItemType.Integer + + Type.int.stack_item ) generator = CodeGenerator({}) @@ -64,7 +64,7 @@ def test_two_bytes_length_integer_constant(self): + Integer(len(byte_input)).to_byte_array(min_length=2) + byte_input + Opcode.CONVERT # convert to integer - + StackItemType.Integer + + Type.int.stack_item ) generator = CodeGenerator({}) @@ -81,7 +81,7 @@ def test_big_integer_constant(self): + Integer(len(byte_input)).to_byte_array(min_length=4) + byte_input + Opcode.CONVERT # convert to integer - + StackItemType.Integer + + Type.int.stack_item ) generator = CodeGenerator({}) diff --git a/boa3_test/tests/test_function.py b/boa3_test/tests/test_function.py index cc3272a77..e91544c34 100644 --- a/boa3_test/tests/test_function.py +++ b/boa3_test/tests/test_function.py @@ -1,8 +1,8 @@ from boa3.boa3 import Boa3 from boa3.exception.CompilerError import MismatchedTypes, TooManyReturns, TypeHintMissing +from boa3.model.type.type import Type from boa3.neo.vm.opcode.Opcode import Opcode from boa3.neo.vm.type.Integer import Integer -from boa3.neo.vm.type.StackItemType import StackItemType from boa3.neo.vm.type.String import String from boa3_test.tests.boa_test import BoaTest @@ -119,7 +119,7 @@ def test_default_return(self): + Opcode.PUSHDATA1 # return 20 + Integer(len(twenty)).to_byte_array() + twenty + Opcode.CONVERT - + StackItemType.Integer + + Type.int.stack_item + Opcode.RET + Opcode.PUSH0 # default return + Opcode.RET diff --git a/boa3_test/tests/test_if.py b/boa3_test/tests/test_if.py index 859309350..ba489a69d 100644 --- a/boa3_test/tests/test_if.py +++ b/boa3_test/tests/test_if.py @@ -1,8 +1,8 @@ from boa3.boa3 import Boa3 from boa3.exception.CompilerError import MismatchedTypes +from boa3.model.type.type import Type from boa3.neo.vm.opcode.Opcode import Opcode from boa3.neo.vm.type.Integer import Integer -from boa3.neo.vm.type.StackItemType import StackItemType from boa3_test.tests.boa_test import BoaTest @@ -252,7 +252,7 @@ def test_if_multiple_branches(self): + Integer(len(twenty)).to_byte_array() + twenty + Opcode.CONVERT - + StackItemType.Integer + + Type.int.stack_item + Opcode.STLOC0 + Opcode.LDLOC0 # return a + Opcode.RET