From a6329ca053b586ec43f2bc7cb40793b9830fa9c9 Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 14:03:23 -0500 Subject: [PATCH 01/15] encode for array --- pyteal/ast/abi/array_base.py | 67 +++++++++++++++++++++++++++++++++++- pyteal/ast/abi/type.py | 9 +++++ 2 files changed, 75 insertions(+), 1 deletion(-) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index 7fdf51dc6..b066335c3 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -13,9 +13,11 @@ from pyteal.ast.expr import Expr from pyteal.ast.seq import Seq from pyteal.ast.int import Int +from pyteal.ast.bytes import Bytes from pyteal.ast.if_ import If from pyteal.ast.unaryexpr import Len -from pyteal.ast.binaryexpr import ExtractUint16 +from pyteal.ast.binaryexpr import ExtractUint16, GetBit +from pyteal.ast.ternaryexpr import SetBit from pyteal.ast.naryexpr import Concat from pyteal.ast.abi.type import TypeSpec, BaseType, ComputedValue @@ -282,5 +284,68 @@ def store_into(self, output: T) -> Expr: valueLength = Int(arrayType._stride()) return output.decode(encodedArray, start_index=valueStart, length=valueLength) + def encode(self) -> Expr: + encodedArray = self.array.encode() + arrayType = self.array.type_spec() + + # If the array element type is Bool, we compute the bit index + # (if array is dynamic we add 16 to bit index for dynamic array length uint16 prefix) + # and decode bit with given array encoding and the bit index for boolean bit. + if self.array.type_spec() == BoolTypeSpec(): + bitIndex = self.index + if arrayType.is_dynamic(): + bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) + return SetBit(Bytes(b"\x00"), Int(0), GetBit(encodedArray, bitIndex)) + + # Compute the byteIndex (first byte indicating the element encoding) + # (If the array is dynamic, add 2 to byte index for dynamic array length uint16 prefix) + byteIndex = Int(arrayType._stride()) * self.index + if arrayType.is_length_dynamic(): + byteIndex = byteIndex + Int(Uint16TypeSpec().byte_length_static()) + + arrayLength = self.array.length() + + # Handling case for array elements are dynamic: + # * `byteIndex` is pointing at the uint16 byte encoding indicating the beginning offset of + # the array element byte encoding. + # + # * `valueStart` is extracted from the uint16 bytes pointed by `byteIndex`. + # + # * If `index == arrayLength - 1` (last element in array), `valueEnd` is pointing at the + # end of the array byte encoding. + # + # * otherwise, `valueEnd` is inferred from `nextValueStart`, which is the beginning offset + # of the next array element byte encoding. + + if arrayType.value_type_spec().is_dynamic(): + valueStart = ExtractUint16(encodedArray, byteIndex) + nextValueStart = ExtractUint16( + encodedArray, byteIndex + Int(Uint16TypeSpec().byte_length_static()) + ) + if arrayType.is_length_dynamic(): + valueStart = valueStart + Int(Uint16TypeSpec().byte_length_static()) + nextValueStart = nextValueStart + Int( + Uint16TypeSpec().byte_length_static() + ) + + valueEnd = ( + If(self.index + Int(1) == arrayLength) + .Then(Len(encodedArray)) + .Else(nextValueStart) + ) + + return substring_for_decoding( + encodedArray, start_index=valueStart, end_index=valueEnd + ) + + # Handling case for array elements are static: + # since array._stride() is element's static byte length + # we partition the substring for array element. + valueStart = byteIndex + valueLength = Int(arrayType._stride()) + return substring_for_decoding( + encodedArray, start_index=valueStart, length=valueLength + ) + ArrayElement.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/type.py b/pyteal/ast/abi/type.py index 3720154ab..2b22a0aa0 100644 --- a/pyteal/ast/abi/type.py +++ b/pyteal/ast/abi/type.py @@ -177,6 +177,15 @@ def store_into(self, output: T_co) -> Expr: # type: ignore[misc] """ pass + @abstractmethod + def encode(self) -> Expr: + """Get the encoding bytes of the value. + + Returns: + An expression which represents the computed value. + """ + return self.use(lambda value: value.encode()) + def use(self, action: Callable[[T_co], Expr]) -> Expr: """Compute the value and pass it to a callable expression. From 93f42ad053dd4082e2860f63d3adf14d78ac779b Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 14:12:40 -0500 Subject: [PATCH 02/15] minor --- pyteal/ast/abi/type.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyteal/ast/abi/type.py b/pyteal/ast/abi/type.py index 2b22a0aa0..6e4d7bbae 100644 --- a/pyteal/ast/abi/type.py +++ b/pyteal/ast/abi/type.py @@ -177,14 +177,13 @@ def store_into(self, output: T_co) -> Expr: # type: ignore[misc] """ pass - @abstractmethod def encode(self) -> Expr: """Get the encoding bytes of the value. Returns: An expression which represents the computed value. """ - return self.use(lambda value: value.encode()) + return self.use(lambda value: value.encode()) # type: ignore def use(self, action: Callable[[T_co], Expr]) -> Expr: """Compute the value and pass it to a callable expression. From 994d9bbd2166e13b96119f847439d5fd23f5af9e Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 15:18:32 -0500 Subject: [PATCH 03/15] minor --- pyteal/ast/abi/tuple.py | 293 ++++++++++++++--------------------- pyteal/ast/abi/tuple_test.py | 12 +- 2 files changed, 126 insertions(+), 179 deletions(-) diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index ff0f9f190..12d612207 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -1,4 +1,5 @@ from inspect import get_annotations +from dataclasses import dataclass from typing import ( List, Sequence, @@ -118,190 +119,138 @@ def _encode_tuple(values: Sequence[BaseType]) -> Expr: return Concat(*toConcat) -def _index_tuple_bytes( - value_types: Sequence[TypeSpec], encoded: Expr, index: int -) -> Expr: - if not (0 <= index < len(value_types)): - raise ValueError("Index outside of range") +@dataclass +class _IndexTuple: + value_types: Sequence[TypeSpec] + encoded: Expr - offset = 0 - ignoreNext = 0 - lastBoolStart = 0 - lastBoolLength = 0 - for i, typeBefore in enumerate(value_types[:index]): - if ignoreNext > 0: - ignoreNext -= 1 - continue - - if typeBefore == BoolTypeSpec(): - lastBoolStart = offset - lastBoolLength = _consecutive_bool_type_spec_num(value_types, i) - offset += _bool_sequence_length(lastBoolLength) - ignoreNext = lastBoolLength - 1 - continue - - if typeBefore.is_dynamic(): - offset += 2 - continue + def __call__(self, index: int, output: BaseType | None = None) -> Expr: + if index not in range(len(self.value_types)): + raise ValueError("Index outside of range") - offset += typeBefore.byte_length_static() - - valueType = value_types[index] - - if type(valueType) is Bool: - if ignoreNext > 0: - # value is in the middle of a bool sequence - bitOffsetInBoolSeq = lastBoolLength - ignoreNext - bitOffsetInEncoded = lastBoolStart * NUM_BITS_IN_BYTE + bitOffsetInBoolSeq - else: - # value is the beginning of a bool sequence (or a single bool) - bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE - return SetBit(Bytes(b"\x00"), Int(0), GetBit(encoded, Int(bitOffsetInEncoded))) - - if valueType.is_dynamic(): - hasNextDynamicValue = False - nextDynamicValueOffset = offset + 2 + offset = 0 ignoreNext = 0 - for i, typeAfter in enumerate(value_types[index + 1 :], start=index + 1): + lastBoolStart = 0 + lastBoolLength = 0 + for i, typeBefore in enumerate(self.value_types[:index]): if ignoreNext > 0: ignoreNext -= 1 continue - if type(typeAfter) is BoolTypeSpec: - boolLength = _consecutive_bool_type_spec_num(value_types, i) - nextDynamicValueOffset += _bool_sequence_length(boolLength) - ignoreNext = boolLength - 1 + if typeBefore == BoolTypeSpec(): + lastBoolStart = offset + lastBoolLength = _consecutive_bool_type_spec_num(self.value_types, i) + offset += _bool_sequence_length(lastBoolLength) + ignoreNext = lastBoolLength - 1 continue - if typeAfter.is_dynamic(): - hasNextDynamicValue = True - break - - nextDynamicValueOffset += typeAfter.byte_length_static() - - start_index = ExtractUint16(encoded, Int(offset)) - if not hasNextDynamicValue: - # This is the final dynamic value, so decode the substring from start_index to the end of - # encoded - return substring_for_decoding(encoded, start_index=start_index) - - # There is a dynamic value after this one, and end_index is where its tail starts, so decode - # the substring from start_index to end_index - end_index = ExtractUint16(encoded, Int(nextDynamicValueOffset)) - return substring_for_decoding( - encoded, start_index=start_index, end_index=end_index - ) - - start_index = Int(offset) - length = Int(valueType.byte_length_static()) - - if index + 1 == len(value_types): - if offset == 0: - # This is the first and only value in the tuple, so decode all of encoded - return encoded - # This is the last value in the tuple, so decode the substring from start_index to the end of - # encoded - return substring_for_decoding(encoded, start_index=start_index) - - if offset == 0: - # This is the first value in the tuple, so decode the substring from 0 with length length - return substring_for_decoding(encoded, length=length) - - # This is not the first or last value, so decode the substring from start_index with length length - return substring_for_decoding(encoded, start_index=start_index, length=length) - - -def _index_tuple( - value_types: Sequence[TypeSpec], encoded: Expr, index: int, output: BaseType -) -> Expr: - if not (0 <= index < len(value_types)): - raise ValueError("Index outside of range") - - offset = 0 - ignoreNext = 0 - lastBoolStart = 0 - lastBoolLength = 0 - for i, typeBefore in enumerate(value_types[:index]): - if ignoreNext > 0: - ignoreNext -= 1 - continue - - if typeBefore == BoolTypeSpec(): - lastBoolStart = offset - lastBoolLength = _consecutive_bool_type_spec_num(value_types, i) - offset += _bool_sequence_length(lastBoolLength) - ignoreNext = lastBoolLength - 1 - continue - - if typeBefore.is_dynamic(): - offset += 2 - continue - - offset += typeBefore.byte_length_static() + if typeBefore.is_dynamic(): + offset += 2 + continue - valueType = value_types[index] - if output.type_spec() != valueType: - raise TypeError("Output type does not match value type") + offset += typeBefore.byte_length_static() - if type(output) is Bool: - if ignoreNext > 0: - # value is in the middle of a bool sequence - bitOffsetInBoolSeq = lastBoolLength - ignoreNext - bitOffsetInEncoded = lastBoolStart * NUM_BITS_IN_BYTE + bitOffsetInBoolSeq - else: - # value is the beginning of a bool sequence (or a single bool) - bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE - return output.decode_bit(encoded, Int(bitOffsetInEncoded)) + valueType = self.value_types[index] + if output is not None and output.type_spec() != valueType: + raise TypeError("Output type does not match value type") - if valueType.is_dynamic(): - hasNextDynamicValue = False - nextDynamicValueOffset = offset + 2 - ignoreNext = 0 - for i, typeAfter in enumerate(value_types[index + 1 :], start=index + 1): + if type(valueType) is BoolTypeSpec: if ignoreNext > 0: - ignoreNext -= 1 - continue - - if type(typeAfter) is BoolTypeSpec: - boolLength = _consecutive_bool_type_spec_num(value_types, i) - nextDynamicValueOffset += _bool_sequence_length(boolLength) - ignoreNext = boolLength - 1 - continue - - if typeAfter.is_dynamic(): - hasNextDynamicValue = True - break + # value is in the middle of a bool sequence + bitOffsetInBoolSeq = lastBoolLength - ignoreNext + bitOffsetInEncoded = ( + lastBoolStart * NUM_BITS_IN_BYTE + bitOffsetInBoolSeq + ) + else: + # value is the beginning of a bool sequence (or a single bool) + bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE + + if output is None: + return SetBit( + Bytes(b"\x00"), + Int(0), + GetBit(self.encoded, Int(bitOffsetInEncoded)), + ) + else: + return cast(Bool, output).decode_bit( + self.encoded, Int(bitOffsetInEncoded) + ) - nextDynamicValueOffset += typeAfter.byte_length_static() + if valueType.is_dynamic(): + hasNextDynamicValue = False + nextDynamicValueOffset = offset + 2 + ignoreNext = 0 + for i, typeAfter in enumerate( + self.value_types[index + 1 :], start=index + 1 + ): + if ignoreNext > 0: + ignoreNext -= 1 + continue + + if type(typeAfter) is BoolTypeSpec: + boolLength = _consecutive_bool_type_spec_num(self.value_types, i) + nextDynamicValueOffset += _bool_sequence_length(boolLength) + ignoreNext = boolLength - 1 + continue + + if typeAfter.is_dynamic(): + hasNextDynamicValue = True + break + + nextDynamicValueOffset += typeAfter.byte_length_static() + + start_index = ExtractUint16(self.encoded, Int(offset)) + if not hasNextDynamicValue: + # This is the final dynamic value, so decode the substring from start_index to the end of + # encoded + if output is None: + return substring_for_decoding(self.encoded, start_index=start_index) + else: + return output.decode(self.encoded, start_index=start_index) + + # There is a dynamic value after this one, and end_index is where its tail starts, so decode + # the substring from start_index to end_index + end_index = ExtractUint16(self.encoded, Int(nextDynamicValueOffset)) + if output is None: + return substring_for_decoding( + self.encoded, start_index=start_index, end_index=end_index + ) + else: + return output.decode( + self.encoded, start_index=start_index, end_index=end_index + ) - start_index = ExtractUint16(encoded, Int(offset)) - if not hasNextDynamicValue: - # This is the final dynamic value, so decode the substring from start_index to the end of + start_index = Int(offset) + length = Int(valueType.byte_length_static()) + + if index + 1 == len(self.value_types): + if offset == 0: + # This is the first and only value in the tuple, so decode all of encoded + if output is None: + return self.encoded + else: + return output.decode(self.encoded) + # This is the last value in the tuple, so decode the substring from start_index to the end of # encoded - return output.decode(encoded, start_index=start_index) - - # There is a dynamic value after this one, and end_index is where its tail starts, so decode - # the substring from start_index to end_index - end_index = ExtractUint16(encoded, Int(nextDynamicValueOffset)) - return output.decode(encoded, start_index=start_index, end_index=end_index) - - start_index = Int(offset) - length = Int(valueType.byte_length_static()) + if output is None: + return substring_for_decoding(self.encoded, start_index=start_index) + else: + return output.decode(self.encoded, start_index=start_index) - if index + 1 == len(value_types): if offset == 0: - # This is the first and only value in the tuple, so decode all of encoded - return output.decode(encoded) - # This is the last value in the tuple, so decode the substring from start_index to the end of - # encoded - return output.decode(encoded, start_index=start_index) - - if offset == 0: - # This is the first value in the tuple, so decode the substring from 0 with length length - return output.decode(encoded, length=length) + # This is the first value in the tuple, so decode the substring from 0 with length length + if output is None: + return substring_for_decoding(self.encoded, length=length) + else: + return output.decode(self.encoded, length=length) - # This is not the first or last value, so decode the substring from start_index with length length - return output.decode(encoded, start_index=start_index, length=length) + # This is not the first or last value, so decode the substring from start_index with length length + if output is None: + return substring_for_decoding( + self.encoded, start_index=start_index, length=length + ) + else: + return output.decode(self.encoded, start_index=start_index, length=length) class TupleTypeSpec(TypeSpec): @@ -492,19 +441,17 @@ def produced_type_spec(self) -> TypeSpec: return self.tuple.type_spec().value_type_specs()[self.index] def store_into(self, output: T) -> Expr: - return _index_tuple( - self.tuple.type_spec().value_type_specs(), - self.tuple.encode(), + return _IndexTuple( + self.tuple.type_spec().value_type_specs(), self.tuple.encode() + )( self.index, output, ) def encode(self) -> Expr: - return _index_tuple_bytes( - self.tuple.type_spec().value_type_specs(), - self.tuple.encode(), - self.index, - ) + return _IndexTuple( + self.tuple.type_spec().value_type_specs(), self.tuple.encode() + )(self.index) TupleElement.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/tuple_test.py b/pyteal/ast/abi/tuple_test.py index b175ce7bd..3bd3244d0 100644 --- a/pyteal/ast/abi/tuple_test.py +++ b/pyteal/ast/abi/tuple_test.py @@ -3,7 +3,7 @@ import pyteal as pt from pyteal import abi -from pyteal.ast.abi.tuple import _encode_tuple, _index_tuple, TupleElement +from pyteal.ast.abi.tuple import _encode_tuple, _IndexTuple, TupleElement from pyteal.ast.abi.bool import _encode_bool_sequence from pyteal.ast.abi.util import substring_for_decoding from pyteal.ast.abi.type_test import ContainerType @@ -430,7 +430,7 @@ class IndexTest(NamedTuple): for i, test in enumerate(tests): output = test.types[test.typeIndex].new_instance() - expr = _index_tuple(test.types, encoded, test.typeIndex, output) + expr = _IndexTuple(test.types, encoded)(test.typeIndex, output) assert expr.type_of() == pt.TealType.none assert not expr.has_return() @@ -446,17 +446,17 @@ class IndexTest(NamedTuple): assert actual == expected, "Test at index {} failed".format(i) with pytest.raises(ValueError): - _index_tuple(test.types, encoded, len(test.types), output) + _IndexTuple(test.types, encoded)(len(test.types), output) with pytest.raises(ValueError): - _index_tuple(test.types, encoded, -1, output) + _IndexTuple(test.types, encoded)(-1, output) otherType = abi.Uint64() if output.type_spec() == otherType.type_spec(): otherType = abi.Uint16() with pytest.raises(TypeError): - _index_tuple(test.types, encoded, test.typeIndex, otherType) + _IndexTuple(test.types, encoded)(test.typeIndex, otherType) def test_TupleTypeSpec_eq(): @@ -825,7 +825,7 @@ def test_TupleElement_store_into(): assert expr.type_of() == pt.TealType.none assert not expr.has_return() - expectedExpr = _index_tuple(test, tupleValue.encode(), j, output) + expectedExpr = _IndexTuple(test, tupleValue.encode())(j, output) expected, _ = expectedExpr.__teal__(options) expected.addIncoming() expected = pt.TealBlock.NormalizeBlocks(expected) From 8aaefc9ee14703fbc16fa8e123bb8338135ad88d Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 15:30:56 -0500 Subject: [PATCH 04/15] unify logic for array part --- pyteal/ast/abi/array_base.py | 115 +++++++++++------------------------ 1 file changed, 37 insertions(+), 78 deletions(-) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index b066335c3..b7d2a33e9 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -209,20 +209,8 @@ def __init__(self, array: Array[T], index: Expr) -> None: def produced_type_spec(self) -> TypeSpec: return self.array.type_spec().value_type_spec() - def store_into(self, output: T) -> Expr: - """Partitions the byte string of the given ABI array and stores the byte string of array - element in the ABI value output. - - The function first checks if the output type matches with array element type, and throw - error if type-mismatch. - - Args: - output: An ABI typed value that the array element byte string stores into. - - Returns: - An expression that stores the byte string of the array element into value `output`. - """ - if output.type_spec() != self.produced_type_spec(): + def __proto_encoding_store_into(self, output: T | None = None) -> Expr: + if output is not None and output.type_spec() != self.produced_type_spec(): raise TealInputError("Output type does not match value type") encodedArray = self.array.encode() @@ -231,11 +219,15 @@ def store_into(self, output: T) -> Expr: # If the array element type is Bool, we compute the bit index # (if array is dynamic we add 16 to bit index for dynamic array length uint16 prefix) # and decode bit with given array encoding and the bit index for boolean bit. - if output.type_spec() == BoolTypeSpec(): + if self.array.type_spec().value_type_spec() == BoolTypeSpec(): bitIndex = self.index if arrayType.is_dynamic(): bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) - return cast(Bool, output).decode_bit(encodedArray, bitIndex) + + if output is not None: + return cast(Bool, output).decode_bit(encodedArray, bitIndex) + else: + return SetBit(Bytes(b"\x00"), Int(0), GetBit(encodedArray, bitIndex)) # Compute the byteIndex (first byte indicating the element encoding) # (If the array is dynamic, add 2 to byte index for dynamic array length uint16 prefix) @@ -273,79 +265,46 @@ def store_into(self, output: T) -> Expr: .Else(nextValueStart) ) - return output.decode( - encodedArray, start_index=valueStart, end_index=valueEnd - ) + if output is not None: + return output.decode( + encodedArray, start_index=valueStart, end_index=valueEnd + ) + else: + return substring_for_decoding( + encodedArray, start_index=valueStart, end_index=valueEnd + ) # Handling case for array elements are static: # since array._stride() is element's static byte length # we partition the substring for array element. valueStart = byteIndex valueLength = Int(arrayType._stride()) - return output.decode(encodedArray, start_index=valueStart, length=valueLength) - - def encode(self) -> Expr: - encodedArray = self.array.encode() - arrayType = self.array.type_spec() - - # If the array element type is Bool, we compute the bit index - # (if array is dynamic we add 16 to bit index for dynamic array length uint16 prefix) - # and decode bit with given array encoding and the bit index for boolean bit. - if self.array.type_spec() == BoolTypeSpec(): - bitIndex = self.index - if arrayType.is_dynamic(): - bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) - return SetBit(Bytes(b"\x00"), Int(0), GetBit(encodedArray, bitIndex)) - - # Compute the byteIndex (first byte indicating the element encoding) - # (If the array is dynamic, add 2 to byte index for dynamic array length uint16 prefix) - byteIndex = Int(arrayType._stride()) * self.index - if arrayType.is_length_dynamic(): - byteIndex = byteIndex + Int(Uint16TypeSpec().byte_length_static()) - - arrayLength = self.array.length() + if output is not None: + return output.decode( + encodedArray, start_index=valueStart, length=valueLength + ) + else: + return substring_for_decoding( + encodedArray, start_index=valueStart, length=valueLength + ) - # Handling case for array elements are dynamic: - # * `byteIndex` is pointing at the uint16 byte encoding indicating the beginning offset of - # the array element byte encoding. - # - # * `valueStart` is extracted from the uint16 bytes pointed by `byteIndex`. - # - # * If `index == arrayLength - 1` (last element in array), `valueEnd` is pointing at the - # end of the array byte encoding. - # - # * otherwise, `valueEnd` is inferred from `nextValueStart`, which is the beginning offset - # of the next array element byte encoding. + def store_into(self, output: T) -> Expr: + """Partitions the byte string of the given ABI array and stores the byte string of array + element in the ABI value output. - if arrayType.value_type_spec().is_dynamic(): - valueStart = ExtractUint16(encodedArray, byteIndex) - nextValueStart = ExtractUint16( - encodedArray, byteIndex + Int(Uint16TypeSpec().byte_length_static()) - ) - if arrayType.is_length_dynamic(): - valueStart = valueStart + Int(Uint16TypeSpec().byte_length_static()) - nextValueStart = nextValueStart + Int( - Uint16TypeSpec().byte_length_static() - ) + The function first checks if the output type matches with array element type, and throw + error if type-mismatch. - valueEnd = ( - If(self.index + Int(1) == arrayLength) - .Then(Len(encodedArray)) - .Else(nextValueStart) - ) + Args: + output: An ABI typed value that the array element byte string stores into. - return substring_for_decoding( - encodedArray, start_index=valueStart, end_index=valueEnd - ) + Returns: + An expression that stores the byte string of the array element into value `output`. + """ + return self.__proto_encoding_store_into(output) - # Handling case for array elements are static: - # since array._stride() is element's static byte length - # we partition the substring for array element. - valueStart = byteIndex - valueLength = Int(arrayType._stride()) - return substring_for_decoding( - encodedArray, start_index=valueStart, length=valueLength - ) + def encode(self) -> Expr: + return self.__proto_encoding_store_into() ArrayElement.__module__ = "pyteal.abi" From 803d3a0b1bc8fa029db1533a2fee3f974957c2d0 Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 15:39:45 -0500 Subject: [PATCH 05/15] minor --- pyteal/ast/abi/tuple_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pyteal/ast/abi/tuple_test.py b/pyteal/ast/abi/tuple_test.py index 3bd3244d0..841a2b375 100644 --- a/pyteal/ast/abi/tuple_test.py +++ b/pyteal/ast/abi/tuple_test.py @@ -1,4 +1,4 @@ -from typing import NamedTuple, List, Callable, Literal, cast +from typing import NamedTuple, Callable, Literal, cast import pytest import pyteal as pt @@ -13,7 +13,7 @@ def test_encodeTuple(): class EncodeTest(NamedTuple): - types: List[abi.BaseType] + types: list[abi.BaseType] expected: pt.Expr # variables used to construct the tests @@ -30,7 +30,7 @@ class EncodeTest(NamedTuple): tail_holder = pt.ScratchVar() encoded_tail = pt.ScratchVar() - tests: List[EncodeTest] = [ + tests: list[EncodeTest] = [ EncodeTest(types=[], expected=pt.Bytes("")), EncodeTest(types=[uint64_a], expected=uint64_a.encode()), EncodeTest( @@ -225,7 +225,7 @@ class EncodeTest(NamedTuple): def test_indexTuple(): class IndexTest(NamedTuple): - types: List[abi.TypeSpec] + types: list[abi.TypeSpec] typeIndex: int expected: Callable[[abi.BaseType], pt.Expr] @@ -239,7 +239,7 @@ class IndexTest(NamedTuple): encoded = pt.Bytes("encoded") - tests: List[IndexTest] = [ + tests: list[IndexTest] = [ IndexTest( types=[uint64_t], typeIndex=0, @@ -485,7 +485,7 @@ def test_TupleTypeSpec_value_type_specs(): def test_TupleTypeSpec_length_static(): - tests: List[List[abi.TypeSpec]] = [ + tests: list[list[abi.TypeSpec]] = [ [], [abi.Uint64TypeSpec()], [ @@ -751,7 +751,7 @@ def test_Tuple_encode(): def test_Tuple_length(): - tests: List[List[abi.TypeSpec]] = [ + tests: list[list[abi.TypeSpec]] = [ [], [abi.Uint64TypeSpec()], [ @@ -779,7 +779,7 @@ def test_Tuple_length(): def test_Tuple_getitem(): - tests: List[List[abi.TypeSpec]] = [ + tests: list[list[abi.TypeSpec]] = [ [], [abi.Uint64TypeSpec()], [ @@ -805,7 +805,7 @@ def test_Tuple_getitem(): def test_TupleElement_store_into(): - tests: List[List[abi.TypeSpec]] = [ + tests: list[list[abi.TypeSpec]] = [ [], [abi.Uint64TypeSpec()], [ From b0793c649a98abbc8be70b07f57534787749d3cf Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 15:49:55 -0500 Subject: [PATCH 06/15] naming --- pyteal/ast/abi/array_base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index b7d2a33e9..d54da0b65 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -209,7 +209,7 @@ def __init__(self, array: Array[T], index: Expr) -> None: def produced_type_spec(self) -> TypeSpec: return self.array.type_spec().value_type_spec() - def __proto_encoding_store_into(self, output: T | None = None) -> Expr: + def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: if output is not None and output.type_spec() != self.produced_type_spec(): raise TealInputError("Output type does not match value type") @@ -301,10 +301,10 @@ def store_into(self, output: T) -> Expr: Returns: An expression that stores the byte string of the array element into value `output`. """ - return self.__proto_encoding_store_into(output) + return self.__prototype_encoding_store_into(output) def encode(self) -> Expr: - return self.__proto_encoding_store_into() + return self.__prototype_encoding_store_into() ArrayElement.__module__ = "pyteal.abi" From 76e6b589af5e6a25e74846ce0befa86dafe2833f Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 17:01:56 -0500 Subject: [PATCH 07/15] abstraction: get part from full encoding --- pyteal/ast/abi/array_base.py | 49 +++++++++++------------ pyteal/ast/abi/tuple.py | 75 +++++++++++++++++------------------- pyteal/ast/abi/util.py | 49 +++++++++++++++++++++++ 3 files changed, 107 insertions(+), 66 deletions(-) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index d54da0b65..a9067c31b 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -13,18 +13,19 @@ from pyteal.ast.expr import Expr from pyteal.ast.seq import Seq from pyteal.ast.int import Int -from pyteal.ast.bytes import Bytes from pyteal.ast.if_ import If from pyteal.ast.unaryexpr import Len -from pyteal.ast.binaryexpr import ExtractUint16, GetBit -from pyteal.ast.ternaryexpr import SetBit +from pyteal.ast.binaryexpr import ExtractUint16 from pyteal.ast.naryexpr import Concat from pyteal.ast.abi.type import TypeSpec, BaseType, ComputedValue from pyteal.ast.abi.tuple import _encode_tuple -from pyteal.ast.abi.bool import Bool, BoolTypeSpec +from pyteal.ast.abi.bool import BoolTypeSpec from pyteal.ast.abi.uint import Uint16, Uint16TypeSpec -from pyteal.ast.abi.util import substring_for_decoding +from pyteal.ast.abi.util import ( + substring_for_decoding, + _get_encoding_or_store_from_encoded_bytes, +) T = TypeVar("T", bound=BaseType) @@ -223,11 +224,9 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: bitIndex = self.index if arrayType.is_dynamic(): bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) - - if output is not None: - return cast(Bool, output).decode_bit(encodedArray, bitIndex) - else: - return SetBit(Bytes(b"\x00"), Int(0), GetBit(encodedArray, bitIndex)) + return _get_encoding_or_store_from_encoded_bytes( + BoolTypeSpec(), encodedArray, output, start_index=bitIndex + ) # Compute the byteIndex (first byte indicating the element encoding) # (If the array is dynamic, add 2 to byte index for dynamic array length uint16 prefix) @@ -265,28 +264,26 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: .Else(nextValueStart) ) - if output is not None: - return output.decode( - encodedArray, start_index=valueStart, end_index=valueEnd - ) - else: - return substring_for_decoding( - encodedArray, start_index=valueStart, end_index=valueEnd - ) + return _get_encoding_or_store_from_encoded_bytes( + arrayType.value_type_spec(), + encodedArray, + output, + start_index=valueStart, + end_index=valueEnd, + ) # Handling case for array elements are static: # since array._stride() is element's static byte length # we partition the substring for array element. valueStart = byteIndex valueLength = Int(arrayType._stride()) - if output is not None: - return output.decode( - encodedArray, start_index=valueStart, length=valueLength - ) - else: - return substring_for_decoding( - encodedArray, start_index=valueStart, length=valueLength - ) + return _get_encoding_or_store_from_encoded_bytes( + arrayType.value_type_spec(), + encodedArray, + output, + start_index=valueStart, + length=valueLength, + ) def store_into(self, output: T) -> Expr: """Partitions the byte string of the given ABI array and stores the byte string of array diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 12d612207..e9c4a38df 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -15,15 +15,14 @@ ) from collections import OrderedDict -from pyteal.types import TealType +from pyteal.types import TealType, require_type from pyteal.errors import TealInputError, TealInternalError from pyteal.ast.expr import Expr from pyteal.ast.seq import Seq from pyteal.ast.int import Int from pyteal.ast.bytes import Bytes from pyteal.ast.unaryexpr import Len -from pyteal.ast.binaryexpr import ExtractUint16, GetBit -from pyteal.ast.ternaryexpr import SetBit +from pyteal.ast.binaryexpr import ExtractUint16 from pyteal.ast.naryexpr import Concat from pyteal.ast.abstractvar import alloc_abstract_var @@ -38,7 +37,11 @@ _bool_aware_static_byte_length, ) from pyteal.ast.abi.uint import NUM_BITS_IN_BYTE, Uint16 -from pyteal.ast.abi.util import substring_for_decoding, type_spec_from_annotation +from pyteal.ast.abi.util import ( + substring_for_decoding, + type_spec_from_annotation, + _get_encoding_or_store_from_encoded_bytes, +) def _encode_tuple(values: Sequence[BaseType]) -> Expr: @@ -124,6 +127,9 @@ class _IndexTuple: value_types: Sequence[TypeSpec] encoded: Expr + def __post_init__(self): + require_type(self.encoded, TealType.bytes) + def __call__(self, index: int, output: BaseType | None = None) -> Expr: if index not in range(len(self.value_types)): raise ValueError("Index outside of range") @@ -165,16 +171,12 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: # value is the beginning of a bool sequence (or a single bool) bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE - if output is None: - return SetBit( - Bytes(b"\x00"), - Int(0), - GetBit(self.encoded, Int(bitOffsetInEncoded)), - ) - else: - return cast(Bool, output).decode_bit( - self.encoded, Int(bitOffsetInEncoded) - ) + return _get_encoding_or_store_from_encoded_bytes( + BoolTypeSpec(), + self.encoded, + output, + start_index=Int(bitOffsetInEncoded), + ) if valueType.is_dynamic(): hasNextDynamicValue = False @@ -203,22 +205,20 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: if not hasNextDynamicValue: # This is the final dynamic value, so decode the substring from start_index to the end of # encoded - if output is None: - return substring_for_decoding(self.encoded, start_index=start_index) - else: - return output.decode(self.encoded, start_index=start_index) + return _get_encoding_or_store_from_encoded_bytes( + valueType, self.encoded, output, start_index=start_index + ) # There is a dynamic value after this one, and end_index is where its tail starts, so decode # the substring from start_index to end_index end_index = ExtractUint16(self.encoded, Int(nextDynamicValueOffset)) - if output is None: - return substring_for_decoding( - self.encoded, start_index=start_index, end_index=end_index - ) - else: - return output.decode( - self.encoded, start_index=start_index, end_index=end_index - ) + return _get_encoding_or_store_from_encoded_bytes( + valueType, + self.encoded, + output, + start_index=start_index, + end_index=end_index, + ) start_index = Int(offset) length = Int(valueType.byte_length_static()) @@ -232,25 +232,20 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: return output.decode(self.encoded) # This is the last value in the tuple, so decode the substring from start_index to the end of # encoded - if output is None: - return substring_for_decoding(self.encoded, start_index=start_index) - else: - return output.decode(self.encoded, start_index=start_index) + return _get_encoding_or_store_from_encoded_bytes( + valueType, self.encoded, output, start_index=start_index + ) if offset == 0: # This is the first value in the tuple, so decode the substring from 0 with length length - if output is None: - return substring_for_decoding(self.encoded, length=length) - else: - return output.decode(self.encoded, length=length) + return _get_encoding_or_store_from_encoded_bytes( + valueType, self.encoded, output, length=length + ) # This is not the first or last value, so decode the substring from start_index with length length - if output is None: - return substring_for_decoding( - self.encoded, start_index=start_index, length=length - ) - else: - return output.decode(self.encoded, start_index=start_index, length=length) + return _get_encoding_or_store_from_encoded_bytes( + valueType, self.encoded, output, start_index=start_index, length=length + ) class TupleTypeSpec(TypeSpec): diff --git a/pyteal/ast/abi/util.py b/pyteal/ast/abi/util.py index 36b828390..b8b6d8a94 100644 --- a/pyteal/ast/abi/util.py +++ b/pyteal/ast/abi/util.py @@ -13,6 +13,7 @@ import algosdk.abi from pyteal.errors import TealInputError +from pyteal.types import require_type, TealType from pyteal.ast.expr import Expr from pyteal.ast.int import Int from pyteal.ast.substring import Extract, Substring, Suffix @@ -593,3 +594,51 @@ def type_spec_is_assignable_to(a: TypeSpec, b: TypeSpec) -> bool: return True return False + + +def _get_encoding_or_store_from_encoded_bytes( + encoding_type: TypeSpec, + full_encoding: Expr, + output: BaseType | None = None, + *, + start_index: Expr | None = None, + end_index: Expr | None = None, + length: Expr | None = None, +) -> Expr: + from pyteal.ast.abi import BoolTypeSpec, Bool + from pyteal.ast.bytes import Bytes + from pyteal.ast.binaryexpr import GetBit + from pyteal.ast.ternaryexpr import SetBit + + require_type(full_encoding, TealType.bytes) + + match encoding_type: + case BoolTypeSpec(): + if start_index is None: + raise TealInputError( + "on BoolTypeSpec, requiring start index to be not None." + ) + + if output is None: + return SetBit( + Bytes(b"\x00"), + Int(0), + GetBit(full_encoding, start_index), + ) + else: + return cast(Bool, output).decode_bit(full_encoding, start_index) + case _: + if output is None: + return substring_for_decoding( + encoded=full_encoding, + start_index=start_index, + end_index=end_index, + length=length, + ) + else: + return output.decode( + full_encoding, + start_index=start_index, + end_index=end_index, + length=length, + ) From 0292b030df8001bee06c3b7a560edb651f16b325 Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 17:16:26 -0500 Subject: [PATCH 08/15] naming --- pyteal/ast/abi/array_base.py | 8 ++++---- pyteal/ast/abi/tuple.py | 14 +++++++------- pyteal/ast/abi/util.py | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index a9067c31b..c7faef272 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -24,7 +24,7 @@ from pyteal.ast.abi.uint import Uint16, Uint16TypeSpec from pyteal.ast.abi.util import ( substring_for_decoding, - _get_encoding_or_store_from_encoded_bytes, + _get_or_store_encoded_bytes, ) T = TypeVar("T", bound=BaseType) @@ -224,7 +224,7 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: bitIndex = self.index if arrayType.is_dynamic(): bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( BoolTypeSpec(), encodedArray, output, start_index=bitIndex ) @@ -264,7 +264,7 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: .Else(nextValueStart) ) - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( arrayType.value_type_spec(), encodedArray, output, @@ -277,7 +277,7 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: # we partition the substring for array element. valueStart = byteIndex valueLength = Int(arrayType._stride()) - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( arrayType.value_type_spec(), encodedArray, output, diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index e9c4a38df..7091088bd 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -40,7 +40,7 @@ from pyteal.ast.abi.util import ( substring_for_decoding, type_spec_from_annotation, - _get_encoding_or_store_from_encoded_bytes, + _get_or_store_encoded_bytes, ) @@ -171,7 +171,7 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: # value is the beginning of a bool sequence (or a single bool) bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( BoolTypeSpec(), self.encoded, output, @@ -205,14 +205,14 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: if not hasNextDynamicValue: # This is the final dynamic value, so decode the substring from start_index to the end of # encoded - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( valueType, self.encoded, output, start_index=start_index ) # There is a dynamic value after this one, and end_index is where its tail starts, so decode # the substring from start_index to end_index end_index = ExtractUint16(self.encoded, Int(nextDynamicValueOffset)) - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( valueType, self.encoded, output, @@ -232,18 +232,18 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: return output.decode(self.encoded) # This is the last value in the tuple, so decode the substring from start_index to the end of # encoded - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( valueType, self.encoded, output, start_index=start_index ) if offset == 0: # This is the first value in the tuple, so decode the substring from 0 with length length - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( valueType, self.encoded, output, length=length ) # This is not the first or last value, so decode the substring from start_index with length length - return _get_encoding_or_store_from_encoded_bytes( + return _get_or_store_encoded_bytes( valueType, self.encoded, output, start_index=start_index, length=length ) diff --git a/pyteal/ast/abi/util.py b/pyteal/ast/abi/util.py index b8b6d8a94..71cd7d4a2 100644 --- a/pyteal/ast/abi/util.py +++ b/pyteal/ast/abi/util.py @@ -596,7 +596,7 @@ def type_spec_is_assignable_to(a: TypeSpec, b: TypeSpec) -> bool: return False -def _get_encoding_or_store_from_encoded_bytes( +def _get_or_store_encoded_bytes( encoding_type: TypeSpec, full_encoding: Expr, output: BaseType | None = None, From f503c5f46d50ef9b5b7aeebf83bd1304490dc79e Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 17:19:19 -0500 Subject: [PATCH 09/15] moving imports out --- pyteal/ast/abi/util.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyteal/ast/abi/util.py b/pyteal/ast/abi/util.py index 71cd7d4a2..7cf893377 100644 --- a/pyteal/ast/abi/util.py +++ b/pyteal/ast/abi/util.py @@ -15,6 +15,9 @@ from pyteal.errors import TealInputError from pyteal.types import require_type, TealType from pyteal.ast.expr import Expr +from pyteal.ast.bytes import Bytes +from pyteal.ast.binaryexpr import GetBit +from pyteal.ast.ternaryexpr import SetBit from pyteal.ast.int import Int from pyteal.ast.substring import Extract, Substring, Suffix from pyteal.ast.abi.type import TypeSpec, BaseType @@ -606,9 +609,6 @@ def _get_or_store_encoded_bytes( length: Expr | None = None, ) -> Expr: from pyteal.ast.abi import BoolTypeSpec, Bool - from pyteal.ast.bytes import Bytes - from pyteal.ast.binaryexpr import GetBit - from pyteal.ast.ternaryexpr import SetBit require_type(full_encoding, TealType.bytes) From 33c3e8767734206fc3e72b4b8b1d8b5e6ec5f7ab Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 18:53:22 -0500 Subject: [PATCH 10/15] dataclass --- pyteal/ast/abi/array_base.py | 18 ++++---- pyteal/ast/abi/tuple.py | 36 ++++++++-------- pyteal/ast/abi/util.py | 83 +++++++++++++++++++----------------- 3 files changed, 68 insertions(+), 69 deletions(-) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index c7faef272..638fb83a2 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -24,7 +24,7 @@ from pyteal.ast.abi.uint import Uint16, Uint16TypeSpec from pyteal.ast.abi.util import ( substring_for_decoding, - _get_or_store_encoded_bytes, + _GetAgainstEncoding, ) T = TypeVar("T", bound=BaseType) @@ -224,9 +224,9 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: bitIndex = self.index if arrayType.is_dynamic(): bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) - return _get_or_store_encoded_bytes( - BoolTypeSpec(), encodedArray, output, start_index=bitIndex - ) + return _GetAgainstEncoding( + BoolTypeSpec(), encodedArray, start_index=bitIndex + ).get_or_store(output) # Compute the byteIndex (first byte indicating the element encoding) # (If the array is dynamic, add 2 to byte index for dynamic array length uint16 prefix) @@ -264,26 +264,24 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: .Else(nextValueStart) ) - return _get_or_store_encoded_bytes( + return _GetAgainstEncoding( arrayType.value_type_spec(), encodedArray, - output, start_index=valueStart, end_index=valueEnd, - ) + ).get_or_store(output) # Handling case for array elements are static: # since array._stride() is element's static byte length # we partition the substring for array element. valueStart = byteIndex valueLength = Int(arrayType._stride()) - return _get_or_store_encoded_bytes( + return _GetAgainstEncoding( arrayType.value_type_spec(), encodedArray, - output, start_index=valueStart, length=valueLength, - ) + ).get_or_store(output) def store_into(self, output: T) -> Expr: """Partitions the byte string of the given ABI array and stores the byte string of array diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 7091088bd..142e98ab0 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -40,7 +40,7 @@ from pyteal.ast.abi.util import ( substring_for_decoding, type_spec_from_annotation, - _get_or_store_encoded_bytes, + _GetAgainstEncoding, ) @@ -171,12 +171,11 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: # value is the beginning of a bool sequence (or a single bool) bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE - return _get_or_store_encoded_bytes( + return _GetAgainstEncoding( BoolTypeSpec(), self.encoded, - output, start_index=Int(bitOffsetInEncoded), - ) + ).get_or_store(output) if valueType.is_dynamic(): hasNextDynamicValue = False @@ -205,20 +204,19 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: if not hasNextDynamicValue: # This is the final dynamic value, so decode the substring from start_index to the end of # encoded - return _get_or_store_encoded_bytes( - valueType, self.encoded, output, start_index=start_index - ) + return _GetAgainstEncoding( + valueType, self.encoded, start_index=start_index + ).get_or_store(output) # There is a dynamic value after this one, and end_index is where its tail starts, so decode # the substring from start_index to end_index end_index = ExtractUint16(self.encoded, Int(nextDynamicValueOffset)) - return _get_or_store_encoded_bytes( + return _GetAgainstEncoding( valueType, self.encoded, - output, start_index=start_index, end_index=end_index, - ) + ).get_or_store(output) start_index = Int(offset) length = Int(valueType.byte_length_static()) @@ -232,20 +230,20 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: return output.decode(self.encoded) # This is the last value in the tuple, so decode the substring from start_index to the end of # encoded - return _get_or_store_encoded_bytes( - valueType, self.encoded, output, start_index=start_index - ) + return _GetAgainstEncoding( + valueType, self.encoded, start_index=start_index + ).get_or_store(output) if offset == 0: # This is the first value in the tuple, so decode the substring from 0 with length length - return _get_or_store_encoded_bytes( - valueType, self.encoded, output, length=length - ) + return _GetAgainstEncoding( + valueType, self.encoded, length=length + ).get_or_store(output) # This is not the first or last value, so decode the substring from start_index with length length - return _get_or_store_encoded_bytes( - valueType, self.encoded, output, start_index=start_index, length=length - ) + return _GetAgainstEncoding( + valueType, self.encoded, start_index=start_index, length=length + ).get_or_store(output) class TupleTypeSpec(TypeSpec): diff --git a/pyteal/ast/abi/util.py b/pyteal/ast/abi/util.py index 7cf893377..c9926b01e 100644 --- a/pyteal/ast/abi/util.py +++ b/pyteal/ast/abi/util.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass, field from typing import ( Any, Literal, @@ -599,46 +600,48 @@ def type_spec_is_assignable_to(a: TypeSpec, b: TypeSpec) -> bool: return False -def _get_or_store_encoded_bytes( - encoding_type: TypeSpec, - full_encoding: Expr, - output: BaseType | None = None, - *, - start_index: Expr | None = None, - end_index: Expr | None = None, - length: Expr | None = None, -) -> Expr: - from pyteal.ast.abi import BoolTypeSpec, Bool +@dataclass +class _GetAgainstEncoding: + type_spec: TypeSpec + full_encoding: Expr + start_index: Expr | None = field(kw_only=True, default=None) + end_index: Expr | None = field(kw_only=True, default=None) + length: Expr | None = field(kw_only=True, default=None) - require_type(full_encoding, TealType.bytes) + def __post_init__(self): + from pyteal.ast.abi import BoolTypeSpec - match encoding_type: - case BoolTypeSpec(): - if start_index is None: - raise TealInputError( - "on BoolTypeSpec, requiring start index to be not None." - ) + require_type(self.full_encoding, TealType.bytes) + if self.type_spec == BoolTypeSpec(): + require_type(self.start_index, TealType.uint64) - if output is None: - return SetBit( - Bytes(b"\x00"), - Int(0), - GetBit(full_encoding, start_index), - ) - else: - return cast(Bool, output).decode_bit(full_encoding, start_index) - case _: - if output is None: - return substring_for_decoding( - encoded=full_encoding, - start_index=start_index, - end_index=end_index, - length=length, - ) - else: - return output.decode( - full_encoding, - start_index=start_index, - end_index=end_index, - length=length, - ) + def get_or_store(self, output: BaseType | None = None) -> Expr: + from pyteal.ast.abi import BoolTypeSpec, Bool + + match self.type_spec: + case BoolTypeSpec(): + if output is None: + return SetBit( + Bytes(b"\x00"), + Int(0), + GetBit(self.full_encoding, cast(Expr, self.start_index)), + ) + else: + return cast(Bool, output).decode_bit( + self.full_encoding, cast(Expr, self.start_index) + ) + case _: + if output is None: + return substring_for_decoding( + encoded=self.full_encoding, + start_index=self.start_index, + end_index=self.end_index, + length=self.length, + ) + else: + return output.decode( + self.full_encoding, + start_index=self.start_index, + end_index=self.end_index, + length=self.length, + ) From 69b6b8922ade9a4b54a8a8e707bd1cd069bf109c Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 18:58:38 -0500 Subject: [PATCH 11/15] shuffle input orders --- pyteal/ast/abi/array_base.py | 6 +++--- pyteal/ast/abi/tuple.py | 12 ++++++------ pyteal/ast/abi/util.py | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py index 638fb83a2..e88558c58 100644 --- a/pyteal/ast/abi/array_base.py +++ b/pyteal/ast/abi/array_base.py @@ -225,7 +225,7 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: if arrayType.is_dynamic(): bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) return _GetAgainstEncoding( - BoolTypeSpec(), encodedArray, start_index=bitIndex + encodedArray, BoolTypeSpec(), start_index=bitIndex ).get_or_store(output) # Compute the byteIndex (first byte indicating the element encoding) @@ -265,8 +265,8 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: ) return _GetAgainstEncoding( - arrayType.value_type_spec(), encodedArray, + arrayType.value_type_spec(), start_index=valueStart, end_index=valueEnd, ).get_or_store(output) @@ -277,8 +277,8 @@ def __prototype_encoding_store_into(self, output: T | None = None) -> Expr: valueStart = byteIndex valueLength = Int(arrayType._stride()) return _GetAgainstEncoding( - arrayType.value_type_spec(), encodedArray, + arrayType.value_type_spec(), start_index=valueStart, length=valueLength, ).get_or_store(output) diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 142e98ab0..48df56e2f 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -172,8 +172,8 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: bitOffsetInEncoded = offset * NUM_BITS_IN_BYTE return _GetAgainstEncoding( - BoolTypeSpec(), self.encoded, + BoolTypeSpec(), start_index=Int(bitOffsetInEncoded), ).get_or_store(output) @@ -205,15 +205,15 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: # This is the final dynamic value, so decode the substring from start_index to the end of # encoded return _GetAgainstEncoding( - valueType, self.encoded, start_index=start_index + self.encoded, valueType, start_index=start_index ).get_or_store(output) # There is a dynamic value after this one, and end_index is where its tail starts, so decode # the substring from start_index to end_index end_index = ExtractUint16(self.encoded, Int(nextDynamicValueOffset)) return _GetAgainstEncoding( - valueType, self.encoded, + valueType, start_index=start_index, end_index=end_index, ).get_or_store(output) @@ -231,18 +231,18 @@ def __call__(self, index: int, output: BaseType | None = None) -> Expr: # This is the last value in the tuple, so decode the substring from start_index to the end of # encoded return _GetAgainstEncoding( - valueType, self.encoded, start_index=start_index + self.encoded, valueType, start_index=start_index ).get_or_store(output) if offset == 0: # This is the first value in the tuple, so decode the substring from 0 with length length return _GetAgainstEncoding( - valueType, self.encoded, length=length + self.encoded, valueType, length=length ).get_or_store(output) # This is not the first or last value, so decode the substring from start_index with length length return _GetAgainstEncoding( - valueType, self.encoded, start_index=start_index, length=length + self.encoded, valueType, start_index=start_index, length=length ).get_or_store(output) diff --git a/pyteal/ast/abi/util.py b/pyteal/ast/abi/util.py index c9926b01e..127c3d2d0 100644 --- a/pyteal/ast/abi/util.py +++ b/pyteal/ast/abi/util.py @@ -602,8 +602,8 @@ def type_spec_is_assignable_to(a: TypeSpec, b: TypeSpec) -> bool: @dataclass class _GetAgainstEncoding: - type_spec: TypeSpec full_encoding: Expr + type_spec: TypeSpec start_index: Expr | None = field(kw_only=True, default=None) end_index: Expr | None = field(kw_only=True, default=None) length: Expr | None = field(kw_only=True, default=None) From 9d0b75a933a7bfc96b4786c8c0bd8668bfcd23f5 Mon Sep 17 00:00:00 2001 From: Hang Su Date: Tue, 3 Jan 2023 19:02:06 -0500 Subject: [PATCH 12/15] __call__ rename method to get_or_store --- pyteal/ast/abi/tuple.py | 6 +++--- pyteal/ast/abi/tuple_test.py | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 48df56e2f..fa0f78683 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -130,7 +130,7 @@ class _IndexTuple: def __post_init__(self): require_type(self.encoded, TealType.bytes) - def __call__(self, index: int, output: BaseType | None = None) -> Expr: + def get_or_store(self, index: int, output: BaseType | None = None) -> Expr: if index not in range(len(self.value_types)): raise ValueError("Index outside of range") @@ -436,7 +436,7 @@ def produced_type_spec(self) -> TypeSpec: def store_into(self, output: T) -> Expr: return _IndexTuple( self.tuple.type_spec().value_type_specs(), self.tuple.encode() - )( + ).get_or_store( self.index, output, ) @@ -444,7 +444,7 @@ def store_into(self, output: T) -> Expr: def encode(self) -> Expr: return _IndexTuple( self.tuple.type_spec().value_type_specs(), self.tuple.encode() - )(self.index) + ).get_or_store(self.index) TupleElement.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/tuple_test.py b/pyteal/ast/abi/tuple_test.py index 841a2b375..9bfe71a05 100644 --- a/pyteal/ast/abi/tuple_test.py +++ b/pyteal/ast/abi/tuple_test.py @@ -430,7 +430,7 @@ class IndexTest(NamedTuple): for i, test in enumerate(tests): output = test.types[test.typeIndex].new_instance() - expr = _IndexTuple(test.types, encoded)(test.typeIndex, output) + expr = _IndexTuple(test.types, encoded).get_or_store(test.typeIndex, output) assert expr.type_of() == pt.TealType.none assert not expr.has_return() @@ -446,17 +446,17 @@ class IndexTest(NamedTuple): assert actual == expected, "Test at index {} failed".format(i) with pytest.raises(ValueError): - _IndexTuple(test.types, encoded)(len(test.types), output) + _IndexTuple(test.types, encoded).get_or_store(len(test.types), output) with pytest.raises(ValueError): - _IndexTuple(test.types, encoded)(-1, output) + _IndexTuple(test.types, encoded).get_or_store(-1, output) otherType = abi.Uint64() if output.type_spec() == otherType.type_spec(): otherType = abi.Uint16() with pytest.raises(TypeError): - _IndexTuple(test.types, encoded)(test.typeIndex, otherType) + _IndexTuple(test.types, encoded).get_or_store(test.typeIndex, otherType) def test_TupleTypeSpec_eq(): @@ -825,7 +825,9 @@ def test_TupleElement_store_into(): assert expr.type_of() == pt.TealType.none assert not expr.has_return() - expectedExpr = _IndexTuple(test, tupleValue.encode())(j, output) + expectedExpr = _IndexTuple(test, tupleValue.encode()).get_or_store( + j, output + ) expected, _ = expectedExpr.__teal__(options) expected.addIncoming() expected = pt.TealBlock.NormalizeBlocks(expected) From 44360d1a5af75828055d595bca5e7af7efc90625 Mon Sep 17 00:00:00 2001 From: Hang Su Date: Wed, 4 Jan 2023 09:03:24 -0500 Subject: [PATCH 13/15] testcase for array elem encode --- pyteal/ast/abi/array_base_test.py | 102 ++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/pyteal/ast/abi/array_base_test.py b/pyteal/ast/abi/array_base_test.py index 74dfa74ec..e48703648 100644 --- a/pyteal/ast/abi/array_base_test.py +++ b/pyteal/ast/abi/array_base_test.py @@ -161,3 +161,105 @@ def test_ArrayElement_store_into(): with pytest.raises(pt.TealInputError): element.store_into(abi.Tuple(abi.TupleTypeSpec(elementType))) + + +def test_ArrayElement_encoding(): + from pyteal.ast.abi.util import substring_for_decoding + + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + staticArrayType = abi.StaticArrayTypeSpec(elementType, 100) + staticArray = staticArrayType.new_instance() + index = pt.Int(9) + + element = abi.ArrayElement(staticArray, index) + expr = element.encode() + + encoded = staticArray.encode() + stride = pt.Int(staticArray.type_spec()._stride()) + expectedLength = staticArray.length() + if elementType == abi.BoolTypeSpec(): + expectedExpr = pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, index), + ) + elif not elementType.is_dynamic(): + expectedExpr = substring_for_decoding( + encoded, start_index=stride * index, length=stride + ) + else: + expectedExpr = substring_for_decoding( + encoded, + start_index=pt.ExtractUint16(encoded, stride * index), + end_index=pt.If(index + pt.Int(1) == expectedLength) + .Then(pt.Len(encoded)) + .Else(pt.ExtractUint16(encoded, stride * index + pt.Int(2))), + ) + + expected, _ = expectedExpr.__teal__(options) + expected.addIncoming() + expected = pt.TealBlock.NormalizeBlocks(expected) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + element.store_into(abi.Tuple(abi.TupleTypeSpec(elementType))) + + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + dynamicArray = dynamicArrayType.new_instance() + index = pt.Int(9) + + element = abi.ArrayElement(dynamicArray, index) + expr = element.encode() + + encoded = dynamicArray.encode() + stride = pt.Int(dynamicArray.type_spec()._stride()) + expectedLength = dynamicArray.length() + if elementType == abi.BoolTypeSpec(): + expectedExpr = pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, index + pt.Int(16)), + ) + elif not elementType.is_dynamic(): + expectedExpr = substring_for_decoding( + encoded, start_index=stride * index + pt.Int(2), length=stride + ) + else: + expectedExpr = substring_for_decoding( + encoded, + start_index=pt.ExtractUint16(encoded, stride * index + pt.Int(2)) + + pt.Int(2), + end_index=pt.If(index + pt.Int(1) == expectedLength) + .Then(pt.Len(encoded)) + .Else( + pt.ExtractUint16(encoded, stride * index + pt.Int(2) + pt.Int(2)) + + pt.Int(2) + ), + ) + + expected, _ = expectedExpr.__teal__(options) + expected.addIncoming() + expected = pt.TealBlock.NormalizeBlocks(expected) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + with pt.TealComponent.Context.ignoreScratchSlotEquality(): + assert actual == expected + + assert pt.TealBlock.MatchScratchSlotReferences( + pt.TealBlock.GetReferencedScratchSlots(actual), + pt.TealBlock.GetReferencedScratchSlots(expected), + ) + + with pytest.raises(pt.TealInputError): + element.store_into(abi.Tuple(abi.TupleTypeSpec(elementType))) From 54d427a25abcac65fcfae95054e9cbd1cad18e5b Mon Sep 17 00:00:00 2001 From: Hang Su Date: Wed, 4 Jan 2023 09:50:54 -0500 Subject: [PATCH 14/15] testcase for tuple elements --- pyteal/ast/abi/tuple_test.py | 211 ++++++++++++++++++++++++++++------- 1 file changed, 170 insertions(+), 41 deletions(-) diff --git a/pyteal/ast/abi/tuple_test.py b/pyteal/ast/abi/tuple_test.py index 9bfe71a05..09c7c4dde 100644 --- a/pyteal/ast/abi/tuple_test.py +++ b/pyteal/ast/abi/tuple_test.py @@ -227,7 +227,8 @@ def test_indexTuple(): class IndexTest(NamedTuple): types: list[abi.TypeSpec] typeIndex: int - expected: Callable[[abi.BaseType], pt.Expr] + expected_store: Callable[[abi.BaseType], pt.Expr] + expected_encode: pt.Expr # variables used to construct the tests uint64_t = abi.Uint64TypeSpec() @@ -243,99 +244,162 @@ class IndexTest(NamedTuple): IndexTest( types=[uint64_t], typeIndex=0, - expected=lambda output: output.decode(encoded), + expected_store=lambda output: output.decode(encoded), + expected_encode=encoded, ), IndexTest( types=[uint64_t, uint64_t], typeIndex=0, - expected=lambda output: output.decode(encoded, length=pt.Int(8)), + expected_store=lambda output: output.decode(encoded, length=pt.Int(8)), + expected_encode=substring_for_decoding(encoded, length=pt.Int(8)), ), IndexTest( types=[uint64_t, uint64_t], typeIndex=1, - expected=lambda output: output.decode(encoded, start_index=pt.Int(8)), + expected_store=lambda output: output.decode(encoded, start_index=pt.Int(8)), + expected_encode=substring_for_decoding(encoded, start_index=pt.Int(8)), ), IndexTest( types=[uint64_t, byte_t, uint64_t], typeIndex=1, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, start_index=pt.Int(8), length=pt.Int(1) + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.Int(8), length=pt.Int(1) ), ), IndexTest( types=[uint64_t, byte_t, uint64_t], typeIndex=2, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( encoded, start_index=pt.Int(9), length=pt.Int(8) ), + expected_encode=substring_for_decoding(encoded, start_index=pt.Int(9)), ), IndexTest( types=[bool_t], typeIndex=0, - expected=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(0)), + ), ), IndexTest( types=[bool_t, bool_t], typeIndex=0, - expected=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(0)), + ), ), IndexTest( types=[bool_t, bool_t], typeIndex=1, - expected=lambda output: output.decode_bit(encoded, pt.Int(1)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(1)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(1)), + ), ), IndexTest( types=[uint64_t, bool_t], typeIndex=1, - expected=lambda output: output.decode_bit(encoded, pt.Int(8 * 8)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(8 * 8)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(64)), + ), ), IndexTest( types=[uint64_t, bool_t, bool_t], typeIndex=1, - expected=lambda output: output.decode_bit(encoded, pt.Int(8 * 8)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(8 * 8)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(64)), + ), ), IndexTest( types=[uint64_t, bool_t, bool_t], typeIndex=2, - expected=lambda output: output.decode_bit(encoded, pt.Int(8 * 8 + 1)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(8 * 8 + 1)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(65)), + ), ), IndexTest( types=[bool_t, uint64_t], typeIndex=0, - expected=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(0)), + ), ), IndexTest( types=[bool_t, uint64_t], typeIndex=1, - expected=lambda output: output.decode(encoded, start_index=pt.Int(1)), + expected_store=lambda output: output.decode(encoded, start_index=pt.Int(1)), + expected_encode=substring_for_decoding(encoded, start_index=pt.Int(1)), ), IndexTest( types=[bool_t, bool_t, uint64_t], typeIndex=0, - expected=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(0)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(0)), + ), ), IndexTest( types=[bool_t, bool_t, uint64_t], typeIndex=1, - expected=lambda output: output.decode_bit(encoded, pt.Int(1)), + expected_store=lambda output: output.decode_bit(encoded, pt.Int(1)), + expected_encode=pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(1)), + ), ), IndexTest( types=[bool_t, bool_t, uint64_t], typeIndex=2, - expected=lambda output: output.decode(encoded, start_index=pt.Int(1)), + expected_store=lambda output: output.decode(encoded, start_index=pt.Int(1)), + expected_encode=substring_for_decoding(encoded, start_index=pt.Int(1)), ), IndexTest( - types=[tuple_t], typeIndex=0, expected=lambda output: output.decode(encoded) + types=[tuple_t], + typeIndex=0, + expected_store=lambda output: output.decode(encoded), + expected_encode=encoded, ), IndexTest( types=[byte_t, tuple_t], typeIndex=1, - expected=lambda output: output.decode(encoded, start_index=pt.Int(1)), + expected_store=lambda output: output.decode(encoded, start_index=pt.Int(1)), + expected_encode=substring_for_decoding(encoded, start_index=pt.Int(1)), ), IndexTest( types=[tuple_t, byte_t], typeIndex=0, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, + start_index=pt.Int(0), + length=pt.Int(tuple_t.byte_length_static()), + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.Int(0), length=pt.Int(tuple_t.byte_length_static()), @@ -344,7 +408,12 @@ class IndexTest(NamedTuple): IndexTest( types=[byte_t, tuple_t, byte_t], typeIndex=1, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, + start_index=pt.Int(1), + length=pt.Int(tuple_t.byte_length_static()), + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.Int(1), length=pt.Int(tuple_t.byte_length_static()), @@ -353,35 +422,52 @@ class IndexTest(NamedTuple): IndexTest( types=[dynamic_array_t1], typeIndex=0, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(0)) + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(0)) ), ), IndexTest( types=[byte_t, dynamic_array_t1], typeIndex=1, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)) + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)) ), ), IndexTest( types=[dynamic_array_t1, byte_t], typeIndex=0, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(0)) + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(0)) ), ), IndexTest( types=[byte_t, dynamic_array_t1, byte_t], typeIndex=1, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)) + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)) ), ), IndexTest( types=[byte_t, dynamic_array_t1, byte_t, dynamic_array_t2], typeIndex=1, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, + start_index=pt.ExtractUint16(encoded, pt.Int(1)), + end_index=pt.ExtractUint16(encoded, pt.Int(4)), + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)), end_index=pt.ExtractUint16(encoded, pt.Int(4)), @@ -390,14 +476,22 @@ class IndexTest(NamedTuple): IndexTest( types=[byte_t, dynamic_array_t1, byte_t, dynamic_array_t2], typeIndex=3, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(4)) + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(4)) ), ), IndexTest( types=[byte_t, dynamic_array_t1, tuple_t, dynamic_array_t2], typeIndex=1, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, + start_index=pt.ExtractUint16(encoded, pt.Int(1)), + end_index=pt.ExtractUint16(encoded, pt.Int(4)), + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)), end_index=pt.ExtractUint16(encoded, pt.Int(4)), @@ -406,14 +500,22 @@ class IndexTest(NamedTuple): IndexTest( types=[byte_t, dynamic_array_t1, tuple_t, dynamic_array_t2], typeIndex=3, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(4)) + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(4)) ), ), IndexTest( types=[byte_t, dynamic_array_t2, bool_t, bool_t, dynamic_array_t2], typeIndex=1, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( + encoded, + start_index=pt.ExtractUint16(encoded, pt.Int(1)), + end_index=pt.ExtractUint16(encoded, pt.Int(4)), + ), + expected_encode=substring_for_decoding( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)), end_index=pt.ExtractUint16(encoded, pt.Int(4)), @@ -422,28 +524,34 @@ class IndexTest(NamedTuple): IndexTest( types=[byte_t, dynamic_array_t1, bool_t, bool_t, dynamic_array_t2], typeIndex=4, - expected=lambda output: output.decode( + expected_store=lambda output: output.decode( encoded, start_index=pt.ExtractUint16(encoded, pt.Int(4)) ), + expected_encode=substring_for_decoding( + encoded, + start_index=pt.ExtractUint16(encoded, pt.Int(4)), + ), ), ] for i, test in enumerate(tests): output = test.types[test.typeIndex].new_instance() - expr = _IndexTuple(test.types, encoded).get_or_store(test.typeIndex, output) - assert expr.type_of() == pt.TealType.none - assert not expr.has_return() + expr_store = _IndexTuple(test.types, encoded).get_or_store( + test.typeIndex, output + ) + assert expr_store.type_of() == pt.TealType.none + assert not expr_store.has_return() - expected, _ = test.expected(output).__teal__(options) - expected.addIncoming() - expected = pt.TealBlock.NormalizeBlocks(expected) + expected_store, _ = test.expected_store(output).__teal__(options) + expected_store.addIncoming() + expected_store = pt.TealBlock.NormalizeBlocks(expected_store) - actual, _ = expr.__teal__(options) - actual.addIncoming() - actual = pt.TealBlock.NormalizeBlocks(actual) + actual_store, _ = expr_store.__teal__(options) + actual_store.addIncoming() + actual_store = pt.TealBlock.NormalizeBlocks(actual_store) with pt.TealComponent.Context.ignoreExprEquality(): - assert actual == expected, "Test at index {} failed".format(i) + assert actual_store == expected_store, f"Test at index {i} failed" with pytest.raises(ValueError): _IndexTuple(test.types, encoded).get_or_store(len(test.types), output) @@ -458,6 +566,27 @@ class IndexTest(NamedTuple): with pytest.raises(TypeError): _IndexTuple(test.types, encoded).get_or_store(test.typeIndex, otherType) + expr_encode = _IndexTuple(test.types, encoded).get_or_store(test.typeIndex) + assert expr_encode.type_of() == pt.TealType.bytes + assert not expr_encode.has_return() + + expected_encode, _ = test.expected_encode.__teal__(options) + expected_encode.addIncoming() + expected_encode = pt.TealBlock.NormalizeBlocks(expected_encode) + + actual_encode, _ = expr_encode.__teal__(options) + actual_encode.addIncoming() + actual_encode = pt.TealBlock.NormalizeBlocks(actual_encode) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual_encode == expected_encode, f"Test at index {i} failed" + + with pytest.raises(ValueError): + _IndexTuple(test.types, encoded).get_or_store(len(test.types)) + + with pytest.raises(ValueError): + _IndexTuple(test.types, encoded).get_or_store(-1) + def test_TupleTypeSpec_eq(): tupleA = abi.TupleTypeSpec( From d2fda147d86dcc7015bef57b68cd9477c82aa28a Mon Sep 17 00:00:00 2001 From: Hang Su Date: Wed, 4 Jan 2023 11:38:51 -0500 Subject: [PATCH 15/15] testcase for _GetAgainstEncoding --- pyteal/ast/abi/util_test.py | 147 +++++++++++++++++++++++++++++++++++- 1 file changed, 146 insertions(+), 1 deletion(-) diff --git a/pyteal/ast/abi/util_test.py b/pyteal/ast/abi/util_test.py index 2b9dfd73b..9190cc560 100644 --- a/pyteal/ast/abi/util_test.py +++ b/pyteal/ast/abi/util_test.py @@ -1,4 +1,5 @@ -from typing import Callable, NamedTuple, Literal, Optional, Any, get_origin +from dataclasses import dataclass, field +from typing import Callable, NamedTuple, Literal, Optional, Any, get_origin, cast from inspect import isabstract import pytest @@ -11,6 +12,7 @@ int_literal_from_annotation, type_spec_from_algosdk, type_spec_is_assignable_to, + _GetAgainstEncoding, ) options = pt.CompileOptions(version=5) @@ -1057,3 +1059,146 @@ def exists_in_unsafe_bidirectional(_ts: type): assert not exists_in_unsafe_bidirectional(ts) else: assert exists_in_unsafe_bidirectional(ts) + + +@dataclass +class GetAgainstEncodingTestcase: + type_spec: abi.TypeSpec + expected_store: Callable[[abi.BaseType, pt.Expr], pt.Expr] + expected_encode: Callable[[pt.Expr], pt.Expr] + start_index: Optional[pt.Int] = field(kw_only=True, default=None) + end_index: Optional[pt.Int] = field(kw_only=True, default=None) + length: Optional[pt.Int] = field(kw_only=True, default=None) + + +@pytest.mark.parametrize( + "testcase", + [ + GetAgainstEncodingTestcase( + type_spec=abi.Uint64TypeSpec(), + length=pt.Int(8), + expected_store=lambda output, encoded: output.decode( + encoded, length=pt.Int(8) + ), + expected_encode=lambda encoded: substring_for_decoding( + encoded, length=pt.Int(8) + ), + ), + GetAgainstEncodingTestcase( + type_spec=abi.Uint64TypeSpec(), + start_index=pt.Int(8), + expected_store=lambda output, encoded: output.decode( + encoded, start_index=pt.Int(8) + ), + expected_encode=lambda encoded: substring_for_decoding( + encoded, start_index=pt.Int(8) + ), + ), + GetAgainstEncodingTestcase( + type_spec=abi.ByteTypeSpec(), + start_index=pt.Int(8), + length=pt.Int(1), + expected_store=lambda output, encoded: output.decode( + encoded, start_index=pt.Int(8), length=pt.Int(1) + ), + expected_encode=lambda encoded: substring_for_decoding( + encoded, start_index=pt.Int(8), length=pt.Int(1) + ), + ), + GetAgainstEncodingTestcase( + type_spec=abi.BoolTypeSpec(), + start_index=pt.Int(0), + expected_store=lambda output, encoded: cast(abi.Bool, output).decode_bit( + encoded, pt.Int(0) + ), + expected_encode=lambda encoded: pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(0)), + ), + ), + GetAgainstEncodingTestcase( + type_spec=abi.BoolTypeSpec(), + start_index=pt.Int(1), + expected_store=lambda output, encoded: cast(abi.Bool, output).decode_bit( + encoded, pt.Int(1) + ), + expected_encode=lambda encoded: pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(1)), + ), + ), + GetAgainstEncodingTestcase( + type_spec=abi.BoolTypeSpec(), + start_index=pt.Int(64), + expected_store=lambda output, encoded: cast(abi.Bool, output).decode_bit( + encoded, pt.Int(64) + ), + expected_encode=lambda encoded: pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(64)), + ), + ), + GetAgainstEncodingTestcase( + type_spec=abi.BoolTypeSpec(), + start_index=pt.Int(65), + expected_store=lambda output, encoded: cast(abi.Bool, output).decode_bit( + encoded, pt.Int(8 * 8 + 1) + ), + expected_encode=lambda encoded: pt.SetBit( + pt.Bytes(b"\x00"), + pt.Int(0), + pt.GetBit(encoded, pt.Int(65)), + ), + ), + ], +) +def test_get_against_encoding(testcase: GetAgainstEncodingTestcase): + encoded = pt.Bytes("encoded") + + output_expr = testcase.type_spec.new_instance() + expected_store_expr = testcase.expected_store(output_expr, encoded) + expected_encode_expr = testcase.expected_encode(encoded) + + get_against_encoding = _GetAgainstEncoding( + encoded, + testcase.type_spec, + start_index=testcase.start_index, + end_index=testcase.end_index, + length=testcase.length, + ) + actual_store_expr = get_against_encoding.get_or_store(output_expr) + actual_encode_expr = get_against_encoding.get_or_store() + + def expected_actual_assert(expected: pt.Expr, actual: pt.Expr): + expected_blocks, _ = expected.__teal__(options) + expected_blocks.addIncoming() + expected_blocks = pt.TealBlock.NormalizeBlocks(expected_blocks) + + actual_blocks, _ = actual.__teal__(options) + actual_blocks.addIncoming() + actual_blocks = pt.TealBlock.NormalizeBlocks(actual_blocks) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual_blocks == expected_blocks + + expected_actual_assert(expected_store_expr, actual_store_expr) + expected_actual_assert(expected_encode_expr, actual_encode_expr) + + +def test_get_against_encoding_negative_cases(): + with pytest.raises(TypeError) as te: + _GetAgainstEncoding( + pt.Bytes("encoded"), + abi.BoolTypeSpec(), + ) + + assert "Expected a TealType.uint64 object" in str(te) + + with pytest.raises(pt.TealTypeError): + _GetAgainstEncoding( + pt.Int(123), + abi.BoolTypeSpec(), + )