diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..ba2dab8ca --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,45 @@ +name: "Build workflow" +on: + pull_request: + push: + branches: + - master +jobs: + build-test: + runs-on: ubuntu-20.04 + container: python:${{ matrix.python }}-slim + strategy: + matrix: + python: ['3.6', '3.7', '3.8', '3.9'] + steps: + - run: python3 --version + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install pip dependencies + run: pip install -r requirements.txt + - name: Build and Test + run: | + pytest + mypy pyteal + python3 -c "import pyteal" scripts/generate_init.py --check + black --check . + upload-to-pypi: + runs-on: ubuntu-20.04 + needs: ['build-test'] + if: ${{ github.event_name == 'push' && contains(github.ref, 'master') && startsWith(github.ref, 'refs/tags') }} + steps: + - name: Check out code + uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Install dependencies + run: pip install wheel + - name: Build package + run: python setup.py sdist bdist_wheel + - name: Release + uses: pypa/gh-action-pypi-publish@release/v1 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index b844389a3..000000000 --- a/.travis.yml +++ /dev/null @@ -1,34 +0,0 @@ -language: python - -install: - - pip install -r requirements.txt - -script: - - pytest - - mypy pyteal - - python3 -c "import pyteal" scripts/generate_init.py --check - - black --check . - -jobs: - include: - - stage: Testing - name: Python 3.6 - python: "3.6" - - name: Python 3.7 - python: "3.7" - - name: Python 3.8 - python: "3.8" - - name: Python 3.9 - python: "3.9" - - stage: Release - name: PyPi release - python: "3.9" - script: skip - if: tag IS present AND type = push - deploy: - provider: pypi - username: __token__ - on: - tags: true - branch: master - distributions: "sdist bdist_wheel" diff --git a/pyteal/__init__.pyi b/pyteal/__init__.pyi index 7e1c47318..72647d9a7 100644 --- a/pyteal/__init__.pyi +++ b/pyteal/__init__.pyi @@ -78,6 +78,7 @@ __all__ = [ "Div", "Mod", "Exp", + "Divw", "BitwiseAnd", "BitwiseOr", "BitwiseXor", @@ -121,6 +122,7 @@ __all__ = [ "ScratchStackStore", "ScratchVar", "MaybeValue", + "MultiValue", "BytesAdd", "BytesMinus", "BytesDiv", diff --git a/pyteal/ast/__init__.py b/pyteal/ast/__init__.py index 22cc173e6..15cbf21ef 100644 --- a/pyteal/ast/__init__.py +++ b/pyteal/ast/__init__.py @@ -90,7 +90,7 @@ ) # ternary ops -from .ternaryexpr import Ed25519Verify, SetBit, SetByte +from .ternaryexpr import Divw, Ed25519Verify, SetBit, SetByte from .substring import Substring, Extract, Suffix # more ops @@ -121,6 +121,10 @@ from .scratch import ScratchSlot, ScratchLoad, ScratchStore, ScratchStackStore from .scratchvar import ScratchVar from .maybe import MaybeValue +from .multi import MultiValue + +# abi +from . import abi # abi from . import abi @@ -187,6 +191,7 @@ "Div", "Mod", "Exp", + "Divw", "BitwiseAnd", "BitwiseOr", "BitwiseXor", @@ -230,6 +235,7 @@ "ScratchStackStore", "ScratchVar", "MaybeValue", + "MultiValue", "BytesAdd", "BytesMinus", "BytesDiv", diff --git a/pyteal/ast/abi/array.py b/pyteal/ast/abi/array.py index 54c6e52e4..703033b41 100644 --- a/pyteal/ast/abi/array.py +++ b/pyteal/ast/abi/array.py @@ -1,5 +1,4 @@ from typing import ( - Callable, Union, Sequence, TypeVar, @@ -9,20 +8,20 @@ ) from abc import abstractmethod -from ...types import TealType +from ...types import TealType, require_type from ...errors import TealInputError from ..expr import Expr from ..seq import Seq from ..int import Int from ..if_ import If +from ..unaryexpr import Len from ..binaryexpr import ExtractUint16 from ..naryexpr import Concat -from ..substring import Extract, Substring, Suffix from .type import Type, ComputedType, substringForDecoding from .tuple import encodeTuple from .bool import Bool, boolSequenceLength -from .uint import NUM_BITS_IN_BYTE, Uint16 +from .uint import Uint16 T = TypeVar("T", bound=Type) @@ -35,7 +34,7 @@ def __init__(self, valueType: T, staticLength: Optional[int]) -> None: self._has_offsets = valueType.is_dynamic() if self._has_offsets: - self._stride = 2 + self._stride = Uint16().byte_length_static() else: self._stride = valueType.byte_length_static() self._static_length = staticLength @@ -54,8 +53,13 @@ def decode( return self.stored_value.store(extracted) def set(self, values: Sequence[T]) -> Expr: - if not all(self._valueType.has_same_type_as(value) for value in values): - raise TealInputError("Input values do not match type") + for index, value in enumerate(values): + if not self._valueType.has_same_type_as(value): + raise TealInputError( + "Cannot assign type {} at index {} to {}".format( + value, index, self._valueType + ) + ) encoded = encodeTuple(values) @@ -74,18 +78,13 @@ def length(self) -> Expr: pass def __getitem__(self, index: Union[int, Expr]) -> "ArrayElement[T]": - return self._getItemWithLength(index, None) - - def _getItemWithLength( - self, index: Union[int, Expr], length: Expr = None - ) -> "ArrayElement[T]": if type(index) is int: if index < 0 or ( self._static_length is not None and index >= self._static_length ): - raise TealInputError("Index out of bounds") + raise TealInputError("Index out of bounds: {}".format(index)) index = Int(index) - return ArrayElement(self, cast(Expr, index), length) + return ArrayElement(self, cast(Expr, index)) def __str__(self) -> str: return self._valueType.__str__() + ( @@ -98,6 +97,10 @@ def __str__(self) -> str: # until something like https://github.com/python/mypy/issues/3345 is added, we can't make the size of the array a generic parameter class StaticArray(Array[T]): def __init__(self, valueType: T, length: int) -> None: + if length < 0: + raise TealInputError( + "Static array length cannot be negative. Got {}".format(length) + ) super().__init__(valueType, length) def has_same_type_as(self, other: Type) -> bool: @@ -120,7 +123,12 @@ def byte_length_static(self) -> int: return boolSequenceLength(self.length_static()) return self.length_static() * self._valueType.byte_length_static() - def set(self, values: Sequence[T]) -> Expr: + def set(self, values: Union[Sequence[T], "StaticArray[T]"]) -> Expr: + if isinstance(values, Type): + if not self.has_same_type_as(values): + raise TealInputError("Cannot assign type {} to {}".format(values, self)) + return self.stored_value.store(cast(StaticArray[T], values).encode()) + if self.length_static() != len(values): raise TealInputError( "Incorrect length for values. Expected {}, got {}".format( @@ -157,6 +165,13 @@ def is_dynamic(self) -> bool: def byte_length_static(self) -> int: raise ValueError("Type is dynamic") + def set(self, values: Union[Sequence[T], "DynamicArray[T]"]) -> Expr: + if isinstance(values, Type): + if not self.has_same_type_as(values): + raise TealInputError("Cannot assign type {} to {}".format(values, self)) + return self.stored_value.store(cast(DynamicArray[T], values).encode()) + return super().set(values) + def length(self) -> Expr: output = Uint16() return Seq( @@ -169,47 +184,48 @@ def length(self) -> Expr: class ArrayElement(ComputedType[T]): - def __init__(self, array: Array[T], index: Expr, length: Expr = None) -> None: + def __init__(self, array: Array[T], index: Expr) -> None: super().__init__(array._valueType) + require_type(index, TealType.uint64) self.array = array self.index = index - self.length = length def store_into(self, output: T) -> Expr: - offsetBit = self.index - offsetIndex = Int(self.array._stride) * self.index - if self.array._static_length is None: - offsetBit = offsetBit + Int(2 * NUM_BITS_IN_BYTE) - offsetIndex = offsetIndex + Int(2) - - if self.length is not None: - arrayLength = self.length - else: - arrayLength = self.array.length() - - encodedArray = self.array.encode() - if not self.array._valueType.has_same_type_as(output): raise TealInputError("Output type does not match value type") + encodedArray = self.array.encode() + if type(output) is Bool: - return cast(Bool, output).decodeBit(encodedArray, offsetBit) + bitIndex = self.index + if self.array.is_dynamic(): + bitIndex = bitIndex + Int(Uint16().bits()) + return cast(Bool, output).decodeBit(encodedArray, bitIndex) + + byteIndex = Int(self.array._stride) * self.index + if self.array._static_length is None: + byteIndex = byteIndex + Int(Uint16().byte_length_static()) + + arrayLength = self.array.length() + + if self.array._valueType.is_dynamic(): + valueStart = ExtractUint16(encodedArray, byteIndex) + nextValueStart = ExtractUint16( + encodedArray, byteIndex + Int(Uint16().byte_length_static()) + ) + if self.array._static_length is None: + valueStart = valueStart + Int(Uint16().byte_length_static()) + nextValueStart = nextValueStart + Int(Uint16().byte_length_static()) - if self.array._has_offsets: - valueStart = ExtractUint16(encodedArray, offsetIndex) valueEnd = ( If(self.index + Int(1) == arrayLength) - .Then(arrayLength * Int(self.array._stride)) - .Else(ExtractUint16(encodedArray, offsetIndex + Int(2))) + .Then(Len(encodedArray)) + .Else(nextValueStart) ) - if self.array._static_length is None: - valueStart = valueStart + Int(2) - valueEnd = valueEnd + Int(2) - return output.decode(encodedArray, startIndex=valueStart, endIndex=valueEnd) - valueStart = offsetIndex + valueStart = byteIndex valueLength = Int(self.array._stride) return output.decode(encodedArray, startIndex=valueStart, length=valueLength) diff --git a/pyteal/ast/abi/array_test.py b/pyteal/ast/abi/array_test.py index ebe1d2564..797103db9 100644 --- a/pyteal/ast/abi/array_test.py +++ b/pyteal/ast/abi/array_test.py @@ -56,6 +56,9 @@ def test_StaticArray_init(): assert staticArrayType._stride == elementType.byte_length_static() assert staticArrayType._static_length == length + with pytest.raises(TealInputError): + abi.StaticArray(elementType, -1) + for elementType in DYNAMIC_TYPES: for length in range(256): staticArrayType = abi.StaticArray(elementType, length) @@ -64,6 +67,9 @@ def test_StaticArray_init(): assert staticArrayType._stride == 2 assert staticArrayType._static_length == length + with pytest.raises(TealInputError): + abi.StaticArray(elementType, -1) + def test_StaticArray_str(): for elementType in STATIC_TYPES + DYNAMIC_TYPES: @@ -169,7 +175,7 @@ def test_StaticArray_decode(): assert actual == expected -def test_StaticArray_set(): +def test_StaticArray_set_values(): staticArrayType = abi.StaticArray(abi.Uint64(), 10) with pytest.raises(TealInputError): @@ -205,6 +211,38 @@ def test_StaticArray_set(): assert actual == expected +def test_StaticArray_set_copy(): + staticArrayType = abi.StaticArray(abi.Uint64(), 10) + otherArray = abi.StaticArray(abi.Uint64(), 10) + + with pytest.raises(TealInputError): + staticArrayType.set(abi.StaticArray(abi.Uint64(), 11)) + + with pytest.raises(TealInputError): + staticArrayType.set(abi.StaticArray(abi.Uint8(), 10)) + + with pytest.raises(TealInputError): + staticArrayType.set(abi.Uint64()) + + expr = staticArrayType.set(otherArray) + assert expr.type_of() == TealType.none + assert not expr.has_return() + + expected = TealSimpleBlock( + [ + TealOp(None, Op.load, otherArray.stored_value.slot), + TealOp(None, Op.store, staticArrayType.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + def test_StaticArray_encode(): staticArrayType = abi.StaticArray(abi.Uint64(), 10) expr = staticArrayType.encode() @@ -257,7 +295,6 @@ def test_StaticArray_getitem(): assert type(element) is ArrayElement assert element.array is staticArrayType assert element.index is indexExpr - assert element.length is None for index in range(length): # static indexes @@ -266,7 +303,6 @@ def test_StaticArray_getitem(): assert element.array is staticArrayType assert type(element.index) is Int assert element.index.value == index - assert element.length is None with pytest.raises(TealInputError): staticArrayType[-1] @@ -370,7 +406,7 @@ def test_DynamicArray_decode(): assert actual == expected -def test_DynamicArray_set(): +def test_DynamicArray_set_values(): dynamicArrayType = abi.DynamicArray(abi.Uint64()) valuesToSet: List[abi.Uint64] = [ @@ -409,6 +445,35 @@ def test_DynamicArray_set(): ) +def test_DynamicArray_set_copy(): + dynamicArrayType = abi.DynamicArray(abi.Uint64()) + otherArray = abi.DynamicArray(abi.Uint64()) + + with pytest.raises(TealInputError): + dynamicArrayType.set(abi.DynamicArray(abi.Uint8())) + + with pytest.raises(TealInputError): + dynamicArrayType.set(abi.Uint64()) + + expr = dynamicArrayType.set(otherArray) + assert expr.type_of() == TealType.none + assert not expr.has_return() + + expected = TealSimpleBlock( + [ + TealOp(None, Op.load, otherArray.stored_value.slot), + TealOp(None, Op.store, dynamicArrayType.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + def test_DynamicArray_encode(): dynamicArrayType = abi.DynamicArray(abi.Uint64()) expr = dynamicArrayType.encode() @@ -463,7 +528,6 @@ def test_DynamicArray_getitem(): assert type(element) is ArrayElement assert element.array is dynamicArrayType assert element.index is indexExpr - assert element.length is None for index in (0, 1, 2, 3, 1000): # static indexes @@ -472,104 +536,111 @@ def test_DynamicArray_getitem(): assert element.array is dynamicArrayType assert type(element.index) is Int assert element.index.value == index - assert element.length is None with pytest.raises(TealInputError): dynamicArrayType[-1] +def test_ArrayElement_init(): + array = abi.DynamicArray(abi.Uint64()) + index = Int(6) + + element = ArrayElement(array, index) + assert element.array is array + assert element.index is index + + with pytest.raises(TealTypeError): + ArrayElement(array, Bytes("abc")) + + with pytest.raises(TealTypeError): + ArrayElement(array, Assert(index)) + + def test_ArrayElement_store_into(): for elementType in STATIC_TYPES + DYNAMIC_TYPES: staticArrayType = abi.StaticArray(elementType, 100) index = Int(9) - for lengthExpr in (None, Int(500)): - element = ArrayElement(staticArrayType, index, lengthExpr) - output = elementType.new_instance() - expr = element.store_into(output) - - encoded = staticArrayType.encode() - stride = Int(staticArrayType._stride) - expectedLength = ( - lengthExpr if lengthExpr is not None else staticArrayType.length() + element = ArrayElement(staticArrayType, index) + output = elementType.new_instance() + expr = element.store_into(output) + + encoded = staticArrayType.encode() + stride = Int(staticArrayType._stride) + expectedLength = staticArrayType.length() + if type(elementType) is abi.Bool: + expectedExpr = cast(abi.Bool, output).decodeBit(encoded, index) + elif not elementType.is_dynamic(): + expectedExpr = output.decode( + encoded, startIndex=stride * index, length=stride + ) + else: + expectedExpr = output.decode( + encoded, + startIndex=ExtractUint16(encoded, stride * index), + endIndex=If(index + Int(1) == expectedLength) + .Then(Len(encoded)) + .Else(ExtractUint16(encoded, stride * index + Int(2))), ) - if type(elementType) is abi.Bool: - expectedExpr = cast(abi.Bool, output).decodeBit(encoded, index) - elif not elementType.is_dynamic(): - expectedExpr = output.decode( - encoded, startIndex=stride * index, length=stride - ) - else: - expectedExpr = output.decode( - encoded, - startIndex=ExtractUint16(encoded, stride * index), - endIndex=If(index + Int(1) == expectedLength) - .Then(expectedLength * stride) - .Else(ExtractUint16(encoded, stride * index + Int(2))), - ) - expected, _ = expectedExpr.__teal__(options) - expected.addIncoming() - expected = TealBlock.NormalizeBlocks(expected) + expected, _ = expectedExpr.__teal__(options) + expected.addIncoming() + expected = TealBlock.NormalizeBlocks(expected) - actual, _ = expr.__teal__(options) - actual.addIncoming() - actual = TealBlock.NormalizeBlocks(actual) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) - with TealComponent.Context.ignoreExprEquality(): - assert actual == expected + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected - with pytest.raises(TealInputError): - element.store_into(abi.Tuple(output)) + with pytest.raises(TealInputError): + element.store_into(abi.Tuple(output)) for elementType in STATIC_TYPES + DYNAMIC_TYPES: dynamicArrayType = abi.DynamicArray(elementType) index = Int(9) - for lengthExpr in (None, Int(500)): - element = ArrayElement(dynamicArrayType, index, lengthExpr) - output = elementType.new_instance() - expr = element.store_into(output) - - encoded = dynamicArrayType.encode() - stride = Int(dynamicArrayType._stride) - expectedLength = ( - lengthExpr if lengthExpr is not None else dynamicArrayType.length() + element = ArrayElement(dynamicArrayType, index) + output = elementType.new_instance() + expr = element.store_into(output) + + encoded = dynamicArrayType.encode() + stride = Int(dynamicArrayType._stride) + expectedLength = dynamicArrayType.length() + if type(elementType) is abi.Bool: + expectedExpr = cast(abi.Bool, output).decodeBit(encoded, index + Int(16)) + elif not elementType.is_dynamic(): + expectedExpr = output.decode( + encoded, startIndex=stride * index + Int(2), length=stride + ) + else: + expectedExpr = output.decode( + encoded, + startIndex=ExtractUint16(encoded, stride * index + Int(2)) + Int(2), + endIndex=If(index + Int(1) == expectedLength) + .Then(Len(encoded)) + .Else( + ExtractUint16(encoded, stride * index + Int(2) + Int(2)) + Int(2) + ), ) - if type(elementType) is abi.Bool: - expectedExpr = cast(abi.Bool, output).decodeBit( - encoded, index + Int(16) - ) - elif not elementType.is_dynamic(): - expectedExpr = output.decode( - encoded, startIndex=stride * index + Int(2), length=stride - ) - else: - expectedExpr = output.decode( - encoded, - startIndex=ExtractUint16(encoded, stride * index + Int(2)) + Int(2), - endIndex=If(index + Int(1) == expectedLength) - .Then(expectedLength * stride) - .Else(ExtractUint16(encoded, stride * index + Int(2) + Int(2))) - + Int(2), - ) - expected, _ = expectedExpr.__teal__(options) - expected.addIncoming() - expected = TealBlock.NormalizeBlocks(expected) + expected, _ = expectedExpr.__teal__(options) + expected.addIncoming() + expected = TealBlock.NormalizeBlocks(expected) - actual, _ = expr.__teal__(options) - actual.addIncoming() - actual = TealBlock.NormalizeBlocks(actual) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) - with TealComponent.Context.ignoreExprEquality(): - with TealComponent.Context.ignoreScratchSlotEquality(): - assert actual == expected + with TealComponent.Context.ignoreExprEquality(): + with TealComponent.Context.ignoreScratchSlotEquality(): + assert actual == expected - assert TealBlock.MatchScratchSlotReferences( - TealBlock.GetReferencedScratchSlots(actual), - TealBlock.GetReferencedScratchSlots(expected), - ) + assert TealBlock.MatchScratchSlotReferences( + TealBlock.GetReferencedScratchSlots(actual), + TealBlock.GetReferencedScratchSlots(expected), + ) - with pytest.raises(TealInputError): - element.store_into(abi.Tuple(output)) + with pytest.raises(TealInputError): + element.store_into(abi.Tuple(output)) diff --git a/pyteal/ast/abi/bool.py b/pyteal/ast/abi/bool.py index 69ffe15bb..8794e4370 100644 --- a/pyteal/ast/abi/bool.py +++ b/pyteal/ast/abi/bool.py @@ -2,6 +2,8 @@ from ...types import TealType from ..expr import Expr +from ..seq import Seq +from ..assert_ import Assert from ..int import Int from ..bytes import Bytes from ..binaryexpr import GetBit @@ -31,11 +33,22 @@ def get(self) -> Expr: return self.stored_value.load() def set(self, value: Union[bool, Expr, "Bool"]) -> Expr: + checked = False if type(value) is bool: value = Int(1 if value else 0) + checked = True + if type(value) is Bool: value = value.get() - return self.stored_value.store(cast(Expr, value)) + checked = True + + if checked: + return self.stored_value.store(cast(Expr, value)) + + return Seq( + self.stored_value.store(cast(Expr, value)), + Assert(self.stored_value.load() < Int(2)), + ) def decode( self, @@ -49,8 +62,8 @@ def decode( startIndex = Int(0) return self.decodeBit(encoded, startIndex * Int(NUM_BITS_IN_BYTE)) - def decodeBit(self, encoded, bit: Expr) -> Expr: - return self.set(GetBit(encoded, bit)) + def decodeBit(self, encoded, bitIndex: Expr) -> Expr: + return self.stored_value.store(GetBit(encoded, bitIndex)) def encode(self) -> Expr: return SetBit(Bytes(b"\x00"), Int(0), self.get()) @@ -67,7 +80,7 @@ def boolAwareStaticByteLength(types: Sequence[Type]) -> int: ignoreNext -= 1 continue if type(t) is Bool: - numBools = consecutiveBools(types, i) + numBools = consecutiveBoolNum(types, i) ignoreNext = numBools - 1 length += boolSequenceLength(numBools) continue @@ -75,7 +88,7 @@ def boolAwareStaticByteLength(types: Sequence[Type]) -> int: return length -def consecutiveBools(types: Sequence[Type], startIndex: int) -> int: +def consecutiveBoolNum(types: Sequence[Type], startIndex: int) -> int: numConsecutiveBools = 0 for t in types[startIndex:]: if type(t) is not Bool: diff --git a/pyteal/ast/abi/bool_test.py b/pyteal/ast/abi/bool_test.py index c992d4197..9883b2dad 100644 --- a/pyteal/ast/abi/bool_test.py +++ b/pyteal/ast/abi/bool_test.py @@ -4,7 +4,7 @@ from ... import * from .bool import ( boolAwareStaticByteLength, - consecutiveBools, + consecutiveBoolNum, boolSequenceLength, encodeBoolSequence, ) @@ -77,6 +77,10 @@ def test_Bool_set_expr(): TealOp(None, Op.int, 1), TealOp(None, Op.logic_or), TealOp(None, Op.store, boolType.stored_value.slot), + TealOp(None, Op.load, boolType.stored_value.slot), + TealOp(None, Op.int, 2), + TealOp(None, Op.lt), + TealOp(None, Op.assert_), ] ) @@ -125,10 +129,10 @@ def test_Bool_get(): def test_Bool_decode(): boolType = abi.Bool() + encoded = Bytes("encoded") for startIndex in (None, Int(1)): for endIndex in (None, Int(2)): for length in (None, Int(3)): - encoded = Bytes("encoded") expr = boolType.decode( encoded, startIndex=startIndex, endIndex=endIndex, length=length ) @@ -232,7 +236,7 @@ class ByteLengthTest(NamedTuple): assert actual == test.expectedLength, "Test at index {} failed".format(i) -def test_consecutiveBools(): +def test_consecutiveBoolNum(): class ConsecutiveTest(NamedTuple): types: List[abi.Type] start: int @@ -265,7 +269,7 @@ class ConsecutiveTest(NamedTuple): ] for i, test in enumerate(tests): - actual = consecutiveBools(test.types, test.start) + actual = consecutiveBoolNum(test.types, test.start) assert actual == test.expected, "Test at index {} failed".format(i) diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py index 35e5971d0..404b58cbb 100644 --- a/pyteal/ast/abi/tuple.py +++ b/pyteal/ast/abi/tuple.py @@ -19,7 +19,7 @@ from .type import Type, ComputedType, substringForDecoding from .bool import ( Bool, - consecutiveBools, + consecutiveBoolNum, boolSequenceLength, encodeBoolSequence, boolAwareStaticByteLength, @@ -39,7 +39,7 @@ def encodeTuple(values: Sequence[Type]) -> Expr: continue if type(elem) is Bool: - numBools = consecutiveBools(values, i) + numBools = consecutiveBoolNum(values, i) ignoreNext = numBools - 1 head_length_static += boolSequenceLength(numBools) heads.append( @@ -120,7 +120,7 @@ def indexTuple( if type(typeBefore) is Bool: lastBoolStart = offset - lastBoolLength = consecutiveBools(valueTypes, i) + lastBoolLength = consecutiveBoolNum(valueTypes, i) offset += boolSequenceLength(lastBoolLength) ignoreNext = lastBoolLength - 1 continue @@ -155,9 +155,10 @@ def indexTuple( continue if type(typeAfter) is Bool: - boolLength = consecutiveBools(valueTypes, i) + boolLength = consecutiveBoolNum(valueTypes, i) nextDynamicValueOffset += boolSequenceLength(boolLength) ignoreNext = boolLength - 1 + continue if typeAfter.is_dynamic(): hasNextDynamicValue = True diff --git a/pyteal/ast/abi/tuple_test.py b/pyteal/ast/abi/tuple_test.py index 0d44ace36..5af924b8c 100644 --- a/pyteal/ast/abi/tuple_test.py +++ b/pyteal/ast/abi/tuple_test.py @@ -292,6 +292,31 @@ class IndexTest(NamedTuple): typeIndex=2, expected=lambda output: output.decodeBit(encoded, Int(8 * 8 + 1)), ), + IndexTest( + types=[bool_a, uint64_a], + typeIndex=0, + expected=lambda output: output.decodeBit(encoded, Int(0)), + ), + IndexTest( + types=[bool_a, uint64_a], + typeIndex=1, + expected=lambda output: output.decode(encoded, startIndex=Int(1)), + ), + IndexTest( + types=[bool_a, bool_b, uint64_a], + typeIndex=0, + expected=lambda output: output.decodeBit(encoded, Int(0)), + ), + IndexTest( + types=[bool_a, bool_b, uint64_a], + typeIndex=1, + expected=lambda output: output.decodeBit(encoded, Int(1)), + ), + IndexTest( + types=[bool_a, bool_b, uint64_a], + typeIndex=2, + expected=lambda output: output.decode(encoded, startIndex=Int(1)), + ), IndexTest( types=[tuple_a], typeIndex=0, expected=lambda output: output.decode(encoded) ), @@ -374,6 +399,22 @@ class IndexTest(NamedTuple): encoded, startIndex=ExtractUint16(encoded, Int(4)) ), ), + IndexTest( + types=[byte_a, dynamic_array_a, bool_a, bool_b, dynamic_array_b], + typeIndex=1, + expected=lambda output: output.decode( + encoded, + startIndex=ExtractUint16(encoded, Int(1)), + endIndex=ExtractUint16(encoded, Int(4)), + ), + ), + IndexTest( + types=[byte_a, dynamic_array_a, bool_a, bool_b, dynamic_array_b], + typeIndex=4, + expected=lambda output: output.decode( + encoded, startIndex=ExtractUint16(encoded, Int(4)) + ), + ), ] for i, test in enumerate(tests): diff --git a/pyteal/ast/abi/type.py b/pyteal/ast/abi/type.py index b90275194..842bd8c77 100644 --- a/pyteal/ast/abi/type.py +++ b/pyteal/ast/abi/type.py @@ -13,28 +13,66 @@ class Type(ABC): + """The abstract base class for all ABI types. + + This class contains both information about an ABI type, and a value that conforms to that type. + The value is contained in a unique ScratchVar that only the type has access to. As a result, the + value of an ABI type is mutable and can be efficiently referenced multiple times without needing + to recompute it. + """ + def __init__(self, valueType: TealType) -> None: + """Create a new Type. + + Args: + valueType: The TealType (uint64 or bytes) that this ABI type will store in its internal + ScratchVar. + """ super().__init__() self.stored_value = ScratchVar(valueType) @abstractmethod def has_same_type_as(self, other: "Type") -> bool: + """Check if this type is considered equal to the other ABI type, irrespective of their + values. + + Args: + other: The ABI type to compare to. + + Returns: + True if and only if self and other can store the same ABI value. + """ pass @abstractmethod def new_instance(self: T) -> T: + """Create a new instance of this ABI type. + + The value of this type will not be applied to the new type. + """ pass @abstractmethod def is_dynamic(self) -> bool: + """Check if this ABI type is dynamic. + + If a type is dynamic, the length of its encoding depends on its value. Otherwise, the type + is considered static (not dynamic). + """ pass @abstractmethod def byte_length_static(self) -> int: + """Get the byte length of this ABI type's encoding. Only valid for static types.""" pass @abstractmethod def encode(self) -> Expr: + """Encode this ABI type to a byte string. + + Returns: + A PyTeal expression that encodes this type to a byte string. + """ pass @abstractmethod @@ -81,6 +119,7 @@ def decode( @abstractmethod def __str__(self) -> str: + """Get the string representation of this ABI type, used for creating method signatures.""" pass diff --git a/pyteal/ast/app.py b/pyteal/ast/app.py index b8c49ab45..23e265094 100644 --- a/pyteal/ast/app.py +++ b/pyteal/ast/app.py @@ -93,7 +93,7 @@ def optedIn(cls, account: Expr, app: Expr) -> "App": Txn.Accounts or is Txn.Sender, must be evaluated to bytes). app: An index into Txn.ForeignApps that corresponds to the application to read from, must be evaluated to uint64 (or, since v4, an application id that appears in - Txn.ForeignApps or is the CurrentApplicationID, must be evaluated to bytes). + Txn.ForeignApps or is the CurrentApplicationID, must be evaluated to int). """ require_type(account, TealType.anytype) require_type(app, TealType.uint64) @@ -123,7 +123,7 @@ def localGetEx(cls, account: Expr, app: Expr, key: Expr) -> MaybeValue: Txn.Accounts or is Txn.Sender, must be evaluated to bytes). app: An index into Txn.ForeignApps that corresponds to the application to read from, must be evaluated to uint64 (or, since v4, an application id that appears in - Txn.ForeignApps or is the CurrentApplicationID, must be evaluated to bytes). + Txn.ForeignApps or is the CurrentApplicationID, must be evaluated to int). key: The key to read from the account's local state. Must evaluate to bytes. """ require_type(account, TealType.anytype) diff --git a/pyteal/ast/gitxn.py b/pyteal/ast/gitxn.py index 168899945..4108d86ff 100644 --- a/pyteal/ast/gitxn.py +++ b/pyteal/ast/gitxn.py @@ -1,10 +1,12 @@ -from typing import TYPE_CHECKING, cast +from typing import TYPE_CHECKING, cast, Union from pyteal.config import MAX_GROUP_SIZE from ..errors import TealInputError, verifyFieldVersion, verifyTealVersion from ..ir import TealOp, Op, TealBlock +from .expr import Expr from .txn import TxnExpr, TxnField, TxnObject, TxnaExpr +from ..types import require_type, TealType if TYPE_CHECKING: from ..compiler import CompileOptions @@ -15,6 +17,15 @@ class GitxnExpr(TxnExpr): def __init__(self, txnIndex: int, field: TxnField) -> None: super().__init__(Op.gitxn, "Gitxn", field) + + # Currently we do not have gitxns. Only gitxn with immediate transaction index supported. + if type(txnIndex) is not int: + raise TealInputError( + "Invalid gitxn syntax with immediate transaction field {} and transaction index {}".format( + field, txnIndex + ) + ) + self.txnIndex = txnIndex def __str__(self): @@ -23,14 +34,6 @@ def __str__(self): def __teal__(self, options: "CompileOptions"): verifyFieldVersion(self.field.arg_name, self.field.min_version, options.version) - # currently we do not have gitxns, only gitxn with immediate transaction index supported - if type(self.txnIndex) is not int: - raise TealInputError( - "Invalid gitxn syntax with immediate transaction field {} and transaction index {}".format( - self.field, self.txnIndex - ) - ) - verifyTealVersion( Op.gitxn.min_version, options.version, @@ -46,8 +49,14 @@ def __teal__(self, options: "CompileOptions"): class GitxnaExpr(TxnaExpr): """An expression that accesses an inner transaction array field from an inner transaction in the last inner group.""" - def __init__(self, txnIndex: int, field: TxnField, index: int) -> None: - super().__init__(Op.gitxna, None, "Gitxna", field, index) + def __init__(self, txnIndex: int, field: TxnField, index: Union[int, Expr]) -> None: + super().__init__(Op.gitxna, Op.gitxnas, "Gitxna", field, index) + + if type(txnIndex) is not int: + raise TealInputError( + f"Invalid txnIndex type: Expected int, but received {txnIndex}." + ) + self.txnIndex = txnIndex def __str__(self): @@ -57,18 +66,23 @@ def __str__(self): def __teal__(self, options: "CompileOptions"): verifyFieldVersion(self.field.arg_name, self.field.min_version, options.version) - if type(self.txnIndex) is not int or type(self.index) is not int: - raise TealInputError( - "Invalid gitxna syntax with immediate transaction index {}, transaction field {}, array index {}".format( - self.txnIndex, self.field, self.index - ) - ) + + if type(self.index) is int: + opToUse = Op.gitxna + else: + opToUse = Op.gitxnas verifyTealVersion( - Op.gitxna.min_version, options.version, "TEAL version too low to use gitxna" + opToUse.min_version, + options.version, + "TEAL version too low to use op {}".format(opToUse), ) - op = TealOp(self, Op.gitxna, self.txnIndex, self.field.arg_name, self.index) - return TealBlock.FromOp(options, op) + + if type(self.index) is int: + op = TealOp(self, opToUse, self.txnIndex, self.field.arg_name, self.index) + return TealBlock.FromOp(options, op) + op = TealOp(self, opToUse, self.txnIndex, self.field.arg_name) + return TealBlock.FromOp(options, op, cast(Expr, self.index)) GitxnaExpr.__module__ = "pyteal" @@ -85,14 +99,14 @@ def __getitem__(self, txnIndex: int) -> TxnObject: if txnIndex < 0 or txnIndex >= MAX_GROUP_SIZE: raise TealInputError( - "Invalid Gtxn index {}, shoud be in [0, {})".format( + "Invalid Gtxn index {}, should be in [0, {})".format( txnIndex, MAX_GROUP_SIZE ) ) return TxnObject( lambda field: GitxnExpr(txnIndex, field), - lambda field, index: GitxnaExpr(txnIndex, field, cast(int, index)), + lambda field, index: GitxnaExpr(txnIndex, field, index), ) diff --git a/pyteal/ast/gitxn_test.py b/pyteal/ast/gitxn_test.py index 677cfc09d..ab950067b 100644 --- a/pyteal/ast/gitxn_test.py +++ b/pyteal/ast/gitxn_test.py @@ -7,24 +7,75 @@ def test_gitxn_invalid(): - with pytest.raises(TealInputError): - GitxnExpr(0, TxnField.sender).__teal__(teal5Options) + for ctor, e in [ + ( + lambda: Gitxn[MAX_GROUP_SIZE], + TealInputError, + ), + ( + lambda: Gitxn[-1], + TealInputError, + ), + ]: + with pytest.raises(e): + ctor() - with pytest.raises(TealInputError): - Gitxn[MAX_GROUP_SIZE].sender() - with pytest.raises(TealInputError): - Gitxn[-1].asset_sender() +def test_gitxn_valid(): + for i in range(MAX_GROUP_SIZE): + Gitxn[i].sender() - with pytest.raises(TealInputError): - Gitxn[Bytes("first")].sender() +def test_gitxn_expr_invalid(): + for f, e in [ + ( + lambda: GitxnExpr(Int(1), TxnField.sender), + TealInputError, + ), + ( + lambda: GitxnExpr(1, TxnField.sender).__teal__(teal5Options), + TealInputError, + ), + ]: + with pytest.raises(e): + f() -def test_gitxn_valid(): - GitxnaExpr(0, TxnField.application_args, 1).__teal__(teal6Options) - for i in range(MAX_GROUP_SIZE): - Gitxn[i].sender() +def test_gitxn_expr_valid(): + GitxnExpr(1, TxnField.sender).__teal__(teal6Options) + + +def test_gitxna_expr_invalid(): + for f, e in [ + ( + lambda: GitxnaExpr("Invalid_type", TxnField.application_args, 1), + TealInputError, + ), + ( + lambda: GitxnaExpr(1, TxnField.application_args, "Invalid_type"), + TealInputError, + ), + ( + lambda: GitxnaExpr(0, TxnField.application_args, Assert(Int(1))), + TealTypeError, + ), + ( + lambda: GitxnaExpr(0, TxnField.application_args, 0).__teal__(teal5Options), + TealInputError, + ), + ]: + with pytest.raises(e): + f() + + +def test_gitxna_valid(): + [ + e.__teal__(teal6Options) + for e in [ + GitxnaExpr(0, TxnField.application_args, 1), + GitxnaExpr(0, TxnField.application_args, Int(1)), + ] + ] # txn_test.py performs additional testing diff --git a/pyteal/ast/gtxn.py b/pyteal/ast/gtxn.py index c4b0c14f7..b12f8f9d5 100644 --- a/pyteal/ast/gtxn.py +++ b/pyteal/ast/gtxn.py @@ -11,11 +11,21 @@ from ..compiler import CompileOptions +def validate_txn_index_or_throw(txnIndex: Union[int, Expr]): + if not isinstance(txnIndex, (int, Expr)): + raise TealInputError( + f"Invalid txnIndex type: Expected int or Expr, but received {txnIndex}" + ) + if isinstance(txnIndex, Expr): + require_type(txnIndex, TealType.uint64) + + class GtxnExpr(TxnExpr): """An expression that accesses a transaction field from a transaction in the current group.""" def __init__(self, txnIndex: Union[int, Expr], field: TxnField) -> None: super().__init__(Op.gtxn, "Gtxn", field) + validate_txn_index_or_throw(txnIndex) self.txnIndex = txnIndex def __str__(self): @@ -51,6 +61,7 @@ def __init__( self, txnIndex: Union[int, Expr], field: TxnField, index: Union[int, Expr] ) -> None: super().__init__(Op.gtxna, Op.gtxnas, "Gtxna", field, index) + validate_txn_index_or_throw(txnIndex) self.txnIndex = txnIndex def __str__(self): diff --git a/pyteal/ast/gtxn_test.py b/pyteal/ast/gtxn_test.py index 168090918..9b895d100 100644 --- a/pyteal/ast/gtxn_test.py +++ b/pyteal/ast/gtxn_test.py @@ -2,19 +2,57 @@ from .. import * +teal6Options = CompileOptions(version=6) -def test_gtxn_invalid(): - with pytest.raises(TealInputError): - Gtxn[-1].fee() - - with pytest.raises(TealInputError): - Gtxn[MAX_GROUP_SIZE + 1].sender() - with pytest.raises(TealTypeError): - Gtxn[Pop(Int(0))].sender() - - with pytest.raises(TealTypeError): - Gtxn[Bytes("index")].sender() +def test_gtxn_invalid(): + for f, e in [ + (lambda: Gtxn[-1], TealInputError), + (lambda: Gtxn[MAX_GROUP_SIZE + 1], TealInputError), + (lambda: Gtxn[Pop(Int(0))], TealTypeError), + (lambda: Gtxn[Bytes("index")], TealTypeError), + ]: + with pytest.raises(e): + f() + + +def test_gtxn_expr_invalid(): + for f, e in [ + (lambda: GtxnExpr(Assert(Int(1)), TxnField.sender), TealTypeError), + ]: + with pytest.raises(e): + f() + + +def test_gtxn_expr_valid(): + [ + e.__teal__(teal6Options) + for e in [ + GtxnExpr(1, TxnField.sender), + GtxnExpr(Int(1), TxnField.sender), + ] + ] + + +def test_gtxna_expr_invalid(): + for f, e in [ + (lambda: GtxnaExpr("Invalid_type", TxnField.assets, 1), TealInputError), + (lambda: GtxnaExpr(1, TxnField.assets, "Invalid_type"), TealInputError), + (lambda: GtxnaExpr(Assert(Int(1)), TxnField.assets, 1), TealTypeError), + (lambda: GtxnaExpr(1, TxnField.assets, Assert(Int(1))), TealTypeError), + ]: + with pytest.raises(e): + f() + + +def test_gtxna_expr_valid(): + [ + e.__teal__(teal6Options) + for e in [ + GtxnaExpr(1, TxnField.assets, 1), + GtxnaExpr(Int(1), TxnField.assets, Int(1)), + ] + ] # txn_test.py performs additional testing diff --git a/pyteal/ast/itxn.py b/pyteal/ast/itxn.py index 6736c7113..ff857c808 100644 --- a/pyteal/ast/itxn.py +++ b/pyteal/ast/itxn.py @@ -120,7 +120,7 @@ def Submit(cls) -> Expr: :any:`InnerTxnBuilder.Begin` and :any:`InnerTxnBuilder.SetField` must be called before submitting an inner transaction. - This will fail fail if 16 inner transactions have already been executed, or if the + This will fail if 256 inner transactions have already been executed, or if the inner transaction itself fails. Upon failure, the current program will immediately exit and fail as well. @@ -204,7 +204,8 @@ def SetFields(cls, fields: Dict[TxnField, Union[Expr, List[Expr]]]) -> Expr: InnerTxnBuilder.__module__ = "pyteal" InnerTxn: TxnObject = TxnObject( - TxnExprBuilder(Op.itxn, "InnerTxn"), TxnaExprBuilder(Op.itxna, None, "InnerTxna") + TxnExprBuilder(Op.itxn, "InnerTxn"), + TxnaExprBuilder(Op.itxna, Op.itxnas, "InnerTxna"), ) InnerTxn.__module__ = "pyteal" diff --git a/pyteal/ast/maybe.py b/pyteal/ast/maybe.py index ff629a885..e394b4682 100644 --- a/pyteal/ast/maybe.py +++ b/pyteal/ast/maybe.py @@ -1,16 +1,17 @@ from typing import List, Union, TYPE_CHECKING +from pyteal.ast.multi import MultiValue + from ..types import TealType -from ..ir import TealOp, Op, TealBlock +from ..ir import Op from .expr import Expr -from .leafexpr import LeafExpr -from .scratch import ScratchSlot, ScratchLoad +from .scratch import ScratchLoad, ScratchSlot if TYPE_CHECKING: from ..compiler import CompileOptions -class MaybeValue(LeafExpr): +class MaybeValue(MultiValue): """Represents a get operation returning a value that may not exist.""" def __init__( @@ -29,20 +30,15 @@ def __init__( immediate_args (optional): Immediate arguments for the op. Defaults to None. args (optional): Stack arguments for the op. Defaults to None. """ - super().__init__() - self.op = op - self.type = type - self.immediate_args = immediate_args if immediate_args is not None else [] - self.args = args if args is not None else [] - self.slotOk = ScratchSlot() - self.slotValue = ScratchSlot() + types = [type, TealType.uint64] + super().__init__(op, types, immediate_args=immediate_args, args=args) def hasValue(self) -> ScratchLoad: """Check if the value exists. This will return 1 if the value exists, otherwise 0. """ - return self.slotOk.load(TealType.uint64) + return self.output_slots[1].load(self.types[1]) def value(self) -> ScratchLoad: """Get the value. @@ -50,41 +46,26 @@ def value(self) -> ScratchLoad: If the value exists, it will be returned. Otherwise, the zero value for this type will be returned (i.e. either 0 or an empty byte string, depending on the type). """ - return self.slotValue.load(self.type) - - def __str__(self): - ret_str = "(({}".format(self.op) - for a in self.immediate_args: - ret_str += " " + a.__str__() - - for a in self.args: - ret_str += " " + a.__str__() - ret_str += ") " - - storeOk = self.slotOk.store() - storeValue = self.slotValue.store() - - ret_str += storeOk.__str__() + " " + storeValue.__str__() + ")" + return self.output_slots[0].load(self.types[0]) - return ret_str + @property + def slotOk(self) -> ScratchSlot: + """Get the scratch slot that stores hasValue. - def __teal__(self, options: "CompileOptions"): - tealOp = TealOp(self, self.op, *self.immediate_args) - callStart, callEnd = TealBlock.FromOp(options, tealOp, *self.args) - - storeOk = self.slotOk.store() - storeValue = self.slotValue.store() - - storeOkStart, storeOkEnd = storeOk.__teal__(options) - storeValueStart, storeValueEnd = storeValue.__teal__(options) - - callEnd.setNextBlock(storeOkStart) - storeOkEnd.setNextBlock(storeValueStart) + Note: This is mainly added for backwards compatability and normally shouldn't be used + directly in pyteal code. + """ + return self.output_slots[1] - return callStart, storeValueEnd + @property + def slotValue(self) -> ScratchSlot: + """Get the scratch slot that stores the value or the zero value for the type if the value + doesn't exist. - def type_of(self): - return TealType.none + Note: This is mainly added for backwards compatability and normally shouldn't be used + directly in pyteal code. + """ + return self.output_slots[0] MaybeValue.__module__ = "pyteal" diff --git a/pyteal/ast/maybe_test.py b/pyteal/ast/maybe_test.py index d9e4de00c..53daf7977 100644 --- a/pyteal/ast/maybe_test.py +++ b/pyteal/ast/maybe_test.py @@ -26,6 +26,7 @@ def test_maybe_value(): expr = MaybeValue(op, type, immediate_args=iargs, args=args) assert expr.slotOk != expr.slotValue + assert expr.output_slots == [expr.slotValue, expr.slotOk] assert expr.hasValue().type_of() == TealType.uint64 with TealComponent.Context.ignoreExprEquality(): diff --git a/pyteal/ast/multi.py b/pyteal/ast/multi.py new file mode 100644 index 000000000..de8aef39d --- /dev/null +++ b/pyteal/ast/multi.py @@ -0,0 +1,79 @@ +from typing import Callable, List, Union, TYPE_CHECKING + +from ..types import TealType +from ..ir import TealOp, Op, TealBlock +from .expr import Expr +from .leafexpr import LeafExpr +from .scratch import ScratchSlot +from .seq import Seq + +if TYPE_CHECKING: + from ..compiler import CompileOptions + + +class MultiValue(LeafExpr): + """Represents an operation that returns more than one value""" + + def __init__( + self, + op: Op, + types: List[TealType], + *, + immediate_args: List[Union[int, str]] = None, + args: List[Expr] = None + ): + """Create a new MultiValue. + + Args: + op: The operation that returns values. + types: The types of the returned values. + immediate_args (optional): Immediate arguments for the op. Defaults to None. + args (optional): Stack arguments for the op. Defaults to None. + """ + super().__init__() + self.op = op + self.types = types + self.immediate_args = immediate_args if immediate_args is not None else [] + self.args = args if args is not None else [] + + self.output_slots = [ScratchSlot() for _ in self.types] + + def outputReducer(self, reducer: Callable[..., Expr]) -> Expr: + input = [slot.load(self.types[i]) for i, slot in enumerate(self.output_slots)] + return Seq(self, reducer(*input)) + + def __str__(self): + ret_str = "(({}".format(self.op) + for a in self.immediate_args: + ret_str += " " + a.__str__() + + for a in self.args: + ret_str += " " + a.__str__() + ret_str += ") " + + ret_str += " ".join([slot.store().__str__() for slot in self.output_slots]) + ret_str += ")" + + return ret_str + + def __teal__(self, options: "CompileOptions"): + tealOp = TealOp(self, self.op, *self.immediate_args) + callStart, callEnd = TealBlock.FromOp(options, tealOp, *self.args) + + curEnd = callEnd + # the list is reversed in order to preserve the ordering of the opcode's returned + # values. ie the output to stack [A, B, C] should correspond to C->output_slots[2] + # B->output_slots[1], and A->output_slots[0]. + for slot in reversed(self.output_slots): + store = slot.store() + storeStart, storeEnd = store.__teal__(options) + curEnd.setNextBlock(storeStart) + curEnd = storeEnd + + return callStart, curEnd + + def type_of(self): + return TealType.none + + +MultiValue.__module__ = "pyteal" diff --git a/pyteal/ast/multi_test.py b/pyteal/ast/multi_test.py new file mode 100644 index 000000000..665d7517b --- /dev/null +++ b/pyteal/ast/multi_test.py @@ -0,0 +1,191 @@ +import pytest + +from .. import * +from typing import List + +# this is not necessary but mypy complains if it's not included +from .. import CompileOptions + +options = CompileOptions() + + +def __test_single(expr: MultiValue): + assert expr.output_slots[0] != expr.output_slots[1] + + with TealComponent.Context.ignoreExprEquality(): + assert expr.output_slots[0].load().__teal__(options) == ScratchLoad( + expr.output_slots[0] + ).__teal__(options) + + with TealComponent.Context.ignoreExprEquality(): + assert expr.output_slots[1].load().__teal__(options) == ScratchLoad( + expr.output_slots[1] + ).__teal__(options) + + assert expr.type_of() == TealType.none + + +def __test_single_conditional(expr: MultiValue, op, args: List[Expr], iargs, reducer): + __test_single(expr) + + expected_call = TealSimpleBlock( + [ + TealOp(expr, op, *iargs), + TealOp(expr.output_slots[1].store(), Op.store, expr.output_slots[1]), + TealOp(expr.output_slots[0].store(), Op.store, expr.output_slots[0]), + ] + ) + + ifExpr = ( + If(expr.output_slots[1].load()) + .Then(expr.output_slots[0].load()) + .Else(Bytes("None")) + ) + ifBlockStart, _ = ifExpr.__teal__(options) + + expected_call.setNextBlock(ifBlockStart) + + if len(args) == 0: + expected: TealBlock = expected_call + elif len(args) == 1: + expected, after_arg = args[0].__teal__(options) + after_arg.setNextBlock(expected_call) + elif len(args) == 2: + expected, after_arg_1 = args[0].__teal__(options) + arg_2, after_arg_2 = args[1].__teal__(options) + after_arg_1.setNextBlock(arg_2) + after_arg_2.setNextBlock(expected_call) + + expected.addIncoming() + expected = TealBlock.NormalizeBlocks(expected) + + actual, _ = expr.outputReducer(reducer).__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def __test_single_assert(expr: MultiValue, op, args: List[Expr], iargs, reducer): + __test_single(expr) + + expected_call = TealSimpleBlock( + [ + TealOp(expr, op, *iargs), + TealOp(expr.output_slots[1].store(), Op.store, expr.output_slots[1]), + TealOp(expr.output_slots[0].store(), Op.store, expr.output_slots[0]), + ] + ) + + assertExpr = Seq(Assert(expr.output_slots[1].load()), expr.output_slots[0].load()) + assertBlockStart, _ = assertExpr.__teal__(options) + + expected_call.setNextBlock(assertBlockStart) + + if len(args) == 0: + expected: TealBlock = expected_call + elif len(args) == 1: + expected, after_arg = args[0].__teal__(options) + after_arg.setNextBlock(expected_call) + elif len(args) == 2: + expected, after_arg_1 = args[0].__teal__(options) + arg_2, after_arg_2 = args[1].__teal__(options) + after_arg_1.setNextBlock(arg_2) + after_arg_2.setNextBlock(expected_call) + + expected.addIncoming() + expected = TealBlock.NormalizeBlocks(expected) + + actual, _ = expr.outputReducer(reducer).__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def __test_single_with_vars( + expr: MultiValue, op, args: List[Expr], iargs, var1, var2, reducer +): + __test_single(expr) + + expected_call = TealSimpleBlock( + [ + TealOp(expr, op, *iargs), + TealOp(expr.output_slots[1].store(), Op.store, expr.output_slots[1]), + TealOp(expr.output_slots[0].store(), Op.store, expr.output_slots[0]), + ] + ) + + varExpr = Seq( + var1.store(expr.output_slots[1].load()), var2.store(expr.output_slots[0].load()) + ) + varBlockStart, _ = varExpr.__teal__(options) + + expected_call.setNextBlock(varBlockStart) + + if len(args) == 0: + expected: TealBlock = expected_call + elif len(args) == 1: + expected, after_arg = args[0].__teal__(options) + after_arg.setNextBlock(expected_call) + elif len(args) == 2: + expected, after_arg_1 = args[0].__teal__(options) + arg_2, after_arg_2 = args[1].__teal__(options) + after_arg_1.setNextBlock(arg_2) + after_arg_2.setNextBlock(expected_call) + + expected.addIncoming() + expected = TealBlock.NormalizeBlocks(expected) + + actual, _ = expr.outputReducer(reducer).__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_multi_value(): + ops = ( + Op.app_global_get_ex, + Op.app_local_get_ex, + Op.asset_holding_get, + Op.asset_params_get, + ) + types = (TealType.uint64, TealType.bytes, TealType.anytype) + immedate_argv = ([], ["AssetFrozen"]) + argv = ([], [Int(0)], [Int(1), Int(2)]) + + for op in ops: + for type in types: + for iargs in immedate_argv: + for args in argv: + reducer = ( + lambda value, hasValue: If(hasValue) + .Then(value) + .Else(Bytes("None")) + ) + expr = MultiValue( + op, [type, TealType.uint64], immediate_args=iargs, args=args + ) + __test_single_conditional(expr, op, args, iargs, reducer) + + reducer = lambda value, hasValue: Seq(Assert(hasValue), value) + expr = MultiValue( + op, [type, TealType.uint64], immediate_args=iargs, args=args + ) + __test_single_assert(expr, op, args, iargs, reducer) + + hasValueVar = ScratchVar(TealType.uint64) + valueVar = ScratchVar(type) + reducer = lambda value, hasValue: Seq( + hasValueVar.store(hasValue), valueVar.store(value) + ) + expr = MultiValue( + op, [type, TealType.uint64], immediate_args=iargs, args=args + ) + __test_single_with_vars( + expr, op, args, iargs, hasValueVar, valueVar, reducer + ) diff --git a/pyteal/ast/ternaryexpr.py b/pyteal/ast/ternaryexpr.py index 248378587..94776e800 100644 --- a/pyteal/ast/ternaryexpr.py +++ b/pyteal/ast/ternaryexpr.py @@ -128,3 +128,25 @@ def SetByte(value: Expr, index: Expr, newByteValue: Expr) -> TernaryExpr: index, newByteValue, ) + + +def Divw(hi: Expr, lo: Expr, y: Expr) -> TernaryExpr: + """ + Performs wide division by interpreting `hi` and `lo` as a uint128 value. + + Requires TEAL version 6 or higher. + + Args: + hi: Quotient's high 64 bits. Must evaluate to uint64. + lo: Quotient's low 64 bits. Must evaluate to uint64. + y: Divisor. Must evaluate to uint64. + + """ + return TernaryExpr( + Op.divw, + (TealType.uint64, TealType.uint64, TealType.uint64), + TealType.uint64, + hi, + lo, + y, + ) diff --git a/pyteal/ast/ternaryexpr_test.py b/pyteal/ast/ternaryexpr_test.py index 22b4817bd..48b35585e 100644 --- a/pyteal/ast/ternaryexpr_test.py +++ b/pyteal/ast/ternaryexpr_test.py @@ -9,6 +9,7 @@ teal3Options = CompileOptions(version=3) teal4Options = CompileOptions(version=4) teal5Options = CompileOptions(version=5) +teal6Options = CompileOptions(version=6) def test_ed25519verify(): @@ -138,3 +139,35 @@ def test_set_byte_invalid(): with pytest.raises(TealTypeError): SetByte(Bytes("base16", "0xFF"), Int(0), Bytes("one")) + + +def test_divw(): + args = [Int(0), Int(90), Int(30)] + expr = Divw(args[0], args[1], args[2]) + assert expr.type_of() == TealType.uint64 + + expected = TealSimpleBlock( + [ + TealOp(args[0], Op.int, args[0].value), + TealOp(args[1], Op.int, args[1].value), + TealOp(args[2], Op.int, args[2].value), + TealOp(expr, Op.divw), + ] + ) + + actual, _ = expr.__teal__(teal6Options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + +def test_divw_invalid(): + with pytest.raises(TealTypeError): + Divw(Bytes("10"), Int(0), Int(1)) + + with pytest.raises(TealTypeError): + Divw(Int(10), Bytes("0"), Int(1)) + + with pytest.raises(TealTypeError): + Divw(Int(10), Int(0), Bytes("1")) diff --git a/pyteal/ast/txn.py b/pyteal/ast/txn.py index ed0638733..d619ef0bc 100644 --- a/pyteal/ast/txn.py +++ b/pyteal/ast/txn.py @@ -108,6 +108,8 @@ class TxnField(Enum): num_logs = (59, "NumLogs", TealType.uint64, False, 5) created_asset_id = (60, "CreatedAssetID", TealType.uint64, False, 5) created_application_id = (61, "CreatedApplicationID", TealType.uint64, False, 5) + last_log = (62, "LastLog", TealType.bytes, False, 6) + state_proof_pk = (63, "StateProofPK", TealType.bytes, False, 6) def __init__( self, id: int, name: str, type: TealType, is_array: bool, min_version: int @@ -160,6 +162,15 @@ def type_of(self): class TxnaExpr(LeafExpr): """An expression that accesses a transaction array field from the current transaction.""" + @staticmethod + def __validate_index_or_throw(index: Union[int, Expr]): + if not isinstance(index, (int, Expr)): + raise TealInputError( + f"Invalid index type: Expected int or Expr, but received {index}." + ) + if isinstance(index, Expr): + require_type(index, TealType.uint64) + def __init__( self, staticOp: Op, @@ -171,6 +182,8 @@ def __init__( super().__init__() if not field.is_array: raise TealInputError("Unexpected non-array field: {}".format(field)) + self.__validate_index_or_throw(index) + self.staticOp = staticOp self.dynamicOp = dynamicOp self.name = name @@ -605,11 +618,12 @@ def config_asset_clawback(self) -> TxnExpr: def created_asset_id(self) -> TxnExpr: """Get the asset ID allocated by the creation of an ASA. - Currently this only works on inner transactions. - Only set when :any:`type_enum()` is :any:`TxnType.AssetConfig` and this is an asset creation transaction. Requires TEAL version 5 or higher. + + * v5 - Only works on inner transactions. + * >= v6 - Works on top-level and inner transactions. """ return self.makeTxnExpr(TxnField.created_asset_id) @@ -687,14 +701,33 @@ def extra_program_pages(self) -> TxnExpr: def created_application_id(self) -> TxnExpr: """Get the application ID allocated by the creation of an application. - Currently this only works on inner transactions. - Only set when :any:`type_enum()` is :any:`TxnType.ApplicationCall` and this is an app creation call. Requires TEAL version 5 or higher. + + * v5 - Only works on inner transactions. + * >= v6 - Works on top-level and inner transactions. """ return self.makeTxnExpr(TxnField.created_application_id) + def last_log(self) -> TxnExpr: + """A convenience method for getting the last logged message from a transaction. + + Only application calls may log a message. Returns an empty string if no messages were logged. + + Only set when :any:`type_enum()` is :any:`TxnType.ApplicationCall`. + + Requires TEAL version 6 or higher. + """ + return self.makeTxnExpr(TxnField.last_log) + + def state_proof_pk(self) -> TxnExpr: + """Get the state proof public key commitment from a transaction. + + Requires TEAL version 6 or higher. + """ + return self.makeTxnExpr(TxnField.state_proof_pk) + @property def application_args(self) -> TxnArray: """Application call arguments array. @@ -735,11 +768,12 @@ def applications(self) -> TxnArray: def logs(self) -> TxnArray: """The log messages emitted by an application call. - Currently this only works on inner transactions. - :type: TxnArray Requires TEAL version 5 or higher. + + * v5 - Only works on inner transactions. + * >= v6 - Works on top-level and inner transactions. """ return TxnArray(self, TxnField.logs, TxnField.num_logs) diff --git a/pyteal/ast/txn_test.py b/pyteal/ast/txn_test.py index 8f1dff254..3b7f6d741 100644 --- a/pyteal/ast/txn_test.py +++ b/pyteal/ast/txn_test.py @@ -59,6 +59,8 @@ TxnField.nonparticipation: lambda txn: txn.nonparticipation(), TxnField.created_asset_id: lambda txn: txn.created_asset_id(), TxnField.created_application_id: lambda txn: txn.created_application_id(), + TxnField.last_log: lambda txn: txn.last_log(), + TxnField.state_proof_pk: lambda txn: txn.state_proof_pk(), } arrayFieldToProperty: Dict[TxnField, Callable[[TxnObject], TxnArray]] = { @@ -95,9 +97,9 @@ def test_txn_fields(): [], [TealOp(dynamicGtxnArg, Op.int, 0)], ), - (InnerTxn, Op.itxn, Op.itxna, None, [], []), + (InnerTxn, Op.itxn, Op.itxna, Op.itxnas, [], []), *[ - (Gitxn[i], Op.gitxn, Op.gitxna, None, [i], []) + (Gitxn[i], Op.gitxn, Op.gitxna, Op.gitxnas, [i], []) for i in range(MAX_GROUP_SIZE) ], ] diff --git a/pyteal/ir/ops.py b/pyteal/ir/ops.py index aec39652d..7d91dab42 100644 --- a/pyteal/ir/ops.py +++ b/pyteal/ir/ops.py @@ -171,9 +171,12 @@ def min_version(self) -> int: gtxnsas = OpType("gtxnsas", Mode.Signature | Mode.Application, 5) args = OpType("args", Mode.Signature, 5) bsqrt = OpType("bsqrt", Mode.Signature | Mode.Application, 6) + divw = OpType("divw", Mode.Signature | Mode.Application, 6) itxn_next = OpType("itxn_next", Mode.Application, 6) + itxnas = OpType("itxnas", Mode.Application, 6) gitxn = OpType("gitxn", Mode.Application, 6) gitxna = OpType("gitxna", Mode.Application, 6) + gitxnas = OpType("gitxnas", Mode.Application, 6) gloadss = OpType("gloadss", Mode.Application, 6) acct_params_get = OpType("acct_params_get", Mode.Application, 6) # fmt: on diff --git a/pyteal/ir/tealblock.py b/pyteal/ir/tealblock.py index 4cbb36aef..5b337967a 100644 --- a/pyteal/ir/tealblock.py +++ b/pyteal/ir/tealblock.py @@ -230,6 +230,13 @@ def NormalizeBlocks(cls, start: "TealBlock") -> "TealBlock": @classmethod def GetReferencedScratchSlots(cls, start: "TealBlock") -> List["ScratchSlot"]: + """Get all scratch slot references for the graph starting at this TealBlock. + + Returns: + A list of ScratchSlots where each element represents a reference to that slot by a + TealOp in the graph. The order of the list is consistent, and there may be duplicate + ScratchSlots in the list if the same slot is referenced multiple times. + """ slots: List[ScratchSlot] = [] for block in TealBlock.Iterate(start): @@ -242,6 +249,20 @@ def GetReferencedScratchSlots(cls, start: "TealBlock") -> List["ScratchSlot"]: def MatchScratchSlotReferences( cls, actual: List["ScratchSlot"], expected: List["ScratchSlot"] ) -> bool: + """Determine if there is a mapping between the actual and expected lists of ScratchSlots. + + A mapping is defined as follows: + * The actual and expected lists must have the same length. + * For every ScratchSlot referenced by either list: + * If the slot appears in both lists, it must appear the exact same number of times and at + the exact same indexes in both lists. + * If the slot appears only in one list, for each of its appearances in that list, there + must be a ScratchSlot in the other list that appears the exact same number of times + and at the exact same indexes. + + Returns: + True if and only if a mapping as described above exists between actual and expected. + """ if len(actual) != len(expected): return False