diff --git a/.flake8 b/.flake8 index 99982dab1..262d4b172 100644 --- a/.flake8 +++ b/.flake8 @@ -10,6 +10,7 @@ ignore = per-file-ignores = pyteal/compiler/optimizer/__init__.py: F401 + examples/application/abi/algobank.py: F403, F405 examples/application/asset.py: F403, F405 examples/application/opup.py: F403, F405 examples/application/security_token.py: F403, F405 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 593804daa..b08b2190f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,7 +23,7 @@ jobs: - name: Install python dependencies run: make setup-development - name: Build and Test - run: make build-and-test + run: make lint-and-test run-integration-tests: runs-on: ubuntu-20.04 @@ -63,8 +63,8 @@ jobs: run: make sandbox-dev-up - name: Install python dependencies run: make setup-development - - name: Build, Unit Tests and Integration Tests - run: make all-tests + - name: Integration Tests Only + run: make test-integration - name: Stop running images run: make sandbox-dev-stop diff --git a/.gitignore b/.gitignore index f930cdc2f..26d6609eb 100644 --- a/.gitignore +++ b/.gitignore @@ -102,6 +102,7 @@ celerybeat-schedule *.sage.py # Environments +_env .env .venv env/ @@ -138,3 +139,6 @@ dmypy.json # mac OS .DS_Store + +# asdf +.tool-versions diff --git a/Makefile b/Makefile index 43dc1d467..bac5a74fa 100644 --- a/Makefile +++ b/Makefile @@ -40,7 +40,7 @@ black: flake8: flake8 $(ALLPY) -MYPY = pyteal scripts +MYPY = pyteal scripts tests mypy: mypy --show-error-codes $(MYPY) @@ -54,9 +54,9 @@ test-unit: pytest -n $(NUM_PROCS) --durations=10 -sv pyteal tests/unit --ignore tests/unit/blackbox_test.py --ignore tests/unit/user_guide_test.py pytest -n 1 -sv tests/unit/blackbox_test.py tests/unit/user_guide_test.py -build-and-test: check-generate-init lint test-unit +lint-and-test: check-generate-init lint test-unit -# ---- Integration Test (algod required) ---- # +# ---- Integration Tests (algod required) ---- # sandbox-dev-up: docker-compose up -d algod @@ -69,7 +69,7 @@ integration-run: test-integration: integration-run -all-tests: build-and-test test-integration +all-tests: lint-and-test test-integration # ---- Local Github Actions Simulation via `act` ---- # # assumes act is installed, e.g. via `brew install act` diff --git a/docs/abi.rst b/docs/abi.rst new file mode 100644 index 000000000..9186b0ee1 --- /dev/null +++ b/docs/abi.rst @@ -0,0 +1,917 @@ +.. _abi: + +ABI Support +=========== + +.. warning:: + ABI support is still taking shape and is subject to backwards incompatible changes. + * Based on feedback, the API and usage patterns are likely to change. + * For the following use cases, feel encouraged to rely on abstractions. Expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. + + * :any:`ABIReturnSubroutine` usage for ABI Application entry point definition. + + * :any:`Router` usage for defining how to route program invocations. + * For general purpose :any:`Subroutine` definition usage, use at your own risk. Based on feedback, the API and usage patterns will change more freely and with less effort to provide migration paths. + + For these reasons, we strongly recommend using :any:`pragma` or the :any:`Pragma` expression to pin the version of PyTeal in your source code. See :ref:`version pragmas` for more information. + +`ARC-4 `_ introduces a set of standards that increase the interoperability of smart contracts in the Algorand ecosystem. This set of standards is commonly referred to as Algorand's application binary interface, or ABI. + +This page will introduce and explain the relevant concepts necessary to build a PyTeal application that adheres to the ARC-4 ABI standards. + +Types +------ + +The ABI supports a variety of data types whose encodings are standardized. + +.. note:: + Be aware that the ABI type system in PyTeal has been designed specifically for the limited use case of describing a program's inputs and outputs. At the time of writing, we **do not recommend** using ABI types in a program's internal storage or computation logic, as the more basic :any:`TealType.uint64` and :any:`TealType.bytes` :any:`Expr` types are far more efficient for these purposes. + +Fundamentals +~~~~~~~~~~~~ + +Before diving into the specific ABI types, let us first explain the the fundamentals of PyTeal's ABI type system, which includes behavior common to all ABI types. + +:code:`abi.BaseType` +^^^^^^^^^^^^^^^^^^^^ + +:any:`abi.BaseType` is an abstract base class that all ABI type classes inherit from. This class defines a few methods common to all ABI types: + +* :any:`abi.BaseType.decode(...) ` is used to decode and populate a type's value from an encoded byte string. +* :any:`abi.BaseType.encode()` is used to encode a type's value into an encoded byte string. +* :any:`abi.BaseType.type_spec()` is used to get an instance of :any:`abi.TypeSpec` that describes that type. + +:code:`abi.TypeSpec` +^^^^^^^^^^^^^^^^^^^^ + +:any:`abi.TypeSpec` is an abstract base class used to describe ABI types. Every child class of :code:`abi.BaseType` also has a companion :code:`abi.TypeSpec` child class. The :code:`abi.TypeSpec` class has a few methods that return information about the type it represents, but one of the class's most important features is the method :any:`abi.TypeSpec.new_instance()`, which creates and returns a new :any:`abi.BaseType` instance of the ABI type it represents. + +Static vs Dynamic Types +^^^^^^^^^^^^^^^^^^^^^^^ + +An important property of an ABI type is whether it is static or dynamic. + +Static types are defined as types whose encoded length does not depend on the value of that type. This property allows encoding and decoding of static types to be more efficient. For example, the encoding of a :code:`boolean` type will always have a fixed length, regardless of whether the value is true or false. + +Likewise, dynamic types are defined as types whose encoded length does in fact depend on the value that type has. For example, it's not possible to know the encoding size of a variable-sized :code:`string` type without also knowing its value. Due to this dependency on values, the code that PyTeal generates to encode, decode, and manipulate dynamic types is more complex and generally less efficient than the code needed for static types. + +Because of the difference in complexity and efficiency when working with static and dynamic types, **we strongly recommend using static types over dynamic types whenever possible**. Using static types generally makes your program's resource usage more predictable as well, so you can be more confident your app has enough computation budget and storage space when using static types. + +Instantiating Types +^^^^^^^^^^^^^^^^^^^ + +There are a few ways to create an instance of an ABI type. Each method produces the same result, but some may be more convenient than others in certain situations. + +.. note:: + The following examples reference specific ABI types, which will be introduced in the :ref:`Type Categories` section. + +With the Constructor +"""""""""""""""""""""" + +The most straightforward way is to use its constructor, like so: + +.. code-block:: python + + from pyteal import * + + my_uint8 = abi.Uint8() + my_uint64 = abi.Uint64() + my_array = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 12)) + +For simple types, using the constructor is straightforward and works as you would expect. However, compound types like :any:`abi.StaticArray` have type-level arguments, so their constructor must take an :any:`abi.TypeSpec` which fully defines all necessary arguments. These types can be created with a constructor, but it's often not the most convenient way to do so. + +With an :code:`abi.TypeSpec` Instance +"""""""""""""""""""""""""""""""""""""" + +Recall that :any:`abi.TypeSpec` has a :any:`new_instance() ` method which instantiates ABI types. This is another way of instantiating ABI types, if you have an :any:`abi.TypeSpec` instance available. For example: + +.. code-block:: python + + from pyteal import * + + my_uint_type = abi.Uint8TypeSpec() + my_uint = my_uint_type.new_instance() + + my_array_type = abi.StaticArrayTypeSpec(my_uint_type, 12) + my_array = my_array_type.new_instance() + +With :code:`abi.make` +""""""""""""""""""""" + +Using :code:`abi.TypeSpec.new_instance()` makes sense if you already have an instance of the right :any:`abi.TypeSpec`, but otherwise it's not much better than using the constructor. Because of this, we have the :any:`abi.make` method, which is perhaps the most convenient way to create a compound type. + +To use it, you pass in a `PEP 484 `__ Python type annotation that describes the ABI type, and :any:`abi.make` will create an instance of it for you. For example: + +.. code-block:: python + + from typing import Literal + from pyteal import * + + my_uint8 = abi.make(abi.Uint8) + my_uint64 = abi.make(abi.Uint64) + my_array = abi.make(abi.StaticArray[abi.Uint8, Literal[12]]) + +.. note:: + Since Python does not allow integers to be directly embedded in type annotations, you must wrap any integer arguments in the :code:`Literal` annotation from the :code:`typing` module. + +.. _Computed Values: + +Computed Values +^^^^^^^^^^^^^^^^^^^^^^^ + +With the introduction of ABI types, it's only natural for there to be functions and operations which return ABI values. In a conventional language, it would be enough to return an instance of the type directly from the operation. However, in PyTeal, these operations must actually return two values: + +1. An instance of the ABI type that will be populated with the right value +2. An :code:`Expr` object that contains the expressions necessary to compute and populate the value that the return type should have + +In order to combine these two pieces of information, the :any:`abi.ComputedValue[T] ` interface was introduced. Instead of directly returning an instance of the appropriate ABI type, functions that return ABI values will return an :any:`abi.ComputedValue` instance parameterized by the return type. + +For example, the :any:`abi.Tuple.__getitem__` function does not return an :any:`abi.BaseType`; instead, it returns an :code:`abi.TupleElement[abi.BaseType]` instance, which inherits from :code:`abi.ComputedValue[abi.BaseType]`. + +The :any:`abi.ComputedValue[T] ` abstract base class provides the following methods: + +* :any:`abi.ComputedValue[T].produced_type_spec() `: returns the :any:`abi.TypeSpec` representing the ABI type produced by this object. +* :any:`abi.ComputedValue[T].store_into(output: T) `: computes the value and store it into the ABI type instance :code:`output`. +* :any:`abi.ComputedValue[T].use(action: Callable[[T], Expr]) `: computes the value and passes it to the callable expression :code:`action`. This is offered as a convenience over the :code:`store_into(...)` method if you don't want to create a new variable to store the value before using it. + +.. note:: + If you call the methods :code:`store_into(...)` or :code:`use(...)` multiple times, the computation to determine the value will be repeated each time. For this reason, it's recommended to only issue a single call to either of these two methods. + +A brief example is below: + +.. code-block:: python + + from typing import Literal as L + from pyteal import * + + @Subroutine(TealType.none) + def assert_sum_equals( + array: abi.StaticArray[abi.Uint64, L[10]], expected_sum: Expr + ) -> Expr: + """This subroutine asserts that the sum of the elements in `array` equals `expected_sum`""" + i = ScratchVar(TealType.uint64) + actual_sum = ScratchVar(TealType.uint64) + tmp_value = abi.Uint64() + return Seq( + For(i.store(Int(0)), i.load() < array.length(), i.store(i.load() + Int(1))).Do( + If(i.load() <= Int(5)) + # Both branches of this If statement are equivalent + .Then( + # This branch showcases how to use `store_into` + Seq( + array[i.load()].store_into(tmp_value), + actual_sum.store(actual_sum.load() + tmp_value.get()), + ) + ).Else( + # This branch showcases how to use `use` + array[i.load()].use( + lambda value: actual_sum.store(actual_sum.load() + value.get()) + ) + ) + ), + Assert(actual_sum.load() == expected_sum), + ) + +.. _Type Categories: + +Type Categories +~~~~~~~~~~~~~~~~~~~~ + +There are three categories of ABI types: + +#. :ref:`Basic Types` +#. :ref:`Reference Types` +#. :ref:`Transaction Types` + +Each of which is described in detail in the following subsections. + +.. _Basic Types: + +Basic Types +^^^^^^^^^^^^^^^^^^^^^^^^ + +Basic types are the most straightforward category of ABI types. These types are used to hold values and they have no other special meaning, in contrast to the other categories of types. + +Definitions +""""""""""""""""""""" + +PyTeal supports the following basic types: + +============================================== ====================== =================================== ======================================================================================================================================================= +PyTeal Type ARC-4 Type Dynamic / Static Description +============================================== ====================== =================================== ======================================================================================================================================================= +:any:`abi.Uint8` :code:`uint8` Static An 8-bit unsigned integer +:any:`abi.Uint16` :code:`uint16` Static A 16-bit unsigned integer +:any:`abi.Uint32` :code:`uint32` Static A 32-bit unsigned integer +:any:`abi.Uint64` :code:`uint64` Static A 64-bit unsigned integer +:any:`abi.Bool` :code:`bool` Static A boolean value that can be either 0 or 1 +:any:`abi.Byte` :code:`byte` Static An 8-bit unsigned integer. This is an alias for :code:`abi.Uint8` that should be used to indicate non-numeric data, such as binary arrays. +:any:`abi.StaticArray[T,N] ` :code:`T[N]` Static when :code:`T` is static A fixed-length array of :code:`T` with :code:`N` elements +:any:`abi.Address` :code:`address` Static A 32-byte Algorand address. This is an alias for :code:`abi.StaticArray[abi.Byte, Literal[32]]`. +:any:`abi.DynamicArray[T] ` :code:`T[]` Dynamic A variable-length array of :code:`T` +:any:`abi.String` :code:`string` Dynamic A variable-length byte array assumed to contain UTF-8 encoded content. This is an alias for :code:`abi.DynamicArray[abi.Byte]`. +:any:`abi.Tuple`\* :code:`(...)` Static when all elements are static A tuple of multiple types +============================================== ====================== =================================== ======================================================================================================================================================= + +.. note:: + \*A proper implementation of :any:`abi.Tuple` requires a variable amount of generic arguments. Python 3.11 will support this with the introduction of `PEP 646 - Variadic Generics `_, but until then it will not be possible to make :any:`abi.Tuple` a generic type. As a workaround, we have introduced the following subclasses of :any:`abi.Tuple` for tuples containing up to 5 generic arguments: + + * :any:`abi.Tuple0`: a tuple of zero values, :code:`()` + * :any:`abi.Tuple1[T1] `: a tuple of one value, :code:`(T1)` + * :any:`abi.Tuple2[T1,T2] `: a tuple of two values, :code:`(T1,T2)` + * :any:`abi.Tuple3[T1,T2,T3] `: a tuple of three values, :code:`(T1,T2,T3)` + * :any:`abi.Tuple4[T1,T2,T3,T4] `: a tuple of four values, :code:`(T1,T2,T3,T4)` + * :any:`abi.Tuple5[T1,T2,T3,T4,T5] `: a tuple of five values, :code:`(T1,T2,T3,T4,T5)` + +These ARC-4 types are not yet supported in PyTeal: + +* Non-power-of-2 unsigned integers under 64 bits, i.e. :code:`uint24`, :code:`uint48`, :code:`uint56` +* Unsigned integers larger than 64 bits +* Fixed point unsigned integers, i.e. :code:`ufixedx` + +Limitations +""""""""""""""""""""" + +Due to the nature of their encoding, dynamic container types, i.e. :any:`abi.DynamicArray[T] ` and :any:`abi.String`, have an implicit limit on the number of elements they may contain. This limit is :code:`2^16 - 1`, or 65535. However, the AVM has a stack size limit of 4096 for byte strings, so it's unlikely this encoding limit will be reached by your program. + +Static container types have no such limit. + +Usage +""""""""""""""""""""" + +Setting Values +'''''''''''''''' + +All basic types have a :code:`set()` method which can be used to assign a value. The arguments for this method differ depending on the ABI type. For convenience, here are links to the docs for each class's method: + +* :any:`abi.Uint.set(...) `, which is used by all :code:`abi.Uint` classes and :code:`abi.Byte` +* :any:`abi.Bool.set(...) ` +* :any:`abi.StaticArray[T, N].set(...) ` +* :any:`abi.Address.set(...) ` +* :any:`abi.DynamicArray[T].set(...) ` +* :any:`abi.String.set(...) ` +* :any:`abi.Tuple.set(...) ` + +A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior. + +.. code-block:: python + + from pyteal import * + + my_address = abi.make(abi.Address) + my_bool = abi.make(abi.Bool) + my_uint64 = abi.make(abi.Uint64) + my_tuple = abi.make(abi.Tuple3[abi.Address, abi.Bool, abi.Uint64]) + + program = Seq( + my_address.set(Txn.sender()), + my_bool.set(Txn.fee() == Int(0)), + # It's ok to set an abi.Uint to a Python integer. This is actually preferred since PyTeal + # can determine at compile-time that the value will fit in the integer type. + my_uint64.set(5000), + my_tuple.set(my_address, my_bool, my_uint64) + ) + +Getting Single Values +'''''''''''''''''''''' + +All basic types that represent a single value have a :code:`get()` method, which can be used to extract that value. The supported types and methods are: + +* :any:`abi.Uint.get()`, which is used by all :any:`abi.Uint` classes and :any:`abi.Byte` +* :any:`abi.Bool.get()` +* :any:`abi.Address.get()` +* :any:`abi.String.get()` + +A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior. + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.uint64) + def minimum(a: abi.Uint64, b: abi.Uint64) -> Expr: + """Return the minimum value of the two arguments.""" + return ( + If(a.get() < b.get()) + .Then(a.get()) + .Else(b.get()) + ) + +Getting Values at Indexes - Compound Types +'''''''''''''''''''''''''''''''''''''''''' + +The types :any:`abi.StaticArray`, :any:`abi.Address`, :any:`abi.DynamicArray`, :any:`abi.String`, and :any:`abi.Tuple` are compound types, meaning they contain other types whose values can be extracted. The :code:`__getitem__` method, accessible by using square brackets to "index into" an object, can be used to access these values. + +The supported methods are: + +* :any:`abi.StaticArray.__getitem__(index: int | Expr) `, used for :any:`abi.StaticArray` and :any:`abi.Address` +* :any:`abi.Array.__getitem__(index: int | Expr) `, used for :any:`abi.DynamicArray` and :any:`abi.String` +* :any:`abi.Tuple.__getitem__(index: int) ` + +.. note:: + Be aware that these methods return a :any:`ComputedValue`, similar to other PyTeal operations which return ABI types. More information about why that is necessary and how to use a :any:`ComputedValue` can be found in the :ref:`Computed Values` section. + +A brief example is below. Please consult the documentation linked above for each method to learn more about specific usage and behavior. + +.. code-block:: python + + from typing import Literal as L + from pyteal import * + + @Subroutine(TealType.none) + def ensure_all_values_greater_than_5(array: abi.StaticArray[abi.Uint64, L[10]]) -> Expr: + """This subroutine asserts that every value in the input array is greater than 5.""" + i = ScratchVar(TealType.uint64) + return For( + i.store(Int(0)), i.load() < array.length(), i.store(i.load() + Int(1)) + ).Do( + array[i.load()].use(lambda value: Assert(value.get() > Int(5))) + ) + +.. _Reference Types: + +Reference Types +^^^^^^^^^^^^^^^^^^^^^^^^ + +Many applications require the caller to provide "foreign array" values when calling the app. These are the blockchain entities (such as accounts, assets, or other applications) that the application will interact with when executing this call. In the ABI, we have **Reference Types** to describe these requirements. + +Definitions +"""""""""""""""""""""""""""""""""""""""""" + +PyTeal supports the following Reference Types: + +====================== ====================== ================ ======================================================================================================================================================= +PyTeal Type ARC-4 Type Dynamic / Static Description +====================== ====================== ================ ======================================================================================================================================================= +:any:`abi.Account` :code:`account` Static Represents an account that the current transaction can access, stored in the :any:`Txn.accounts ` array +:any:`abi.Asset` :code:`asset` Static Represents an asset that the current transaction can access, stored in the :any:`Txn.assets ` array +:any:`abi.Application` :code:`application` Static Represents an application that the current transaction can access, stored in the :any:`Txn.applications ` array +====================== ====================== ================ ======================================================================================================================================================= + +These types all inherit from the abstract class :any:`abi.ReferenceType`. + +Limitations +"""""""""""""""""""""""""""""""""""""""""" + +Because References Types have a special meaning, they should not be directly created, and they cannot be assigned a value by a program. + +Additionally, Reference Types are only valid in the arguments of a method. They may not appear in a method's return value. + +Note that the AVM has `limitations on the maximum number of foreign references `_ an application call transaction may contain. At the time of writing, these limits are: + +* Accounts: 4 +* Assets: 8 +* Applications: 8 +* Sum of Accounts, Assets, and Applications: 8 + +.. warning:: + Because of these limits, methods that have a large amount of Reference Type arguments may be impossible to call as intended at runtime. + +Usage +"""""""""""""""""""""""""""""""""""""""""" + +Getting Referenced Values +'''''''''''''''''''''''''' + +Depending on the Reference Type, there are different methods available to obtain the value being referenced: + +* :any:`abi.Account.address()` +* :any:`abi.Asset.asset_id()` +* :any:`abi.Application.application_id()` + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def send_inner_txns( + receiver: abi.Account, asset_to_transfer: abi.Asset, app_to_call: abi.Application + ) -> Expr: + return Seq( + InnerTxnBuilder.Begin(), + InnerTxnBuilder.SetFields( + { + TxnField.type_enum: TxnType.AssetTransfer, + TxnField.receiver: receiver.address(), + TxnField.xfer_asset: asset_to_transfer.asset_id(), + TxnField.amount: Int(1_000_000), + } + ), + InnerTxnBuilder.Submit(), + InnerTxnBuilder.Begin(), + InnerTxnBuilder.SetFields( + { + TxnField.type_enum: TxnType.ApplicationCall, + TxnField.application_id: app_to_call.application_id(), + Txn.application_args: [Bytes("hello")], + } + ), + InnerTxnBuilder.Submit(), + ) + +Accessing Parameters of Referenced Values +'''''''''''''''''''''''''''''''''''''''''' + +Reference Types allow the program to access more information about them. Each Reference Type has a :code:`params()` method which can be used to access that object's parameters. These methods are listed below: + +* :any:`abi.Account.params()` returns an :any:`AccountParamObject` +* :any:`abi.Asset.params()` returns an :any:`AssetParamObject` +* :any:`abi.Application.params()` returns an :any:`AppParamObject` + +These method are provided for convenience. They expose the same properties accessible from the :any:`AccountParam`, :any:`AssetParam`, and :any:`AppParam` classes. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def referenced_params_example( + account: abi.Account, asset: abi.Asset, app: abi.Application + ) -> Expr: + return Seq( + account.params().auth_address().outputReducer( + lambda value, has_value: Assert(And(has_value, value == Global.zero_address())) + ), + asset.params().total().outputReducer( + lambda value, has_value: Assert(And(has_value, value == Int(1))) + ), + app.params().creator_address().outputReducer( + lambda value, has_value: Assert(And(has_value, value == Txn.sender())) + ) + ) + +.. note:: + All returned parameters are instances of :any:`MaybeValue`, which is why the :any:`outputReducer(...) ` method is used. + +Accessing Asset Holdings +'''''''''''''''''''''''' + +Similar to the parameters above, asset holding properties can be accessed using one of the following methods: + +* :any:`abi.Account.asset_holding(asset: Expr | abi.Asset) `: given an asset, returns an :any:`AssetHoldingObject` +* :any:`abi.Asset.holding(account: Expr | abi.Account) `: given an account, returns an :any:`AssetHoldingObject` + +These method are provided for convenience. They expose the same properties accessible from the :any:`AssetHolding` class. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def ensure_asset_balance_is_nonzero(account: abi.Account, asset: abi.Asset) -> Expr: + return Seq( + account.asset_holding(asset) + .balance() + .outputReducer(lambda value, has_value: Assert(And(has_value, value > Int(0)))), + # this check is equivalent + asset.holding(account) + .balance() + .outputReducer(lambda value, has_value: Assert(And(has_value, value > Int(0)))), + ) + +.. _Transaction Types: + +Transaction Types +^^^^^^^^^^^^^^^^^^^^^^^^ + +Some application calls require that they are invoked as part of a larger transaction group containing specific additional transactions. In order to express these types of calls, the ABI has **Transaction Types**. + +Every Transaction Type argument represents a specific and unique transaction that must appear immediately before the application call in the same transaction group. A method may have multiple Transaction Type arguments, in which case they must appear in the same order as the method's arguments immediately before the method application call. + +Definitions +"""""""""""""""""""""""""""""""""""""""""" + +PyTeal supports the following Transaction Types: + +===================================== ====================== ================ ======================================================================================================================================================= +PyTeal Type ARC-4 Type Dynamic / Static Description +===================================== ====================== ================ ======================================================================================================================================================= +:any:`abi.Transaction` :code:`txn` Static A catch-all for any type of transaction +:any:`abi.PaymentTransaction` :code:`pay` Static A payment transaction +:any:`abi.KeyRegisterTransaction` :code:`keyreg` Static A key registration transaction +:any:`abi.AssetConfigTransaction` :code:`acfg` Static An asset configuration transaction +:any:`abi.AssetTransferTransaction` :code:`axfer` Static An asset transfer transaction +:any:`abi.AssetFreezeTransaction` :code:`afrz` Static An asset freeze transaction +:any:`abi.ApplicationCallTransaction` :code:`appl` Static An application call transaction +===================================== ====================== ================ ======================================================================================================================================================= + +Limitations +"""""""""""""""""""""""""""""""""""""""""" + +Due to the special meaning of Transaction Types, they **cannot** be used as the return value of a method. They can be used as method arguments, but only at the top-level. This means that it's not possible to embed a Transaction Type inside a tuple or array. + +Transaction Types should not be directly created, and they cannot be modified by a program. + +Because the AVM has a maximum of 16 transactions in a single group, at most 15 Transaction Types may be used in the arguments of a method. + +Usage +"""""""""""""""""""""""""""""""""""""""""" + +Getting the Transaction Group Index +'''''''''''''''''''''''''''''''''''' + +All Transaction Types implement the :any:`abi.Transaction.index()` method, which returns the absolute index of that transaction in the group. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def handle_txn_args( + any_txn: abi.Transaction, + pay: abi.PaymentTransaction, + axfer: abi.AssetTransferTransaction, + ) -> Expr: + return Seq( + Assert(any_txn.index() == Txn.group_index() - Int(3)), + Assert(pay.index() == Txn.group_index() - Int(2)), + Assert(axfer.index() == Txn.group_index() - Int(1)), + ) + +Accessing Transaction Fields +''''''''''''''''''''''''''''' + +All Transaction Types implement the :any:`abi.Transaction.get()` method, which returns a :any:`TxnObject` instance that can be used to access fields from that transaction. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + """This method receives a payment from an account opted into this app + and records it in their local state. + """ + return Seq( + Assert(payment.get().sender() == sender.address()), + Assert(payment.get().receiver() == Global.current_application_address()), + App.localPut( + sender.address(), + Bytes("balance"), + App.localGet(sender.address(), Bytes("balance")) + payment.get().amount(), + ), + ) + + +Subroutines with ABI Types +-------------------------- + +Subroutines can be created that accept ABI types as arguments and produce ABI types as return values. PyTeal will type check all subroutine calls and ensure that the correct types are being passed to such subroutines and that their return values are used correctly. + +There are two different ways to use ABI types in subroutines, depending on whether the return value is an ABI type or a PyTeal :code:`Expr`. + +Subroutines that Return Expressions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If you'd like to create a subroutine that accepts some or all arguments as ABI types, but whose return value is a PyTeal :code:`Expr`, the :any:`@Subroutine ` decorator can be used. + +To indicate the type of each argument, `PEP 484 `__ Python type annotations are used. Unlike normal usage of Python type annotations which are ignored at runtime, type annotations for subroutines inform the PyTeal compiler about the inputs and outputs of a subroutine. Changing these values has a direct effect on the code PyTeal generates. + +An example of this type of subroutine is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.uint64) + def get_volume_of_rectangular_prism( + length: abi.Uint16, width: abi.Uint64, height: Expr + ) -> Expr: + return length.get() * width.get() * height + +Notice that this subroutine accepts the following arguments, not all of which are ABI types: + +* :code:`length`: an ABI :any:`abi.Uint16` type +* :code:`width`: an ABI :any:`abi.Uint64` type +* :code:`height`: a PyTeal :any:`Expr` type + +Despite some inputs being ABI types, calling this subroutine works the same as usual, except the values for the ABI type arguments must be the appropriate ABI type. + +The following example shows how to prepare the arguments for and call :code:`get_volume_of_rectangular_prism()`: + +.. code-block:: python + + # This is a continuation of the previous example + + length = abi.Uint16() + width = abi.Uint64() + height = Int(10) + program = Seq( + length.set(4), + width.set(9), + Assert(get_volume_of_rectangular_prism(length, width, height) > Int(0)) + ) + +Subroutines that Return ABI Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + :any:`ABIReturnSubroutine` is still taking shape and is subject to backwards incompatible changes. + + * For ABI Application entry point definition, feel encouraged to use :any:`ABIReturnSubroutine`. Expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. + * For general purpose usage, use at your own risk. Based on feedback, the API and usage patterns will change more freely and with less effort to provide migration paths. + + For these reasons, we strongly recommend using :any:`pragma` or the :any:`Pragma` expression to pin the version of PyTeal in your source code. See :ref:`version pragmas` for more information. + +In addition to accepting ABI types as arguments, it's also possible for a subroutine to return an ABI type value. + +As mentioned in the :ref:`Computed Values` section, operations which return ABI values instead of traditional :code:`Expr` objects need extra care. In order to solve this problem for subroutines, a new decorator, :any:`@ABIReturnSubroutine ` has been introduced. + +The :code:`@ABIReturnSubroutine` decorator should be used with subroutines that return an ABI value. Subroutines defined with this decorator will have two places to output information: the function return value, and a `keyword-only argument `_ called :code:`output`. The function return value must remain an :code:`Expr`, while the :code:`output` keyword argument will contain the ABI value the subroutine wishes to return. An example is below: + +.. code-block:: python + + from pyteal import * + + @ABIReturnSubroutine + def get_account_status( + account: abi.Address, *, output: abi.Tuple2[abi.Uint64, abi.Bool] + ) -> Expr: + balance = abi.Uint64() + is_admin = abi.Bool() + return Seq( + balance.set(App.localGet(account.get(), Bytes("balance"))), + is_admin.set(App.localGet(account.get(), Bytes("is_admin"))), + output.set(balance, is_admin), + ) + + account = abi.make(abi.Address) + status = abi.make(abi.Tuple2[abi.Uint64, abi.Bool]) + program = Seq( + account.set(Txn.sender()), + # NOTE! The return value of get_account_status(account) is actually a ComputedValue[abi.Tuple2[abi.Uint64, abi.Bool]] + get_account_status(account).store_into(status), + ) + +Notice that even though the original :code:`get_account_status` function returns an :code:`Expr` object, the :code:`@ABIReturnSubroutine` decorator automatically transforms the function's return value and the :code:`output` variable into a :code:`ComputedValue`. As a result, callers of this subroutine must work with a :code:`ComputedValue`. + +The only exception to this transformation is if the subroutine has no return value. Without a return value, a :code:`ComputedValue` is unnecessary and the subroutine will still return an :code:`Expr` to the caller. In this case, the :code:`@ABIReturnSubroutine` decorator acts identically the :code:`@Subroutine` decorator. + +Creating an ARC-4 Program +---------------------------------------------------- + +An ARC-4 program, like all other programs, can be called by application call transactions. ARC-4 programs respond to two specific subtypes of application call transactions: + +* **Method calls**, which encode a specific method to be called and arguments for that method, if needed. +* **Bare app calls**, which have no arguments and no return value. + +A method is a section of code intended to be invoked externally with an application call transaction. Methods may take arguments and may produce a return value. PyTeal implements methods as subroutines which are exposed to be externally callable. + +A bare app call is more limited than a method, since it takes no arguments and cannot return a value. For this reason, bare app calls are more suited to allow on completion actions to take place, such as opting into an app. + +To make it easier for an application to route across the many bare app calls and methods it may support, PyTeal introduces the :any:`Router` class. This class adheres to the ARC-4 ABI conventions with respect to when methods and bare app calls should be invoked. For methods, it also conveniently decodes all arguments and properly encodes and logs the return value as needed. + +The following sections explain how to register bare app calls and methods with the :any:`Router` class. + +.. warning:: + :any:`Router` usage is still taking shape and is subject to backwards incompatible changes. + + Feel encouraged to use :any:`Router` and expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. + + For these reasons, we strongly recommend using :any:`pragma` or the :any:`Pragma` expression to pin the version of PyTeal in your source code. See :ref:`version pragmas` for more information. + +Registering Bare App Calls +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The AVM supports 6 types of OnCompletion options that may be specified on an app call transaction. These actions are: + +#. **No-op**, the absence of an action, represented by :any:`OnComplete.NoOp` +#. **Opt in**, which allocates account-local storage for an app, represented by :any:`OnComplete.OptIn` +#. **Close out**, which removes account-local storage for an app, represented by :any:`OnComplete.CloseOut` +#. **Clear state**, which forcibly removes account-local storage for an app, represented by :any:`OnComplete.ClearState` +#. **Update application**, which updates an app, represented by :any:`OnComplete.UpdateApplication` +#. **Delete application**, which deletes an app, represented by :any:`OnComplete.DeleteApplication` + +In PyTeal, you have the ability to register a bare app call handler for each of these actions. Additionally, a bare app call handler must also specify whether the handler can be invoking during an **app creation transaction** (:any:`CallConfig.CREATE`), during a **non-creation app call** (:any:`CallConfig.CALL`), or during **either** (:any:`CallConfig.ALL`). + +The :any:`BareCallActions` class is used to define a bare app call handler for on completion actions. Each bare app call handler must be an instance of the :any:`OnCompleteAction` class. + +The :any:`OnCompleteAction` class is responsible for holding the actual code for the bare app call handler (an instance of either :code:`Expr` or a subroutine that takes no args and returns nothing) as well as a :any:`CallConfig` option that indicates whether the action is able to be called during a creation app call, a non-creation app call, or either. + +All the bare app calls that an application wishes to support must be provided to the :any:`Router.__init__` method. + +A brief example is below: + +.. code-block:: python + + from pyteal import * + + @Subroutine(TealType.none) + def opt_in_handler() -> Expr: + return App.localPut(Txn.sender(), Bytes("opted_in_round"), Global.round()) + + + @Subroutine(TealType.none) + def assert_sender_is_creator() -> Expr: + return Assert(Txn.sender() == Global.creator_address()) + + + router = Router( + name="ExampleApp", + bare_calls=BareCallActions( + # Allow app creation with a no-op action + no_op=OnCompleteAction( + action=Approve(), call_config=CallConfig.CREATE + ), + + # Register the `opt_in_handler` to be called during opt in. + # + # Since we use `CallConfig.ALL`, this is also a valid way to create this app + # (if the creator wishes to immediately opt in). + opt_in=OnCompleteAction( + action=opt_in_handler, call_config=CallConfig.ALL + ), + + # Allow anyone who opted in to close out from the app. + close_out=OnCompleteAction( + action=Approve(), call_config=CallConfig.CALL + ), + + # Only approve update and delete operations if `assert_sender_is_creator` succeeds. + update_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + delete_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + ), + ) + +.. note:: + When deciding which :any:`CallConfig` value is appropriate for a bare app call or method, consider the question, should it be valid for someone to create my app with this operation? Most of the time the answer will be no, in which case :any:`CallConfig.CALL` should be used. + +Registering Methods +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. warning:: + The :any:`Router` **does not** validate inputs for compound types (:any:`abi.StaticArray`, :any:`abi.Address`, :any:`abi.DynamicArray`, :any:`abi.String`, or :any:`abi.Tuple`). + + **We strongly recommend** methods immediately access and validate compound type parameters *before* persisting arguments for later transactions. For validation, it is sufficient to attempt to extract each element your method will use. If there is an input error for an element, indexing into that element will fail. + + Notes: + + * This recommendation applies to recursively contained compound types as well. Successfully extracting an element which is a compound type does not guarantee the extracted value is valid; you must also inspect its elements as well. + * Because of this, :any:`abi.Address` is **not** guaranteed to have exactly 32 bytes. To defend against unintended behavior, manually verify the length is 32 bytes, i.e. :code:`Assert(Len(address.get()) == Int(32))`. + +There are two ways to register a method with the :any:`Router` class. + +The first way to register a method is with the :any:`Router.add_method_handler` method, which takes an existing subroutine decorated with :any:`@ABIReturnSubroutine `. An example of this is below: + +.. code-block:: python + + from pyteal import * + + router = Router( + name="Calculator", + bare_calls=BareCallActions( + # Allow this app to be created with a no-op call + no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE), + # Allow standalone user opt in and close out + opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + ), + ) + + @ABIReturnSubroutine + def add(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments and returns the result. + + If addition will overflow a uint64, this method will fail. + """ + return output.set(a.get() + b.get()) + + + @ABIReturnSubroutine + def addAndStore(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments, returns the result, and stores it in the sender's local state. + + If addition will overflow a uint64, this method will fail. + + The sender must be opted into the app. Opt-in can occur during this call. + """ + return Seq( + output.set(a.get() + b.get()), + # store the result in the sender's local state too + App.localPut(Txn.sender(), Bytes("result", output.get())), + ) + + # Register the `add` method with the router, using the default `MethodConfig` + # (only no-op, non-creation calls allowed). + router.add_method_handler(add) + + # Register the `addAndStore` method with the router, using a `MethodConfig` that allows + # no-op and opt in non-creation calls. + router.add_method_handler( + addAndStore, + method_config=MethodConfig(no_op=CallConfig.CALL, opt_in=CallConfig.CALL), + ) + +This example registers two methods with the router, :code:`add` and :code:`addAndStore`. + +Because the :code:`add` method does not pass a value for the :code:`method_config` parameter of :any:`Router.add_method_handler`, it will use the default value, which will make it only callable with a transaction that is not an app creation and whose on completion value is :code:`OnComplete.NoOp`. + +On the other hand, the :code:`addAndStore` method does provide a :code:`method_config` value. A value of :code:`MethodConfig(no_op=CallConfig.CALL, opt_in=CallConfig.CALL)` indicates that this method can only be called with a transaction that is not an app creation and whose on completion value is one of :code:`OnComplete.NoOp` or :code:`OnComplete.OptIn`. + +The second way to register a method is with the :any:`Router.method` decorator placed directly on a function. This way is equivalent to the first, but has some properties that make it more convenient for some scenarios. Below is an example equivalent to the prior one, but using the :code:`Router.method` syntax: + +.. code-block:: python + + from pyteal import * + + my_router = Router( + name="Calculator", + bare_calls=BareCallActions( + # Allow this app to be created with a no-op call + no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE), + # Allow standalone user opt in and close out + opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + close_out=OnCompleteAction(action=Approve(), call_config=CallConfig.CALL), + ), + ) + + # NOTE: the first part of the decorator `@my_router.method` is the router variable's name + @my_router.method + def add(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments and returns the result. + + If addition will overflow a uint64, this method will fail. + """ + return output.set(a.get() + b.get()) + + @my_router.method(no_op=CallConfig.CALL, opt_in=CallConfig.CALL) + def addAndStore(a: abi.Uint64, b: abi.Uint64, *, output: abi.Uint64) -> Expr: + """Adds the two arguments, returns the result, and stores it in the sender's local state. + + If addition will overflow a uint64, this method will fail. + + The sender must be opted into the app. Opt-in can occur during this call. + """ + return Seq( + output.set(a.get() + b.get()), + # store the result in the sender's local state too + App.localPut(Txn.sender(), Bytes("result", output.get())), + ) + +Compiling a Router Program +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now that we know how to add bare app call and method call handlers to a :any:`Router`, the next step is to compile the :any:`Router` into TEAL code. + +The :any:`Router.compile_program` method exists for this purpose. It combines all registered methods and bare app calls into two ASTs, one for the approval program and one for clear state program, then internally calls :any:`compileTeal` to compile these expressions and create TEAL code. + +.. note:: + We recommend enabling the :code:`scratch_slots` optimization when compiling a program that uses ABI types, since PyTeal's ABI types implementation makes frequent use of scratch slots under-the-hood. See the :ref:`compiler_optimization` page for more information. + +In addition to receiving the approval and clear state programs, the :any:`Router.compile_program` method also returns a `Python SDK `_ :code:`Contract` object. This object represents an `ARC-4 Contract Description `_, which can be distributed to clients to enable them to call the methods on the contract. + +Here's an example of a complete application that uses the :any:`Router` class: + +.. literalinclude:: ../examples/application/abi/algobank.py + :language: python + +This example uses the :code:`Router.compile_program` method to create the approval program, clear state program, and contract description for the "AlgoBank" contract. The produced :code:`algobank.json` file is below: + +.. literalinclude:: ../examples/application/abi/algobank.json + :language: json + +Calling an ARC-4 Program +-------------------------- + +One of the advantages of developing an ABI-compliant PyTeal contract is that there is a standard way for clients to call your contract. + +Broadly, there are two categories of clients that may wish to call your contract: off-chain systems and other on-chain contracts. The following sections describe how each of these clients can call ABI methods implemented by your contract. + +Off-Chain, from an SDK or :code:`goal` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Off-chain systems can use the `Algorand SDKs `_ or the command-line tool `goal `_ to interact with ABI-compliant contracts. + +Every SDK contains an :code:`AtomicTransactionComposer` type that can be used to build and execute transaction groups, including groups containing ABI method calls. More information and examples of this are available on the `Algorand Developer Portal `_. + +The :code:`goal` CLI has subcommands for creating and submitting various types of transactions. The relevant ones for ABI-compliant contracts are mentioned below: + +* For bare app calls: + * For calls that create an app, :code:`goal app create` (`docs `__) can be used to construct and send an app creation bare app call. + * For non-creation calls, :code:`goal app ` can be used to construct and send a non-creation bare app call. The :code:`` keyword should be replaced with one of `"call" `_ (no-op), `"optin" `_, `"closeout" `_, `"clear" `_, `"update" `_, or `"delete" `_, depending on the on-completion value the caller wishes to use. +* For all method calls: + * :code:`goal app method` (`docs `__) can be used to construct, send, and read the return value of a method call. This command can be used for application creation as well, if the application allows this to happen in one of its methods. + +On-Chain, in an Inner Transaction +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Algorand applications can issue `inner transactions `_, which can be used to invoke other applications. + +In PyTeal, this can be achieved using the :any:`InnerTxnBuilder` class and its functions. To invoke an ABI method, PyTeal has :any:`InnerTxnBuilder.MethodCall(...) ` to properly build a method call and encode its arguments. + +.. note:: + At the time of writing, there is no streamlined way to obtain a method return value. You must manually inspect the :any:`last_log() ` property of either :any:`InnerTxn` or :any:`Gitxn[\] ` to obtain the logged return value. `As described in ARC-4 `_, this value will be prefixed with the 4 bytes :code:`151f7c75` (shown in hex), and after this prefix the encoded ABI value will be available. You can use the :any:`decode(...) ` method on an instance of the appropriate ABI type in order to decode and use this value. diff --git a/docs/api.rst b/docs/api.rst index b427806b5..14e5b4666 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -24,3 +24,19 @@ PyTeal Package :annotation: = The most recently submitted inner transaction. This is an instance of :any:`TxnObject`. + + If a transaction group was submitted most recently, then this will be the last transaction in that group. + + .. data:: Gitxn + :annotation: = + + The most recently submitted inner transaction group. This is an instance of :any:`InnerTxnGroup`. + + If a single transaction was submitted most recently, then this will be a group of size 1. + +.. automodule:: pyteal.abi + :members: + :undoc-members: + :imported-members: + :special-members: __getitem__ + :show-inheritance: diff --git a/docs/index.rst b/docs/index.rst index 9ff3f2fe8..5377bc076 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -45,6 +45,7 @@ PyTeal **hasn't been security audited**. Use it at your own risk. versions compiler_optimization opup + abi .. toctree:: :maxdepth: 3 diff --git a/docs/versions.rst b/docs/versions.rst index b64910451..08b5fcf3b 100644 --- a/docs/versions.rst +++ b/docs/versions.rst @@ -1,6 +1,6 @@ .. _versions: -TEAL Versions +Versions ============= Each version of PyTeal compiles contracts for a specific version of TEAL. Newer versions of TEAL @@ -18,14 +18,34 @@ TEAL Version PyTeal Version 6 >= 0.10.0 ============ ============== +.. _version pragmas: + +Version Pragmas +---------------- + +When writing a PyTeal smart contract, it's important to target a specific AVM version and to compile +with a single PyTeal version. This will ensure your compiled program remains consistent and has the +exact same behavior no matter when you compile it. + +The :any:`pragma` function can be used to assert that the current PyTeal version matches a constraint +of your choosing. This can help strengthen the dependency your source code has on the PyTeal package +version you used when writing it. + +If you are writing code for others to consume, or if your codebase has different PyTeal version +dependencies in different places, the :any:`Pragma` expression can be used to apply a pragma +constraint to only a section of the AST. + +PyTeal v0.5.4 and Below +----------------------- + In order to support TEAL v2, PyTeal v0.6.0 breaks backward compatibility with v0.5.4. PyTeal programs written for PyTeal version 0.5.4 and below will not compile properly and most likely will display an error of the form :code:`AttributeError: * object has no attribute 'teal'`. -**WARNING:** before updating PyTeal to a version with generates TEAL v2 contracts and fixing the -programs to use the global function :any:`compileTeal` rather the class method :code:`.teal()`, make -sure your program abides by the TEAL safety guidelines ``_. -Changing a v1 TEAL program to a v2 TEAL program without any code changes is insecure because v2 -TEAL programs allow rekeying. Specifically, you must add a check that the :code:`RekeyTo` property -of any transaction is set to the zero address when updating an older PyTeal program from v0.5.4 and -below. +.. warning:: + If you are updating from a v1 TEAL program, make + sure your program abides by the `TEAL safety guidelines `_. + Changing a v1 TEAL program to a v2 TEAL program without any code changes is insecure because v2 + TEAL programs allow rekeying. Specifically, you must add a check that the :code:`RekeyTo` property + of any transaction is set to the zero address when updating an older PyTeal program from v0.5.4 and + below. diff --git a/examples/application/abi/__init__.py b/examples/application/abi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/examples/application/abi/algobank.json b/examples/application/abi/algobank.json new file mode 100644 index 000000000..a0e3ef26a --- /dev/null +++ b/examples/application/abi/algobank.json @@ -0,0 +1,54 @@ +{ + "name": "AlgoBank", + "methods": [ + { + "name": "deposit", + "args": [ + { + "type": "pay", + "name": "payment" + }, + { + "type": "account", + "name": "sender" + } + ], + "returns": { + "type": "void" + }, + "desc": "This method receives a payment from an account opted into this app and records it in their local state. The caller may opt into this app during this call." + }, + { + "name": "getBalance", + "args": [ + { + "type": "account", + "name": "user" + } + ], + "returns": { + "type": "uint64" + }, + "desc": "Lookup the balance of a user held by this app." + }, + { + "name": "withdraw", + "args": [ + { + "type": "uint64", + "name": "amount" + }, + { + "type": "account", + "name": "recipient" + } + ], + "returns": { + "type": "void" + }, + "desc": "Withdraw an amount of Algos held by this app. The sender of this method call will be the source of the Algos, and the destination will be the `recipient` argument. This may or may not be the same as the sender's address. This method will fail if the amount of Algos requested to be withdrawn exceeds the amount of Algos held by this app for the sender. The Algos will be transferred to the recipient using an inner transaction whose fee is set to 0, meaning the caller's transaction must include a surplus fee to cover the inner transaction." + } + ], + "desc": null, + "networks": {} +} \ No newline at end of file diff --git a/examples/application/abi/algobank.py b/examples/application/abi/algobank.py new file mode 100644 index 000000000..6950825ee --- /dev/null +++ b/examples/application/abi/algobank.py @@ -0,0 +1,113 @@ +# This example is provided for informational purposes only and has not been audited for security. +from pyteal import * +import json + + +@Subroutine(TealType.none) +def assert_sender_is_creator() -> Expr: + return Assert(Txn.sender() == Global.creator_address()) + + +# move any balance that the user has into the "lost" amount when they close out or clear state +transfer_balance_to_lost = App.globalPut( + Bytes("lost"), + App.globalGet(Bytes("lost")) + App.localGet(Txn.sender(), Bytes("balance")), +) + +router = Router( + name="AlgoBank", + bare_calls=BareCallActions( + # approve a creation no-op call + no_op=OnCompleteAction(action=Approve(), call_config=CallConfig.CREATE), + # approve opt-in calls during normal usage, and during creation as a convenience for the creator + opt_in=OnCompleteAction(action=Approve(), call_config=CallConfig.ALL), + # move any balance that the user has into the "lost" amount when they close out or clear state + close_out=OnCompleteAction( + action=transfer_balance_to_lost, call_config=CallConfig.CALL + ), + clear_state=OnCompleteAction( + action=transfer_balance_to_lost, call_config=CallConfig.CALL + ), + # only the creator can update or delete the app + update_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + delete_application=OnCompleteAction( + action=assert_sender_is_creator, call_config=CallConfig.CALL + ), + ), +) + + +@router.method(no_op=CallConfig.CALL, opt_in=CallConfig.CALL) +def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + """This method receives a payment from an account opted into this app and records it in + their local state. + + The caller may opt into this app during this call. + """ + return Seq( + Assert(payment.get().sender() == sender.address()), + Assert(payment.get().receiver() == Global.current_application_address()), + App.localPut( + sender.address(), + Bytes("balance"), + App.localGet(sender.address(), Bytes("balance")) + payment.get().amount(), + ), + ) + + +@router.method +def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: + """Lookup the balance of a user held by this app.""" + return output.set(App.localGet(user.address(), Bytes("balance"))) + + +@router.method +def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: + """Withdraw an amount of Algos held by this app. + + The sender of this method call will be the source of the Algos, and the destination will be + the `recipient` argument. This may or may not be the same as the sender's address. + + This method will fail if the amount of Algos requested to be withdrawn exceeds the amount of + Algos held by this app for the sender. + + The Algos will be transferred to the recipient using an inner transaction whose fee is set + to 0, meaning the caller's transaction must include a surplus fee to cover the inner + transaction. + """ + return Seq( + # if amount is larger than App.localGet(Txn.sender(), Bytes("balance")), the subtraction + # will underflow and fail this method call + App.localPut( + Txn.sender(), + Bytes("balance"), + App.localGet(Txn.sender(), Bytes("balance")) - amount.get(), + ), + InnerTxnBuilder.Begin(), + InnerTxnBuilder.SetFields( + { + TxnField.type_enum: TxnType.Payment, + TxnField.receiver: recipient.address(), + TxnField.amount: amount.get(), + TxnField.fee: Int(0), + } + ), + InnerTxnBuilder.Submit(), + ) + + +approval_program, clear_state_program, contract = router.compile_program( + version=6, optimize=OptimizeOptions(scratch_slots=True) +) + +if __name__ == "__main__": + with open("algobank_approval.teal", "w") as f: + f.write(approval_program) + + with open("algobank_clear_state.teal", "w") as f: + f.write(clear_state_program) + + with open("algobank.json", "w") as f: + f.write(json.dumps(contract.dictify(), indent=4)) diff --git a/examples/application/abi/algobank_approval.teal b/examples/application/abi/algobank_approval.teal new file mode 100644 index 000000000..be89cd0d3 --- /dev/null +++ b/examples/application/abi/algobank_approval.teal @@ -0,0 +1,226 @@ +#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l8 +txna ApplicationArgs 0 +method "deposit(pay,account)void" +== +bnz main_l7 +txna ApplicationArgs 0 +method "getBalance(account)uint64" +== +bnz main_l6 +txna ApplicationArgs 0 +method "withdraw(uint64,account)void" +== +bnz main_l5 +err +main_l5: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 3 +txna ApplicationArgs 2 +int 0 +getbyte +store 4 +load 3 +load 4 +callsub withdraw_3 +int 1 +return +main_l6: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +int 0 +getbyte +callsub getBalance_2 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return +main_l7: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 +!= +&& +|| +assert +txna ApplicationArgs 1 +int 0 +getbyte +store 1 +txn GroupIndex +int 1 +- +store 0 +load 0 +gtxns TypeEnum +int pay +== +assert +load 0 +load 1 +callsub deposit_1 +int 1 +return +main_l8: +txn OnCompletion +int NoOp +== +bnz main_l18 +txn OnCompletion +int OptIn +== +bnz main_l17 +txn OnCompletion +int CloseOut +== +bnz main_l16 +txn OnCompletion +int UpdateApplication +== +bnz main_l15 +txn OnCompletion +int DeleteApplication +== +bnz main_l14 +err +main_l14: +txn ApplicationID +int 0 +!= +assert +callsub assertsenderiscreator_0 +int 1 +return +main_l15: +txn ApplicationID +int 0 +!= +assert +callsub assertsenderiscreator_0 +int 1 +return +main_l16: +txn ApplicationID +int 0 +!= +assert +byte "lost" +byte "lost" +app_global_get +txn Sender +byte "balance" +app_local_get ++ +app_global_put +int 1 +return +main_l17: +int 1 +return +main_l18: +txn ApplicationID +int 0 +== +assert +int 1 +return + +// assert_sender_is_creator +assertsenderiscreator_0: +txn Sender +global CreatorAddress +== +assert +retsub + +// deposit +deposit_1: +store 6 +store 5 +load 5 +gtxns Sender +load 6 +txnas Accounts +== +assert +load 5 +gtxns Receiver +global CurrentApplicationAddress +== +assert +load 6 +txnas Accounts +byte "balance" +load 6 +txnas Accounts +byte "balance" +app_local_get +load 5 +gtxns Amount ++ +app_local_put +retsub + +// getBalance +getBalance_2: +txnas Accounts +byte "balance" +app_local_get +retsub + +// withdraw +withdraw_3: +store 8 +store 7 +txn Sender +byte "balance" +txn Sender +byte "balance" +app_local_get +load 7 +- +app_local_put +itxn_begin +int pay +itxn_field TypeEnum +load 8 +txnas Accounts +itxn_field Receiver +load 7 +itxn_field Amount +int 0 +itxn_field Fee +itxn_submit +retsub \ No newline at end of file diff --git a/examples/application/abi/algobank_clear_state.teal b/examples/application/abi/algobank_clear_state.teal new file mode 100644 index 000000000..a917c873b --- /dev/null +++ b/examples/application/abi/algobank_clear_state.teal @@ -0,0 +1,17 @@ +#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l2 +err +main_l2: +byte "lost" +byte "lost" +app_global_get +txn Sender +byte "balance" +app_local_get ++ +app_global_put +int 1 +return \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index 4d9be25af..728b3aef1 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,5 +1,8 @@ [mypy] +[mypy-semantic_version.*] +ignore_missing_imports = True + [mypy-pytest.*] ignore_missing_imports = True diff --git a/pyteal/__init__.py b/pyteal/__init__.py index ae30dd312..522d02fe1 100644 --- a/pyteal/__init__.py +++ b/pyteal/__init__.py @@ -1,5 +1,6 @@ from pyteal.ast import * from pyteal.ast import __all__ as ast_all +from pyteal.pragma import pragma from pyteal.ir import * from pyteal.ir import __all__ as ir_all from pyteal.compiler import ( @@ -16,8 +17,14 @@ TealTypeError, TealInputError, TealCompileError, + TealPragmaError, +) +from pyteal.config import ( + MAX_GROUP_SIZE, + NUM_SLOTS, + RETURN_HASH_PREFIX, + METHOD_ARG_NUM_CUTOFF, ) -from pyteal.config import MAX_GROUP_SIZE, NUM_SLOTS # begin __all__ __all__ = ( @@ -28,6 +35,7 @@ "MIN_TEAL_VERSION", "DEFAULT_TEAL_VERSION", "CompileOptions", + "pragma", "compileTeal", "OptimizeOptions", "TealType", @@ -35,8 +43,11 @@ "TealTypeError", "TealInputError", "TealCompileError", + "TealPragmaError", "MAX_GROUP_SIZE", "NUM_SLOTS", + "RETURN_HASH_PREFIX", + "METHOD_ARG_NUM_CUTOFF", ] ) # end __all__ diff --git a/pyteal/__init__.pyi b/pyteal/__init__.pyi index 4fa32d071..0ba3152df 100644 --- a/pyteal/__init__.pyi +++ b/pyteal/__init__.pyi @@ -3,6 +3,7 @@ from pyteal.ast import * from pyteal.ast import __all__ as ast_all +from pyteal.pragma import pragma from pyteal.ir import * from pyteal.ir import __all__ as ir_all from pyteal.compiler import ( @@ -19,24 +20,36 @@ from pyteal.errors import ( TealTypeError, TealInputError, TealCompileError, + TealPragmaError, +) +from pyteal.config import ( + MAX_GROUP_SIZE, + NUM_SLOTS, + RETURN_HASH_PREFIX, + METHOD_ARG_NUM_CUTOFF, ) -from pyteal.config import MAX_GROUP_SIZE, NUM_SLOTS __all__ = [ + "ABIReturnSubroutine", "AccountParam", + "AccountParamObject", "Add", "Addr", "And", "App", "AppField", "AppParam", + "AppParamObject", "Approve", "Arg", "Array", "Assert", "AssetHolding", + "AssetHoldingObject", "AssetParam", + "AssetParamObject", "Balance", + "BareCallActions", "BinaryExpr", "BitLen", "BitwiseAnd", @@ -63,6 +76,7 @@ __all__ = [ "BytesSqrt", "BytesXor", "BytesZero", + "CallConfig", "CompileOptions", "Concat", "Cond", @@ -116,8 +130,10 @@ __all__ = [ "Lt", "MAX_GROUP_SIZE", "MAX_TEAL_VERSION", + "METHOD_ARG_NUM_CUTOFF", "MIN_TEAL_VERSION", "MaybeValue", + "MethodConfig", "MethodSignature", "MinBalance", "Minus", @@ -131,14 +147,18 @@ __all__ = [ "Nonce", "Not", "OnComplete", + "OnCompleteAction", "Op", "OpUp", "OpUpMode", "OptimizeOptions", "Or", "Pop", + "Pragma", + "RETURN_HASH_PREFIX", "Reject", "Return", + "Router", "ScratchIndex", "ScratchLoad", "ScratchSlot", @@ -168,6 +188,7 @@ __all__ = [ "TealInternalError", "TealLabel", "TealOp", + "TealPragmaError", "TealSimpleBlock", "TealType", "TealTypeError", @@ -183,5 +204,7 @@ __all__ = [ "UnaryExpr", "While", "WideRatio", + "abi", "compileTeal", + "pragma", ] diff --git a/pyteal/ast/__init__.py b/pyteal/ast/__init__.py index 613a39480..956bab06b 100644 --- a/pyteal/ast/__init__.py +++ b/pyteal/ast/__init__.py @@ -24,9 +24,14 @@ from pyteal.ast.gitxn import Gitxn, GitxnExpr, GitxnaExpr, InnerTxnGroup from pyteal.ast.gload import ImportScratchValue from pyteal.ast.global_ import Global, GlobalField -from pyteal.ast.app import App, AppField, OnComplete, AppParam -from pyteal.ast.asset import AssetHolding, AssetParam -from pyteal.ast.acct import AccountParam +from pyteal.ast.app import App, AppField, OnComplete, AppParam, AppParamObject +from pyteal.ast.asset import ( + AssetHolding, + AssetHoldingObject, + AssetParam, + AssetParamObject, +) +from pyteal.ast.acct import AccountParam, AccountParamObject # inner txns from pyteal.ast.itxn import InnerTxnBuilder, InnerTxn, InnerTxnAction @@ -35,6 +40,7 @@ from pyteal.ast.array import Array from pyteal.ast.tmpl import Tmpl from pyteal.ast.nonce import Nonce +from pyteal.ast.pragma import Pragma # unary ops from pyteal.ast.unaryexpr import ( @@ -61,9 +67,7 @@ # binary ops from pyteal.ast.binaryexpr import ( BinaryExpr, - Add, Minus, - Mul, Div, Mod, Exp, @@ -104,7 +108,7 @@ from pyteal.ast.substring import Substring, Extract, Suffix # more ops -from pyteal.ast.naryexpr import NaryExpr, And, Or, Concat +from pyteal.ast.naryexpr import NaryExpr, Add, And, Mul, Or, Concat from pyteal.ast.widemath import WideRatio # control flow @@ -120,13 +124,13 @@ SubroutineDeclaration, SubroutineCall, SubroutineFnWrapper, + ABIReturnSubroutine, ) from pyteal.ast.while_ import While from pyteal.ast.for_ import For from pyteal.ast.break_ import Break from pyteal.ast.continue_ import Continue - # misc from pyteal.ast.scratch import ( ScratchIndex, @@ -140,6 +144,16 @@ from pyteal.ast.multi import MultiValue from pyteal.ast.opup import OpUp, OpUpMode from pyteal.ast.ecdsa import EcdsaCurve, EcdsaVerify, EcdsaDecompress, EcdsaRecover +from pyteal.ast.router import ( + Router, + CallConfig, + MethodConfig, + OnCompleteAction, + BareCallActions, +) + +# abi +import pyteal.ast.abi as abi # noqa: I250 __all__ = [ "Expr", @@ -169,9 +183,13 @@ "AppField", "OnComplete", "AppParam", + "AppParamObject", "AssetHolding", + "AssetHoldingObject", "AssetParam", + "AssetParamObject", "AccountParam", + "AccountParamObject", "InnerTxnBuilder", "InnerTxn", "InnerTxnAction", @@ -182,6 +200,7 @@ "Array", "Tmpl", "Nonce", + "Pragma", "UnaryExpr", "Btoi", "Itob", @@ -241,6 +260,7 @@ "SubroutineDeclaration", "SubroutineCall", "SubroutineFnWrapper", + "ABIReturnSubroutine", "ScratchIndex", "ScratchLoad", "ScratchSlot", @@ -277,6 +297,12 @@ "For", "Break", "Continue", + "Router", + "CallConfig", + "MethodConfig", + "OnCompleteAction", + "BareCallActions", + "abi", "EcdsaCurve", "EcdsaVerify", "EcdsaDecompress", diff --git a/pyteal/ast/abi/__init__.py b/pyteal/ast/abi/__init__.py new file mode 100644 index 000000000..e989f347b --- /dev/null +++ b/pyteal/ast/abi/__init__.py @@ -0,0 +1,148 @@ +from pyteal.ast.abi.string import String, StringTypeSpec +from pyteal.ast.abi.address import ( + AddressTypeSpec, + Address, + AddressLength, +) +from pyteal.ast.abi.type import TypeSpec, BaseType, ComputedValue, ReturnedValue +from pyteal.ast.abi.bool import BoolTypeSpec, Bool +from pyteal.ast.abi.uint import ( + UintTypeSpec, + Uint, + ByteTypeSpec, + Byte, + Uint8TypeSpec, + Uint8, + Uint16TypeSpec, + Uint16, + Uint32TypeSpec, + Uint32, + Uint64TypeSpec, + Uint64, +) +from pyteal.ast.abi.tuple import ( + TupleTypeSpec, + Tuple, + TupleElement, + Tuple0, + Tuple1, + Tuple2, + Tuple3, + Tuple4, + Tuple5, +) +from pyteal.ast.abi.array_base import ArrayTypeSpec, Array, ArrayElement +from pyteal.ast.abi.array_static import StaticArrayTypeSpec, StaticArray +from pyteal.ast.abi.array_dynamic import DynamicArrayTypeSpec, DynamicArray +from pyteal.ast.abi.reference_type import ( + ReferenceTypeSpec, + ReferenceType, + Account, + AccountTypeSpec, + Asset, + AssetTypeSpec, + Application, + ApplicationTypeSpec, + ReferenceTypeSpecs, +) +from pyteal.ast.abi.transaction import ( + Transaction, + TransactionTypeSpec, + PaymentTransaction, + PaymentTransactionTypeSpec, + ApplicationCallTransaction, + ApplicationCallTransactionTypeSpec, + AssetConfigTransaction, + AssetConfigTransactionTypeSpec, + AssetFreezeTransaction, + AssetFreezeTransactionTypeSpec, + AssetTransferTransaction, + AssetTransferTransactionTypeSpec, + KeyRegisterTransaction, + KeyRegisterTransactionTypeSpec, + TransactionTypeSpecs, +) +from pyteal.ast.abi.method_return import MethodReturn +from pyteal.ast.abi.util import ( + algosdk_from_annotation, + algosdk_from_type_spec, + make, + size_of, + type_spec_from_annotation, + contains_type_spec, +) + +__all__ = [ + "String", + "StringTypeSpec", + "Account", + "AccountTypeSpec", + "Asset", + "ReferenceTypeSpec", + "ReferenceType", + "AssetTypeSpec", + "Application", + "ApplicationTypeSpec", + "ReferenceType", + "ReferenceTypeSpec", + "ReferenceTypeSpecs", + "Address", + "AddressTypeSpec", + "AddressLength", + "TypeSpec", + "BaseType", + "ComputedValue", + "ReturnedValue", + "BoolTypeSpec", + "Bool", + "UintTypeSpec", + "Uint", + "ByteTypeSpec", + "Byte", + "Uint8TypeSpec", + "Uint8", + "Uint16TypeSpec", + "Uint16", + "Uint32TypeSpec", + "Uint32", + "Uint64TypeSpec", + "Uint64", + "TupleTypeSpec", + "Tuple", + "TupleElement", + "Tuple0", + "Tuple1", + "Tuple2", + "Tuple3", + "Tuple4", + "Tuple5", + "ArrayTypeSpec", + "Array", + "ArrayElement", + "StaticArrayTypeSpec", + "StaticArray", + "DynamicArrayTypeSpec", + "DynamicArray", + "MethodReturn", + "Transaction", + "TransactionTypeSpec", + "PaymentTransaction", + "PaymentTransactionTypeSpec", + "ApplicationCallTransaction", + "ApplicationCallTransactionTypeSpec", + "AssetConfigTransaction", + "AssetConfigTransactionTypeSpec", + "AssetFreezeTransaction", + "AssetFreezeTransactionTypeSpec", + "AssetTransferTransaction", + "AssetTransferTransactionTypeSpec", + "KeyRegisterTransaction", + "KeyRegisterTransactionTypeSpec", + "TransactionTypeSpecs", + "type_spec_from_annotation", + "make", + "size_of", + "algosdk_from_annotation", + "algosdk_from_type_spec", + "contains_type_spec", +] diff --git a/pyteal/ast/abi/address.py b/pyteal/ast/abi/address.py new file mode 100644 index 000000000..d0413cf6c --- /dev/null +++ b/pyteal/ast/abi/address.py @@ -0,0 +1,144 @@ +from enum import IntEnum +from typing import Union, Sequence, Literal, cast +from collections.abc import Sequence as CollectionSequence + +from pyteal.errors import TealInputError + +from pyteal.ast.assert_ import Assert +from pyteal.ast.bytes import Bytes +from pyteal.ast.int import Int +from pyteal.ast.seq import Seq +from pyteal.ast.unaryexpr import Len +from pyteal.ast.addr import Addr +from pyteal.ast.abi.type import ComputedValue, BaseType +from pyteal.ast.abi.array_static import StaticArray, StaticArrayTypeSpec +from pyteal.ast.abi.uint import ByteTypeSpec, Byte +from pyteal.ast.expr import Expr + + +class AddressLength(IntEnum): + String = 58 + Bytes = 32 + + +AddressLength.__module__ = "pyteal.abi" + + +class AddressTypeSpec(StaticArrayTypeSpec): + def __init__(self) -> None: + super().__init__(ByteTypeSpec(), AddressLength.Bytes) + + def new_instance(self) -> "Address": + return Address() + + def annotation_type(self) -> "type[Address]": + return Address + + def __str__(self) -> str: + return "address" + + def __eq__(self, other: object) -> bool: + return isinstance(other, AddressTypeSpec) + + +AddressTypeSpec.__module__ = "pyteal.abi" + + +class Address(StaticArray[Byte, Literal[AddressLength.Bytes]]): + def __init__(self) -> None: + super().__init__(AddressTypeSpec()) + + def type_spec(self) -> AddressTypeSpec: + return AddressTypeSpec() + + def get(self) -> Expr: + """Return the 32-byte value held by this Address as a PyTeal expression. + + The expression will have the type TealType.bytes. + """ + return self.stored_value.load() + + def set( + self, + value: Union[ + str, + bytes, + Expr, + Sequence[Byte], + StaticArray[Byte, Literal[AddressLength.Bytes]], + ComputedValue[StaticArray[Byte, Literal[AddressLength.Bytes]]], + "Address", + ComputedValue["Address"], + ], + ): + """Set the value of this Address to the input value. + + The behavior of this method depends on the input argument type: + + * :code:`str`: set the value to the address from the encoded address string. This string must be a valid 58-character base-32 Algorand address with checksum. + * :code:`bytes`: set the value to the raw address bytes. This byte string must have length 32. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.bytes. The program will fail if the evaluated byte string length is not 32. + * :code:`Sequence[Byte]`: set the bytes of this Address to those contained in this Python sequence (e.g. a list or tuple). A compiler error will occur if the sequence length is not 32. + * :code:`StaticArray[Byte, 32]`: copy the bytes from a StaticArray of 32 bytes. + * :code:`ComputedValue[StaticArray[Byte, 32]]`: copy the bytes from a StaticArray of 32 bytes produced by a ComputedValue. + * :code:`Address`: copy the value from another Address. + * :code:`ComputedValue[Address]`: copy the value from an Address produced by a ComputedValue. + + Args: + value: The new value this Address should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Address. + """ + + match value: + case ComputedValue(): + pts = value.produced_type_spec() + if pts == AddressTypeSpec() or pts == StaticArrayTypeSpec( + ByteTypeSpec(), AddressLength.Bytes + ): + return value.store_into(self) + + raise TealInputError( + f"Got ComputedValue with type spec {pts}, expected AddressTypeSpec or StaticArray[Byte, Literal[AddressLength.Bytes]]" + ) + case BaseType(): + if ( + value.type_spec() == AddressTypeSpec() + or value.type_spec() + == StaticArrayTypeSpec(ByteTypeSpec(), AddressLength.Bytes) + ): + return self.stored_value.store(value.stored_value.load()) + + raise TealInputError( + f"Got {value} with type spec {value.type_spec()}, expected AddressTypeSpec" + ) + case str(): + # Addr throws if value is invalid address + return self.stored_value.store(Addr(value)) + case bytes(): + if len(value) == AddressLength.Bytes: + return self.stored_value.store(Bytes(value)) + raise TealInputError( + f"Got bytes with length {len(value)}, expected {AddressLength.Bytes}" + ) + case Expr(): + return Seq( + self.stored_value.store(value), + Assert( + Len(self.stored_value.load()) == Int(AddressLength.Bytes.value) + ), + ) + case CollectionSequence(): + if len(value) != AddressLength.Bytes: + raise TealInputError( + f"Got bytes with length {len(value)}, expected {AddressLength.Bytes}" + ) + return super().set(cast(Sequence[Byte], value)) + + raise TealInputError( + f"Got {type(value)}, expected Sequence, StaticArray, ComputedValue, Address, str, bytes, Expr" + ) + + +Address.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/address_test.py b/pyteal/ast/abi/address_test.py new file mode 100644 index 000000000..7cc9cd449 --- /dev/null +++ b/pyteal/ast/abi/address_test.py @@ -0,0 +1,274 @@ +import pytest +import pyteal as pt +from pyteal import abi + +from pyteal.ast.abi.address import AddressLength +from pyteal.ast.abi.type_test import ContainerType +from pyteal.ast.abi.util import substring_for_decoding + + +options = pt.CompileOptions(version=5) + + +def test_AddressTypeSpec_str(): + assert str(abi.AddressTypeSpec()) == "address" + + +def test_AddressTypeSpec_is_dynamic(): + assert (abi.AddressTypeSpec()).is_dynamic() is False + + +def test_AddressTypeSpec_byte_length_static(): + assert (abi.AddressTypeSpec()).byte_length_static() == abi.AddressLength.Bytes + + +def test_AddressTypeSpec_length_static(): + assert (abi.AddressTypeSpec()).length_static() == abi.AddressLength.Bytes + + +def test_AddressTypeSpec_new_instance(): + assert isinstance(abi.AddressTypeSpec().new_instance(), abi.Address) + + +def test_AddressTypeSpec_eq(): + assert abi.AddressTypeSpec() == abi.AddressTypeSpec() + + for otherType in ( + abi.ByteTypeSpec(), + abi.StaticArrayTypeSpec(abi.ByteTypeSpec(), 32), + abi.DynamicArrayTypeSpec(abi.ByteTypeSpec()), + ): + assert abi.AddressTypeSpec() != otherType + + +def test_Address_encode(): + value = abi.Address() + expr = value.encode() + assert expr.type_of() == pt.TealType.bytes + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [pt.TealOp(expr, pt.Op.load, value.stored_value.slot)] + ) + actual, _ = expr.__teal__(options) + assert actual == expected + + +def test_Address_decode(): + address = bytes([0] * abi.AddressLength.Bytes) + encoded = pt.Bytes(address) + + for start_index in (None, pt.Int(0)): + for end_index in (None, pt.Int(1)): + for length in (None, pt.Int(2)): + value = abi.Address() + + if end_index is not None and length is not None: + with pytest.raises(pt.TealInputError): + value.decode( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + continue + + expr = value.decode( + encoded, start_index=start_index, end_index=end_index, length=length + ) + assert expr.type_of() == pt.TealType.none + assert expr.has_return() is False + + expectedExpr = value.stored_value.store( + substring_for_decoding( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + ) + 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 + + +def test_Address_get(): + value = abi.Address() + expr = value.get() + assert expr.type_of() == pt.TealType.bytes + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [pt.TealOp(expr, pt.Op.load, value.stored_value.slot)] + ) + actual, _ = expr.__teal__(options) + assert actual == expected + + +def test_Address_set_StaticArray(): + value_to_set = abi.StaticArray( + abi.StaticArrayTypeSpec(abi.ByteTypeSpec(), abi.AddressLength.Bytes) + ) + value = abi.Address() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, value_to_set.stored_value.slot), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + bogus = abi.StaticArray(abi.StaticArrayTypeSpec(abi.ByteTypeSpec(), 10)) + value.set(bogus) + + +def test_Address_set_str(): + for value_to_set in ("CEZZTYHNTVIZFZWT6X2R474Z2P3Q2DAZAKIRTPBAHL3LZ7W4O6VBROVRQA",): + value = abi.Address() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.addr, value_to_set), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(" " * 16) + + +def test_Address_set_bytes(): + for value_to_set in (bytes(32),): + value = abi.Address() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, f"0x{value_to_set.hex()}"), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(bytes(16)) + + with pytest.raises(pt.TealInputError): + value.set(16) + + +def test_Address_set_expr(): + for value_to_set in [pt.Global(pt.GlobalField.zero_address)]: + value = abi.Address() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + vts, _ = value_to_set.__teal__(options) + expected = pt.TealSimpleBlock( + [ + vts.ops[0], + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + pt.TealOp(None, pt.Op.load, value.stored_value.slot), + pt.TealOp(None, pt.Op.len), + pt.TealOp(None, pt.Op.int, AddressLength.Bytes.value), + pt.TealOp(None, pt.Op.eq), + pt.TealOp(None, pt.Op.assert_), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Address_set_copy(): + value = abi.Address() + other = abi.Address() + expr = value.set(other) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, other.stored_value.slot), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(abi.String()) + + +def test_Address_set_computed(): + av = pt.Addr("MDDKJUCTY57KA2PBFI44CLTJ5YHY5YVS4SVQUPZAWSRV2ZAVFKI33O6YPE") + computed_value = ContainerType(abi.AddressTypeSpec(), av) + + value = abi.Address() + expr = value.set(computed_value) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + _, byte_ops = av.__teal__(options) + expected = pt.TealSimpleBlock( + [ + byte_ops.ops[0], + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(ContainerType(abi.ByteTypeSpec(), pt.Int(0x01))) diff --git a/pyteal/ast/abi/array_base.py b/pyteal/ast/abi/array_base.py new file mode 100644 index 000000000..aea164c81 --- /dev/null +++ b/pyteal/ast/abi/array_base.py @@ -0,0 +1,286 @@ +from typing import ( + Union, + Sequence, + TypeVar, + Generic, + Final, + cast, +) +from abc import abstractmethod + +from pyteal.types import TealType, require_type +from pyteal.errors import TealInputError +from pyteal.ast.expr import Expr +from pyteal.ast.seq import Seq +from pyteal.ast.int import Int +from pyteal.ast.if_ import If +from pyteal.ast.unaryexpr import Len +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.uint import Uint16, Uint16TypeSpec +from pyteal.ast.abi.util import substring_for_decoding + +T = TypeVar("T", bound=BaseType) + + +class ArrayTypeSpec(TypeSpec, Generic[T]): + """The abstract base class for both static and dynamic array TypeSpecs.""" + + def __init__(self, value_type_spec: TypeSpec) -> None: + super().__init__() + self.value_spec: Final = value_type_spec + + def value_type_spec(self) -> TypeSpec: + """Get the TypeSpec of the value type this array can hold.""" + return self.value_spec + + def storage_type(self) -> TealType: + return TealType.bytes + + @abstractmethod + def is_length_dynamic(self) -> bool: + """Check if this array has a dynamic or static length.""" + pass + + def _stride(self) -> int: + """Get the "stride" of this array. + + The stride is defined as the byte length of each element in the array's encoded "head" + portion. + + If the underlying value type is static, then the stride is the static byte length of that + type. Otherwise, the stride is the static byte length of a Uint16 (2 bytes). + """ + if self.value_spec.is_dynamic(): + return Uint16TypeSpec().byte_length_static() + return self.value_spec.byte_length_static() + + +ArrayTypeSpec.__module__ = "pyteal.abi" + + +class Array(BaseType, Generic[T]): + """The abstract base class for both ABI static and dynamic array instances. + + This class contains basic implementations of ABI array methods, including: + * basic array elements setting method + * basic encoding and decoding of ABI array + * item retrieving by index (expression or integer) + """ + + def __init__(self, spec: ArrayTypeSpec) -> None: + super().__init__(spec) + + def type_spec(self) -> ArrayTypeSpec[T]: + return cast(ArrayTypeSpec, super().type_spec()) + + def decode( + self, + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None + ) -> Expr: + """Decode a substring of the passed in encoded byte string and set it as this type's value. + + Args: + encoded: An expression containing the bytes to decode. Must evaluate to TealType.bytes. + start_index (optional): An expression containing the index to start decoding. Must + evaluate to TealType.uint64. Defaults to None. + end_index (optional): An expression containing the index to stop decoding. Must evaluate + to TealType.uint64. Defaults to None. + length (optional): An expression containing the length of the substring to decode. Must + evaluate to TealType.uint64. Defaults to None. + + Returns: + An expression that partitions the needed parts from given byte strings and stores into + the scratch variable. + """ + extracted = substring_for_decoding( + encoded, start_index=start_index, end_index=end_index, length=length + ) + return self.stored_value.store(extracted) + + def set(self, values: Sequence[T]) -> Expr: + """Set the ABI array with a sequence of ABI type variables. + + The function first type-check the argument `values` to make sure the sequence of ABI type + variables before storing them to the underlying ScratchVar. If any of the input element does + not match expected array element type, error would be raised about type-mismatch. + + If static length of array is not available, this function would + * infer the array length from the sequence element number. + * store the inferred array length in uint16 format. + * concatenate the encoded array length at the beginning of array encoding. + + Args: + values: The sequence of ABI type variables to store in ABI array. + + Returns: + A PyTeal expression that stores encoded sequence of ABI values in its internal + ScratchVar. + """ + for index, value in enumerate(values): + if self.type_spec().value_type_spec() != value.type_spec(): + raise TealInputError( + "Cannot assign type {} at index {} to {}".format( + value.type_spec(), + index, + self.type_spec().value_type_spec(), + ) + ) + + encoded = _encode_tuple(values) + + if self.type_spec().is_length_dynamic(): + length_tmp = Uint16() + length_prefix = Seq(length_tmp.set(len(values)), length_tmp.encode()) + encoded = Concat(length_prefix, encoded) + + return self.stored_value.store(encoded) + + def encode(self) -> Expr: + """Encode the ABI array to be a byte string. + + Returns: + A PyTeal expression that encodes this ABI array to a byte string. + """ + return self.stored_value.load() + + @abstractmethod + def length(self) -> Expr: + """Get the element number of this ABI array. + + Returns: + A PyTeal expression that represents the array length. + """ + pass + + def __getitem__(self, index: Union[int, Expr]) -> "ArrayElement[T]": + """Retrieve an element by its index in this array. + + Indexes start at 0. + + Args: + index: either a Python integer or a PyTeal expression that evaluates to a TealType.uint64. + If a Python integer is used, this function will raise an error if its value is negative. + In either case, if the index is outside of the bounds of this array, the program will + fail at runtime. + + Returns: + An ArrayElement that corresponds to the element at the given index. This type is a ComputedValue. + """ + if type(index) is int: + if index < 0: + raise TealInputError("Index out of bounds: {}".format(index)) + index = Int(index) + return ArrayElement(self, cast(Expr, index)) + + +Array.__module__ = "pyteal.abi" + + +class ArrayElement(ComputedValue[T]): + """The class that represents an ABI array element. + + This class requires a reference to the array that the array element belongs to, and a PyTeal + expression (required to be TealType.uint64) which contains the array index. + """ + + def __init__(self, array: Array[T], index: Expr) -> None: + """Creates a new ArrayElement. + + Args: + array: The ABI array that the array element belongs to. + index: A PyTeal expression (required to be TealType.uint64) stands for array index. + """ + super().__init__() + require_type(index, TealType.uint64) + self.array = array + self.index = index + + 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(): + raise TealInputError("Output type does not match value type") + + 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 output.type_spec() == BoolTypeSpec(): + bitIndex = self.index + if arrayType.is_dynamic(): + bitIndex = bitIndex + Int(Uint16TypeSpec().bit_size()) + return cast(Bool, output).decode_bit(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 output.decode( + 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) + + +ArrayElement.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/array_base_test.py b/pyteal/ast/abi/array_base_test.py new file mode 100644 index 000000000..74dfa74ec --- /dev/null +++ b/pyteal/ast/abi/array_base_test.py @@ -0,0 +1,163 @@ +from typing import List, cast +import pytest + +import pyteal as pt +from pyteal import abi + +options = pt.CompileOptions(version=5) + +STATIC_TYPES: List[abi.TypeSpec] = [ + abi.BoolTypeSpec(), + abi.Uint8TypeSpec(), + abi.Uint16TypeSpec(), + abi.Uint32TypeSpec(), + abi.Uint64TypeSpec(), + abi.TupleTypeSpec(), + abi.TupleTypeSpec(abi.BoolTypeSpec(), abi.BoolTypeSpec(), abi.Uint64TypeSpec()), + abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 10), + abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 10), + abi.StaticArrayTypeSpec(abi.Uint16TypeSpec(), 10), + abi.StaticArrayTypeSpec(abi.Uint32TypeSpec(), 10), + abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10), + abi.StaticArrayTypeSpec( + abi.TupleTypeSpec(abi.BoolTypeSpec(), abi.BoolTypeSpec(), abi.Uint64TypeSpec()), + 10, + ), +] + +DYNAMIC_TYPES: List[abi.TypeSpec] = [ + abi.DynamicArrayTypeSpec(abi.BoolTypeSpec()), + abi.DynamicArrayTypeSpec(abi.Uint8TypeSpec()), + abi.DynamicArrayTypeSpec(abi.Uint16TypeSpec()), + abi.DynamicArrayTypeSpec(abi.Uint32TypeSpec()), + abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()), + abi.DynamicArrayTypeSpec(abi.TupleTypeSpec()), + abi.DynamicArrayTypeSpec( + abi.TupleTypeSpec(abi.BoolTypeSpec(), abi.BoolTypeSpec(), abi.Uint64TypeSpec()) + ), + abi.DynamicArrayTypeSpec(abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 10)), + abi.DynamicArrayTypeSpec(abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 10)), + abi.DynamicArrayTypeSpec(abi.StaticArrayTypeSpec(abi.Uint16TypeSpec(), 10)), + abi.DynamicArrayTypeSpec(abi.StaticArrayTypeSpec(abi.Uint32TypeSpec(), 10)), + abi.DynamicArrayTypeSpec(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10)), + abi.DynamicArrayTypeSpec( + abi.StaticArrayTypeSpec( + abi.TupleTypeSpec( + abi.BoolTypeSpec(), abi.BoolTypeSpec(), abi.Uint64TypeSpec() + ), + 10, + ) + ), +] + + +def test_ArrayElement_init(): + dynamicArrayType = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + array = dynamicArrayType.new_instance() + index = pt.Int(6) + + element = abi.ArrayElement(array, index) + assert element.array is array + assert element.index is index + + with pytest.raises(pt.TealTypeError): + abi.ArrayElement(array, pt.Bytes("abc")) + + with pytest.raises(pt.TealTypeError): + abi.ArrayElement(array, pt.Assert(index)) + + +def test_ArrayElement_store_into(): + 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) + output = elementType.new_instance() + expr = element.store_into(output) + + encoded = staticArray.encode() + stride = pt.Int(staticArray.type_spec()._stride()) + expectedLength = staticArray.length() + if elementType == abi.BoolTypeSpec(): + expectedExpr = cast(abi.Bool, output).decode_bit(encoded, index) + elif not elementType.is_dynamic(): + expectedExpr = output.decode( + encoded, start_index=stride * index, length=stride + ) + else: + expectedExpr = output.decode( + 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) + output = elementType.new_instance() + expr = element.store_into(output) + + encoded = dynamicArray.encode() + stride = pt.Int(dynamicArray.type_spec()._stride()) + expectedLength = dynamicArray.length() + if elementType == abi.BoolTypeSpec(): + expectedExpr = cast(abi.Bool, output).decode_bit( + encoded, index + pt.Int(16) + ) + elif not elementType.is_dynamic(): + expectedExpr = output.decode( + encoded, start_index=stride * index + pt.Int(2), length=stride + ) + else: + expectedExpr = output.decode( + 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))) diff --git a/pyteal/ast/abi/array_dynamic.py b/pyteal/ast/abi/array_dynamic.py new file mode 100644 index 000000000..e5aec04af --- /dev/null +++ b/pyteal/ast/abi/array_dynamic.py @@ -0,0 +1,104 @@ +from typing import ( + Union, + Sequence, + TypeVar, + cast, +) + + +from pyteal.errors import TealInputError +from pyteal.ast.expr import Expr +from pyteal.ast.seq import Seq + +from pyteal.ast.abi.type import ComputedValue, BaseType +from pyteal.ast.abi.uint import Uint16 +from pyteal.ast.abi.array_base import ArrayTypeSpec, Array + + +T = TypeVar("T", bound=BaseType) + + +class DynamicArrayTypeSpec(ArrayTypeSpec[T]): + def new_instance(self) -> "DynamicArray[T]": + return DynamicArray(self) + + def annotation_type(self) -> "type[DynamicArray[T]]": + return DynamicArray[self.value_type_spec().annotation_type()] # type: ignore[misc] + + def is_length_dynamic(self) -> bool: + return True + + def is_dynamic(self) -> bool: + return True + + def byte_length_static(self) -> int: + raise ValueError("Type is dynamic") + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, DynamicArrayTypeSpec) + and self.value_type_spec() == other.value_type_spec() + ) + + def __str__(self) -> str: + return f"{self.value_type_spec()}[]" + + +DynamicArrayTypeSpec.__module__ = "pyteal.abi" + + +class DynamicArray(Array[T]): + """The class that represents ABI dynamic array type.""" + + def __init__(self, array_type_spec: DynamicArrayTypeSpec[T]) -> None: + super().__init__(array_type_spec) + + def type_spec(self) -> DynamicArrayTypeSpec[T]: + return cast(DynamicArrayTypeSpec[T], super().type_spec()) + + def set( + self, + values: Union[Sequence[T], "DynamicArray[T]", ComputedValue["DynamicArray[T]"]], + ) -> Expr: + """ + Set the elements of this DynamicArray to the input values. + + The behavior of this method depends on the input argument type: + + * :code:`Sequence[T]`: set the elements of this DynamicArray to those contained in this Python sequence (e.g. a list or tuple). A compiler error will occur if any element in the sequence does not match this DynamicArray's element type. + * :code:`DynamicArray[T]`: copy the elements from another DynamicArray. The argument's element type must exactly match this DynamicArray's element type, otherwise an error will occur. + * :code:`ComputedValue[DynamicArray[T]]`: copy the elements from a DynamicArray produced by a ComputedValue. The element type produced by the ComputedValue must exactly match this DynamicArray's element type, otherwise an error will occur. + + Args: + values: The new elements this DynamicArray should have. This must follow the above constraints. + + Returns: + An expression which stores the given value into this DynamicArray. + """ + + if isinstance(values, ComputedValue): + return self._set_with_computed_type(values) + elif isinstance(values, BaseType): + if self.type_spec() != values.type_spec(): + raise TealInputError( + f"Cannot assign type {values.type_spec()} to {self.type_spec()}" + ) + return self.stored_value.store(values.encode()) + return super().set(values) + + def length(self) -> Expr: + """Get the element number of this ABI dynamic array. + + The array length (element number) is encoded in the first 2 bytes of the byte encoding. + + Returns: + A PyTeal expression that represents the dynamic array length. + """ + output = Uint16() + return Seq( + output.decode(self.encode()), + output.get(), + ) + + +DynamicArray.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/array_dynamic_test.py b/pyteal/ast/abi/array_dynamic_test.py new file mode 100644 index 000000000..d60fadf1a --- /dev/null +++ b/pyteal/ast/abi/array_dynamic_test.py @@ -0,0 +1,273 @@ +from typing import List +import pytest + +import pyteal as pt +from pyteal import abi +from pyteal.ast.abi.util import substring_for_decoding +from pyteal.ast.abi.tuple import _encode_tuple +from pyteal.ast.abi.array_base_test import STATIC_TYPES, DYNAMIC_TYPES +from pyteal.ast.abi.type_test import ContainerType + +options = pt.CompileOptions(version=5) + + +def test_DynamicArrayTypeSpec_init(): + for elementType in STATIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + assert dynamicArrayType.value_type_spec() is elementType + assert dynamicArrayType.is_length_dynamic() + assert dynamicArrayType._stride() == elementType.byte_length_static() + + for elementType in DYNAMIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + assert dynamicArrayType.value_type_spec() is elementType + assert dynamicArrayType.is_length_dynamic() + assert dynamicArrayType._stride() == 2 + + +def test_DynamicArrayTypeSpec_str(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + assert str(dynamicArrayType) == "{}[]".format(elementType) + + +def test_DynamicArrayTypeSpec_new_instance(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + instance = dynamicArrayType.new_instance() + assert isinstance(instance, abi.DynamicArray) + assert instance.type_spec() == dynamicArrayType + + +def test_DynamicArrayTypeSpec_eq(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + assert dynamicArrayType == dynamicArrayType + assert dynamicArrayType != abi.TupleTypeSpec(dynamicArrayType) + + +def test_DynamicArrayTypeSpec_is_dynamic(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + assert dynamicArrayType.is_dynamic() + + +def test_DynamicArrayTypeSpec_byte_length_static(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + dynamicArrayType = abi.DynamicArrayTypeSpec(elementType) + with pytest.raises(ValueError): + dynamicArrayType.byte_length_static() + + +def test_DynamicArray_decode(): + encoded = pt.Bytes("encoded") + dynamicArrayType = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + for start_index in (None, pt.Int(1)): + for end_index in (None, pt.Int(2)): + for length in (None, pt.Int(3)): + value = dynamicArrayType.new_instance() + + if end_index is not None and length is not None: + with pytest.raises(pt.TealInputError): + value.decode( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + continue + + expr = value.decode( + encoded, start_index=start_index, end_index=end_index, length=length + ) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expectedExpr = value.stored_value.store( + substring_for_decoding( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + ) + 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 + + +def test_DynamicArray_set_values(): + valuesToSet: List[abi.Uint64] = [ + [], + [abi.Uint64()], + [abi.Uint64() for _ in range(10)], + ] + + dynamicArrayType = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + for values in valuesToSet: + value = dynamicArrayType.new_instance() + expr = value.set(values) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + length_tmp = abi.Uint16() + expectedExpr = value.stored_value.store( + pt.Concat( + pt.Seq(length_tmp.set(len(values)), length_tmp.encode()), + _encode_tuple(values), + ) + ) + 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), + ) + + +def test_DynamicArray_set_copy(): + dynamicArrayType = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + value = dynamicArrayType.new_instance() + otherArray = dynamicArrayType.new_instance() + + with pytest.raises(pt.TealInputError): + value.set(abi.DynamicArray(abi.DynamicArrayTypeSpec(abi.Uint8TypeSpec()))) + + with pytest.raises(pt.TealInputError): + value.set(abi.Uint64()) + + expr = value.set(otherArray) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, otherArray.stored_value.slot), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_DynamicArray_set_computed(): + value = abi.DynamicArray(abi.DynamicArrayTypeSpec(abi.ByteTypeSpec())) + computed = ContainerType( + value.type_spec(), pt.Bytes("this should be a dynamic array") + ) + expr = value.set(computed) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, '"this should be a dynamic array"'), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = actual.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set( + ContainerType( + abi.DynamicArrayTypeSpec(abi.Uint16TypeSpec()), + pt.Bytes("well i am trolling again"), + ) + ) + + +def test_DynamicArray_encode(): + dynamicArrayType = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + value = dynamicArrayType.new_instance() + expr = value.encode() + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [pt.TealOp(None, pt.Op.load, value.stored_value.slot)] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_DynamicArray_length(): + dynamicArrayType = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + value = dynamicArrayType.new_instance() + expr = value.length() + assert expr.type_of() == pt.TealType.uint64 + assert not expr.has_return() + + length_tmp = abi.Uint16() + expectedExpr = pt.Seq(length_tmp.decode(value.encode()), length_tmp.get()) + 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), + ) + + +def test_DynamicArray_getitem(): + dynamicArrayType = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + value = dynamicArrayType.new_instance() + + for index in (0, 1, 2, 3, 1000): + # dynamic indexes + indexExpr = pt.Int(index) + element = value[indexExpr] + assert type(element) is abi.ArrayElement + assert element.array is value + assert element.index is indexExpr + + for index in (0, 1, 2, 3, 1000): + # static indexes + element = value[index] + assert type(element) is abi.ArrayElement + assert element.array is value + assert type(element.index) is pt.Int + assert element.index.value == index + + with pytest.raises(pt.TealInputError): + value[-1] diff --git a/pyteal/ast/abi/array_static.py b/pyteal/ast/abi/array_static.py new file mode 100644 index 000000000..f42ac627c --- /dev/null +++ b/pyteal/ast/abi/array_static.py @@ -0,0 +1,144 @@ +from typing import Final, Generic, Literal, Sequence, TypeVar, Union, cast + +from pyteal.errors import TealInputError +from pyteal.ast.expr import Expr +from pyteal.ast.int import Int + +from pyteal.ast.abi.type import ComputedValue, TypeSpec, BaseType +from pyteal.ast.abi.bool import BoolTypeSpec, _bool_sequence_length +from pyteal.ast.abi.array_base import ArrayTypeSpec, Array, ArrayElement + + +T = TypeVar("T", bound=BaseType) +N = TypeVar("N", bound=int) + + +class StaticArrayTypeSpec(ArrayTypeSpec[T], Generic[T, N]): + def __init__(self, value_type_spec: TypeSpec, array_length: int) -> None: + super().__init__(value_type_spec) + if not isinstance(array_length, int) or array_length < 0: + raise TypeError(f"Unsupported StaticArray length: {array_length}") + + # Casts to `int` to handle downstream usage where value is a subclass of int like `IntEnum`. + self.array_length: Final = int(array_length) + + def new_instance(self) -> "StaticArray[T, N]": + return StaticArray(self) + + def annotation_type(self) -> "type[StaticArray[T, N]]": + return StaticArray[ # type: ignore[misc] + self.value_spec.annotation_type(), Literal[self.array_length] # type: ignore + ] + + def length_static(self) -> int: + """Get the size of this static array type. + + Returns: + A Python integer that represents the static array length. + """ + return self.array_length + + def is_length_dynamic(self) -> bool: + return False + + def is_dynamic(self) -> bool: + return self.value_type_spec().is_dynamic() + + def byte_length_static(self) -> int: + if self.is_dynamic(): + raise ValueError("Type is dynamic") + + value_type = self.value_type_spec() + length = self.length_static() + + if value_type == BoolTypeSpec(): + return _bool_sequence_length(length) + return length * value_type.byte_length_static() + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, StaticArrayTypeSpec) + and self.value_type_spec() == other.value_type_spec() + and self.length_static() == other.length_static() + ) + + def __str__(self) -> str: + return f"{self.value_type_spec()}[{self.length_static()}]" + + +StaticArrayTypeSpec.__module__ = "pyteal.abi" + + +class StaticArray(Array[T], Generic[T, N]): + """The class that represents ABI static array type.""" + + def __init__(self, array_type_spec: StaticArrayTypeSpec[T, N]) -> None: + super().__init__(array_type_spec) + + def type_spec(self) -> StaticArrayTypeSpec[T, N]: + return cast(StaticArrayTypeSpec[T, N], super().type_spec()) + + def set( + self, + values: Union[ + Sequence[T], "StaticArray[T, N]", ComputedValue["StaticArray[T, N]"] + ], + ) -> Expr: + """Set the elements of this StaticArray to the input values. + + The behavior of this method depends on the input argument type: + + * :code:`Sequence[T]`: set the elements of this StaticArray to those contained in this Python sequence (e.g. a list or tuple). A compiler error will occur if any element in the sequence does not match this StaticArray's element type, or if the sequence length does not equal this StaticArray's length. + * :code:`StaticArray[T, N]`: copy the elements from another StaticArray. The argument's element type and length must exactly match this StaticArray's element type and length, otherwise an error will occur. + * :code:`ComputedValue[StaticArray[T, N]]`: copy the elements from a StaticArray produced by a ComputedValue. The element type and length produced by the ComputedValue must exactly match this StaticArray's element type and length, otherwise an error will occur. + + Args: + values: The new elements this StaticArray should have. This must follow the above constraints. + + Returns: + An expression which stores the given value into this StaticArray. + """ + if isinstance(values, ComputedValue): + return self._set_with_computed_type(values) + elif isinstance(values, BaseType): + if self.type_spec() != values.type_spec(): + raise TealInputError( + f"Cannot assign type {values.type_spec()} to {self.type_spec()}" + ) + return self.stored_value.store(values.encode()) + + if self.type_spec().length_static() != len(values): + raise TealInputError( + f"Incorrect length for values. Expected {self.type_spec()}, got {len(values)}" + ) + return super().set(values) + + def length(self) -> Expr: + """Get the element number of this ABI static array. + + Returns: + A PyTeal expression that represents the static array length. + """ + return Int(self.type_spec().length_static()) + + def __getitem__(self, index: Union[int, Expr]) -> "ArrayElement[T]": + """Retrieve an element by its index in this StaticArray. + + Indexes start at 0. + + Args: + index: either a Python integer or a PyTeal expression that evaluates to a TealType.uint64. + If a Python integer is used, this function will raise an error if its value is negative + or if the index is equal to or greater than the length of this StaticArray. If a PyTeal + expression is used, the program will fail at runtime if the index is outside of the + bounds of this StaticArray. + + Returns: + An ArrayElement that corresponds to the element at the given index. This type is a ComputedValue. + """ + if type(index) is int and index >= self.type_spec().length_static(): + raise TealInputError(f"Index out of bounds: {index}") + return super().__getitem__(index) + + +StaticArray.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/array_static_test.py b/pyteal/ast/abi/array_static_test.py new file mode 100644 index 000000000..7c3c7e95d --- /dev/null +++ b/pyteal/ast/abi/array_static_test.py @@ -0,0 +1,305 @@ +import pytest + +import pyteal as pt +from pyteal import abi +from pyteal.ast.abi.util import substring_for_decoding +from pyteal.ast.abi.tuple import _encode_tuple +from pyteal.ast.abi.bool import _bool_sequence_length +from pyteal.ast.abi.type_test import ContainerType +from pyteal.ast.abi.array_base_test import STATIC_TYPES, DYNAMIC_TYPES + +options = pt.CompileOptions(version=5) + + +def test_StaticArrayTypeSpec_init(): + for elementType in STATIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + assert staticArrayType.value_type_spec() is elementType + assert not staticArrayType.is_length_dynamic() + assert staticArrayType._stride() == elementType.byte_length_static() + assert staticArrayType.length_static() == length + + with pytest.raises(TypeError): + abi.StaticArrayTypeSpec(elementType, -1) + + for elementType in DYNAMIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + assert staticArrayType.value_type_spec() is elementType + assert not staticArrayType.is_length_dynamic() + assert staticArrayType._stride() == 2 + assert staticArrayType.length_static() == length + + with pytest.raises(TypeError): + abi.StaticArrayTypeSpec(elementType, -1) + + +def test_StaticArrayTypeSpec_str(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + assert str(staticArrayType) == "{}[{}]".format(elementType, length) + + +def test_StaticArrayTypeSpec_new_instance(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + instance = staticArrayType.new_instance() + assert isinstance( + instance, + abi.StaticArray, + ) + assert instance.type_spec() == staticArrayType + + +def test_StaticArrayTypeSpec_eq(): + for elementType in STATIC_TYPES + DYNAMIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + assert staticArrayType == staticArrayType + assert staticArrayType != abi.StaticArrayTypeSpec(elementType, length + 1) + assert staticArrayType != abi.StaticArrayTypeSpec( + abi.TupleTypeSpec(elementType), length + ) + + +def test_StaticArrayTypeSpec_is_dynamic(): + for elementType in STATIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + assert not staticArrayType.is_dynamic() + + for elementType in DYNAMIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + assert staticArrayType.is_dynamic() + + +def test_StaticArrayTypeSpec_byte_length_static(): + for elementType in STATIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + actual = staticArrayType.byte_length_static() + + if elementType == abi.BoolTypeSpec(): + expected = _bool_sequence_length(length) + else: + expected = elementType.byte_length_static() * length + + assert ( + actual == expected + ), "failed with element type {} and length {}".format(elementType, length) + + for elementType in DYNAMIC_TYPES: + for length in range(256): + staticArrayType = abi.StaticArrayTypeSpec(elementType, length) + with pytest.raises(ValueError): + staticArrayType.byte_length_static() + + +def test_StaticArray_decode(): + encoded = pt.Bytes("encoded") + for start_index in (None, pt.Int(1)): + for end_index in (None, pt.Int(2)): + for length in (None, pt.Int(3)): + value = abi.StaticArray( + abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10) + ) + + if end_index is not None and length is not None: + with pytest.raises(pt.TealInputError): + value.decode( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + continue + + expr = value.decode( + encoded, start_index=start_index, end_index=end_index, length=length + ) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expectedExpr = value.stored_value.store( + substring_for_decoding( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + ) + 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 + + +def test_StaticArray_set_values(): + value = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10)) + + with pytest.raises(pt.TealInputError): + value.set([]) + + with pytest.raises(pt.TealInputError): + value.set([abi.Uint64()] * 9) + + with pytest.raises(pt.TealInputError): + value.set([abi.Uint64()] * 11) + + with pytest.raises(pt.TealInputError): + value.set([abi.Uint16()] * 10) + + with pytest.raises(pt.TealInputError): + value.set([abi.Uint64()] * 9 + [abi.Uint16()]) + + values = [abi.Uint64() for _ in range(10)] + expr = value.set(values) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expectedExpr = value.stored_value.store(_encode_tuple(values)) + 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 + + +def test_StaticArray_set_copy(): + value = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10)) + otherArray = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10)) + + with pytest.raises(pt.TealInputError): + value.set(abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 11))) + + with pytest.raises(pt.TealInputError): + value.set(abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint8TypeSpec(), 10))) + + with pytest.raises(pt.TealInputError): + value.set(abi.Uint64()) + + expr = value.set(otherArray) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, otherArray.stored_value.slot), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_StaticArray_set_computed(): + value = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10)) + computed = ContainerType( + value.type_spec(), pt.Bytes("indeed this is hard to simulate") + ) + expr = value.set(computed) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, '"indeed this is hard to simulate"'), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = actual.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set( + ContainerType( + abi.StaticArrayTypeSpec(abi.Uint16TypeSpec(), 40), + pt.Bytes("well i am trolling"), + ) + ) + + +def test_StaticArray_encode(): + value = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), 10)) + expr = value.encode() + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [pt.TealOp(None, pt.Op.load, value.stored_value.slot)] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_StaticArray_length(): + for length in (0, 1, 2, 3, 1000): + value = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), length)) + expr = value.length() + assert expr.type_of() == pt.TealType.uint64 + assert not expr.has_return() + + expected = pt.TealSimpleBlock([pt.TealOp(None, pt.Op.int, length)]) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_StaticArray_getitem(): + for length in (0, 1, 2, 3, 1000): + value = abi.StaticArray(abi.StaticArrayTypeSpec(abi.Uint64TypeSpec(), length)) + + for index in range(length): + # dynamic indexes + indexExpr = pt.Int(index) + element = value[indexExpr] + assert type(element) is abi.ArrayElement + assert element.array is value + assert element.index is indexExpr + + for index in range(length): + # static indexes + element = value[index] + assert type(element) is abi.ArrayElement + assert element.array is value + assert type(element.index) is pt.Int + assert element.index.value == index + + with pytest.raises(pt.TealInputError): + value[-1] + + with pytest.raises(pt.TealInputError): + value[length] diff --git a/pyteal/ast/abi/bool.py b/pyteal/ast/abi/bool.py new file mode 100644 index 000000000..13552b6db --- /dev/null +++ b/pyteal/ast/abi/bool.py @@ -0,0 +1,181 @@ +from typing import TypeVar, Union, Sequence, Callable + +from pyteal.types import TealType +from pyteal.errors import TealInputError +from pyteal.ast.expr import Expr +from pyteal.ast.int import Int +from pyteal.ast.bytes import Bytes +from pyteal.ast.unaryexpr import Not +from pyteal.ast.binaryexpr import GetBit +from pyteal.ast.ternaryexpr import SetBit +from pyteal.ast.abi.type import ComputedValue, TypeSpec, BaseType +from pyteal.ast.abi.uint import NUM_BITS_IN_BYTE + + +class BoolTypeSpec(TypeSpec): + def new_instance(self) -> "Bool": + return Bool() + + def annotation_type(self) -> "type[Bool]": + return Bool + + def is_dynamic(self) -> bool: + # Only accurate if this value is alone, since up to 8 consecutive bools will fit into a single byte + return False + + def byte_length_static(self) -> int: + return 1 + + def storage_type(self) -> TealType: + return TealType.uint64 + + def __eq__(self, other: object) -> bool: + return isinstance(other, BoolTypeSpec) + + def __str__(self) -> str: + return "bool" + + +BoolTypeSpec.__module__ = "pyteal.abi" + + +class Bool(BaseType): + def __init__(self) -> None: + super().__init__(BoolTypeSpec()) + + def get(self) -> Expr: + """Return the value held by this Bool as a PyTeal expression. + + If the held value is true, an expression that evaluates to 1 will be returned. Otherwise, an + expression that evaluates to 0 will be returned. In either case, the expression will have the + type TealType.uint64. + """ + return self.stored_value.load() + + def set(self, value: Union[bool, Expr, "Bool", ComputedValue["Bool"]]) -> Expr: + """Set the value of this Bool to the input value. + + The behavior of this method depends on the input argument type: + + * :code:`bool`: set the value to a Python boolean value. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.uint64. All values greater than 0 are considered true, while 0 is considered false. + * :code:`Bool`: copy the value from another Bool. + * :code:`ComputedValue[Bool]`: copy the value from a Bool produced by a ComputedValue. + + Args: + value: The new value this Bool should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Bool. + """ + if isinstance(value, ComputedValue): + return self._set_with_computed_type(value) + + checked = False + if type(value) is bool: + value = Int(1 if value else 0) + checked = True + + if isinstance(value, BaseType): + if value.type_spec() != self.type_spec(): + raise TealInputError( + "Cannot set type bool to {}".format(value.type_spec()) + ) + value = value.get() + checked = True + + if checked: + return self.stored_value.store(value) + + # Not(Not(value)) coerces all values greater than 0 to 1 + return self.stored_value.store(Not(Not(value))) + + def decode( + self, + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None + ) -> Expr: + if start_index is None: + start_index = Int(0) + return self.decode_bit(encoded, start_index * Int(NUM_BITS_IN_BYTE)) + + def decode_bit(self, encoded, bit_index: Expr) -> Expr: + return self.stored_value.store(GetBit(encoded, bit_index)) + + def encode(self) -> Expr: + return SetBit(Bytes(b"\x00"), Int(0), self.get()) + + +Bool.__module__ = "pyteal.abi" + + +def _bool_aware_static_byte_length(types: Sequence[TypeSpec]) -> int: + length = 0 + ignoreNext = 0 + for i, t in enumerate(types): + if ignoreNext > 0: + ignoreNext -= 1 + continue + if t == BoolTypeSpec(): + numBools = _consecutive_bool_type_spec_num(types, i) + ignoreNext = numBools - 1 + length += _bool_sequence_length(numBools) + continue + length += t.byte_length_static() + return length + + +T = TypeVar("T") + + +def _consecutive_thing_num( + things: Sequence[T], start_index: int, condition: Callable[[T], bool] +) -> int: + numConsecutiveThings = 0 + for t in things[start_index:]: + if not condition(t): + break + numConsecutiveThings += 1 + return numConsecutiveThings + + +def _consecutive_bool_type_spec_num(types: Sequence[TypeSpec], start_index: int) -> int: + if len(types) != 0 and not isinstance(types[0], TypeSpec): + raise TypeError("Sequence of types expected") + return _consecutive_thing_num(types, start_index, lambda t: t == BoolTypeSpec()) + + +def _consecutive_bool_instance_num(values: Sequence[BaseType], start_index: int) -> int: + if len(values) != 0 and not isinstance(values[0], BaseType): + raise TypeError( + "Sequence of types expected, but got {}".format(type(values[0])) + ) + return _consecutive_thing_num( + values, start_index, lambda t: t.type_spec() == BoolTypeSpec() + ) + + +def _bool_sequence_length(num_bools: int) -> int: + """Get the length in bytes of an encoding of `num_bools` consecutive booleans values.""" + return (num_bools + NUM_BITS_IN_BYTE - 1) // NUM_BITS_IN_BYTE + + +def _encode_bool_sequence(values: Sequence[Bool]) -> Expr: + """Encoding a sequences of boolean values into a byte string. + + Args: + values: The values to encode. Each must be an instance of Bool. + + Returns: + An expression which creates an encoded byte string with the input boolean values. + """ + length = _bool_sequence_length(len(values)) + expr: Expr = Bytes(b"\x00" * length) + + for i, value in enumerate(values): + expr = SetBit(expr, Int(i), value.get()) + + return expr diff --git a/pyteal/ast/abi/bool_test.py b/pyteal/ast/abi/bool_test.py new file mode 100644 index 000000000..45ffa7e30 --- /dev/null +++ b/pyteal/ast/abi/bool_test.py @@ -0,0 +1,422 @@ +from typing import NamedTuple, List +import pytest + +import pyteal as pt +from pyteal import abi +from pyteal.ast.abi.type_test import ContainerType +from pyteal.ast.abi.bool import ( + _bool_aware_static_byte_length, + _consecutive_bool_instance_num, + _consecutive_bool_type_spec_num, + _bool_sequence_length, + _encode_bool_sequence, +) + +options = pt.CompileOptions(version=5) + + +def test_BoolTypeSpec_str(): + assert str(abi.BoolTypeSpec()) == "bool" + + +def test_BoolTypeSpec_is_dynamic(): + assert not abi.BoolTypeSpec().is_dynamic() + + +def test_BoolTypeSpec_byte_length_static(): + assert abi.BoolTypeSpec().byte_length_static() == 1 + + +def test_BoolTypeSpec_new_instance(): + assert isinstance(abi.BoolTypeSpec().new_instance(), abi.Bool) + + +def test_BoolTypeSpec_eq(): + assert abi.BoolTypeSpec() == abi.BoolTypeSpec() + + for otherType in ( + abi.ByteTypeSpec, + abi.Uint64TypeSpec, + abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 1), + abi.DynamicArrayTypeSpec(abi.BoolTypeSpec()), + ): + assert abi.BoolTypeSpec() != otherType + + +def test_Bool_set_static(): + value = abi.Bool() + for value_to_set in (True, False): + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 1 if value_to_set else 0), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Bool_set_expr(): + value = abi.Bool() + expr = value.set(pt.Int(0).Or(pt.Int(1))) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 0), + pt.TealOp(None, pt.Op.int, 1), + pt.TealOp(None, pt.Op.logic_or), + pt.TealOp(None, pt.Op.logic_not), + pt.TealOp(None, pt.Op.logic_not), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Bool_set_copy(): + other = abi.Bool() + value = abi.Bool() + expr = value.set(other) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, other.stored_value.slot), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(abi.Uint16()) + + +def test_Bool_set_computed(): + value = abi.Bool() + computed = ContainerType(abi.BoolTypeSpec(), pt.Int(0x80)) + expr = value.set(computed) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 0x80), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(ContainerType(abi.Uint32TypeSpec(), pt.Int(65537))) + + +def test_Bool_get(): + value = abi.Bool() + expr = value.get() + assert expr.type_of() == pt.TealType.uint64 + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [pt.TealOp(expr, pt.Op.load, value.stored_value.slot)] + ) + + actual, _ = expr.__teal__(options) + + assert actual == expected + + +def test_Bool_decode(): + value = abi.Bool() + encoded = pt.Bytes("encoded") + for start_index in (None, pt.Int(1)): + for end_index in (None, pt.Int(2)): + for length in (None, pt.Int(3)): + expr = value.decode( + encoded, start_index=start_index, end_index=end_index, length=length + ) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, '"encoded"'), + pt.TealOp(None, pt.Op.int, 0 if start_index is None else 1), + pt.TealOp(None, pt.Op.int, 8), + pt.TealOp(None, pt.Op.mul), + pt.TealOp(None, pt.Op.getbit), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Bool_decode_bit(): + value = abi.Bool() + bitIndex = pt.Int(17) + encoded = pt.Bytes("encoded") + expr = value.decode_bit(encoded, bitIndex) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, '"encoded"'), + pt.TealOp(None, pt.Op.int, 17), + pt.TealOp(None, pt.Op.getbit), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Bool_encode(): + value = abi.Bool() + expr = value.encode() + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, "0x00"), + pt.TealOp(None, pt.Op.int, 0), + pt.TealOp(None, pt.Op.load, value.stored_value.slot), + pt.TealOp(None, pt.Op.setbit), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_boolAwareStaticByteLength(): + class ByteLengthTest(NamedTuple): + types: List[abi.TypeSpec] + expectedLength: int + + tests: List[ByteLengthTest] = [ + ByteLengthTest(types=[], expectedLength=0), + ByteLengthTest(types=[abi.Uint64TypeSpec()], expectedLength=8), + ByteLengthTest(types=[abi.BoolTypeSpec()], expectedLength=1), + ByteLengthTest(types=[abi.BoolTypeSpec()] * 8, expectedLength=1), + ByteLengthTest(types=[abi.BoolTypeSpec()] * 9, expectedLength=2), + ByteLengthTest(types=[abi.BoolTypeSpec()] * 16, expectedLength=2), + ByteLengthTest(types=[abi.BoolTypeSpec()] * 17, expectedLength=3), + ByteLengthTest(types=[abi.BoolTypeSpec()] * 100, expectedLength=13), + ByteLengthTest( + types=[abi.BoolTypeSpec(), abi.ByteTypeSpec(), abi.BoolTypeSpec()], + expectedLength=3, + ), + ByteLengthTest( + types=[ + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + ], + expectedLength=3, + ), + ByteLengthTest( + types=[abi.BoolTypeSpec()] * 16 + + [abi.ByteTypeSpec(), abi.BoolTypeSpec(), abi.BoolTypeSpec()], + expectedLength=4, + ), + ] + + for i, test in enumerate(tests): + actual = _bool_aware_static_byte_length(test.types) + assert actual == test.expectedLength, "Test at index {} failed".format(i) + + +def test_consecutiveBool(): + class ConsecutiveTest(NamedTuple): + types: List[abi.TypeSpec] + start: int + expected: int + + tests: List[ConsecutiveTest] = [ + ConsecutiveTest(types=[], start=0, expected=0), + ConsecutiveTest(types=[abi.Uint16TypeSpec()], start=0, expected=0), + ConsecutiveTest(types=[abi.BoolTypeSpec()], start=0, expected=1), + ConsecutiveTest(types=[abi.BoolTypeSpec()], start=1, expected=0), + ConsecutiveTest( + types=[abi.BoolTypeSpec(), abi.BoolTypeSpec()], start=0, expected=2 + ), + ConsecutiveTest( + types=[abi.BoolTypeSpec(), abi.BoolTypeSpec()], start=1, expected=1 + ), + ConsecutiveTest( + types=[abi.BoolTypeSpec(), abi.BoolTypeSpec()], start=2, expected=0 + ), + ConsecutiveTest( + types=[abi.BoolTypeSpec() for _ in range(10)], start=0, expected=10 + ), + ConsecutiveTest( + types=[ + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + ], + start=0, + expected=2, + ), + ConsecutiveTest( + types=[ + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + ], + start=2, + expected=0, + ), + ConsecutiveTest( + types=[ + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + ], + start=3, + expected=1, + ), + ConsecutiveTest( + types=[ + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.ByteTypeSpec(), + ], + start=0, + expected=0, + ), + ConsecutiveTest( + types=[ + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.ByteTypeSpec(), + ], + start=1, + expected=2, + ), + ] + + for i, test in enumerate(tests): + actual = _consecutive_bool_type_spec_num(test.types, test.start) + assert actual == test.expected, "Test at index {} failed".format(i) + + actual = _consecutive_bool_instance_num( + [t.new_instance() for t in test.types], test.start + ) + assert actual == test.expected, "Test at index {} failed".format(i) + + +def test_boolSequenceLength(): + class SeqLengthTest(NamedTuple): + numBools: int + expectedLength: int + + tests: List[SeqLengthTest] = [ + SeqLengthTest(numBools=0, expectedLength=0), + SeqLengthTest(numBools=1, expectedLength=1), + SeqLengthTest(numBools=8, expectedLength=1), + SeqLengthTest(numBools=9, expectedLength=2), + SeqLengthTest(numBools=16, expectedLength=2), + SeqLengthTest(numBools=17, expectedLength=3), + SeqLengthTest(numBools=100, expectedLength=13), + ] + + for i, test in enumerate(tests): + actual = _bool_sequence_length(test.numBools) + assert actual == test.expectedLength, "Test at index {} failed".format(i) + + +def test_encodeBoolSequence(): + class EncodeSeqTest(NamedTuple): + types: List[abi.Bool] + expectedLength: int + + tests: List[EncodeSeqTest] = [ + EncodeSeqTest(types=[], expectedLength=0), + EncodeSeqTest(types=[abi.Bool()], expectedLength=1), + EncodeSeqTest(types=[abi.Bool() for _ in range(4)], expectedLength=1), + EncodeSeqTest(types=[abi.Bool() for _ in range(8)], expectedLength=1), + EncodeSeqTest(types=[abi.Bool() for _ in range(9)], expectedLength=2), + EncodeSeqTest(types=[abi.Bool() for _ in range(100)], expectedLength=13), + ] + + for i, test in enumerate(tests): + expr = _encode_bool_sequence(test.types) + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + setBits = [ + [ + pt.TealOp(None, pt.Op.int, j), + pt.TealOp(None, pt.Op.load, testType.stored_value.slot), + pt.TealOp(None, pt.Op.setbit), + ] + for j, testType in enumerate(test.types) + ] + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, "0x" + ("00" * test.expectedLength)), + ] + + [expr for setBit in setBits for expr in setBit] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected, "Test at index {} failed".format(i) diff --git a/pyteal/ast/abi/method_return.py b/pyteal/ast/abi/method_return.py new file mode 100644 index 000000000..24566b5bd --- /dev/null +++ b/pyteal/ast/abi/method_return.py @@ -0,0 +1,42 @@ +from typing import TYPE_CHECKING, Tuple +from pyteal.ast.abi import BaseType +from pyteal.types import TealType +from pyteal.errors import TealInputError +from pyteal.ast.expr import Expr +from pyteal.ast.unaryexpr import Log +from pyteal.ast.naryexpr import Concat +from pyteal.ast.bytes import Bytes +from pyteal.ir import TealBlock, TealSimpleBlock, Op +from pyteal.config import RETURN_HASH_PREFIX + +if TYPE_CHECKING: + from pyteal.compiler import CompileOptions + + +class MethodReturn(Expr): + def __init__(self, arg: BaseType): + super().__init__() + if not isinstance(arg, BaseType): + raise TealInputError(f"Expecting an ABI type argument but get {arg}") + self.arg = arg + + def __teal__(self, options: "CompileOptions") -> Tuple[TealBlock, TealSimpleBlock]: + if options.version < Op.log.min_version: + raise TealInputError( + f"current version {options.version} is lower than log's min version {Op.log.min_version}" + ) + return Log(Concat(Bytes(RETURN_HASH_PREFIX), self.arg.encode())).__teal__( + options + ) + + def __str__(self) -> str: + return f"(MethodReturn {self.arg.type_spec()})" + + def type_of(self) -> TealType: + return TealType.none + + def has_return(self) -> bool: + return False + + +MethodReturn.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/method_return_test.py b/pyteal/ast/abi/method_return_test.py new file mode 100644 index 000000000..00657b6be --- /dev/null +++ b/pyteal/ast/abi/method_return_test.py @@ -0,0 +1,33 @@ +import pytest + +import pyteal as pt +from pyteal import abi + + +POSITIVE_CASES = [ + abi.Uint16(), + abi.Uint32(), + abi.StaticArray(abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 12)), +] + + +@pytest.mark.parametrize("case", POSITIVE_CASES) +def test_method_return(case): + m_ret = abi.MethodReturn(case) + assert m_ret.type_of() == pt.TealType.none + assert not m_ret.has_return() + assert str(m_ret) == f"(MethodReturn {case.type_spec()})" + + +NEGATIVE_CASES = [ + pt.Int(0), + pt.Bytes("aaaaaaa"), + abi.Uint16, + abi.Uint32, +] + + +@pytest.mark.parametrize("case", NEGATIVE_CASES) +def test_method_return_error(case): + with pytest.raises(pt.TealInputError): + abi.MethodReturn(case) diff --git a/pyteal/ast/abi/reference_type.py b/pyteal/ast/abi/reference_type.py new file mode 100644 index 000000000..8ee17ac2f --- /dev/null +++ b/pyteal/ast/abi/reference_type.py @@ -0,0 +1,222 @@ +from typing import List, Final, TypeVar, cast +from abc import abstractmethod +from pyteal.ast.abi.type import BaseType, TypeSpec +from pyteal.ast.abi.uint import NUM_BITS_IN_BYTE, uint_decode + +from pyteal.ast.expr import Expr +from pyteal.ast.txn import Txn +from pyteal.ast.acct import AccountParamObject +from pyteal.ast.asset import AssetHoldingObject, AssetParamObject +from pyteal.ast.app import AppParamObject +from pyteal.errors import TealInputError +from pyteal.types import TealType + + +T = TypeVar("T", bound="ReferenceType") + + +class ReferenceTypeSpec(TypeSpec): + @abstractmethod + def new_instance(self) -> "ReferenceType": + pass + + @abstractmethod + def annotation_type(self) -> "type[ReferenceType]": + pass + + def bit_size(self) -> int: + """Get the bit size of the index this reference type holds""" + return NUM_BITS_IN_BYTE + + def is_dynamic(self) -> bool: + return False + + def byte_length_static(self) -> int: + return 1 + + def storage_type(self) -> TealType: + return TealType.uint64 + + +ReferenceTypeSpec.__module__ = "pyteal.abi" + + +class ReferenceType(BaseType): + @abstractmethod + def __init__(self, spec: ReferenceTypeSpec) -> None: + super().__init__(spec) + + def type_spec(self) -> ReferenceTypeSpec: + return cast(ReferenceTypeSpec, super().type_spec()) + + def referenced_index(self) -> Expr: + """Get the reference index for this value. + + The three reference types (account, application, asset) contain indexes into a foreign array + of the transaction. This method returns that index. + + If this reference type is an application or asset, note that this DOES NOT return the + application or asset ID. See :code:`application_id()` or :code:`asset_id()` for that. + """ + return self.stored_value.load() + + def decode( + self, + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None, + ) -> Expr: + return uint_decode( + self.type_spec().bit_size(), + self.stored_value, + encoded, + start_index, + end_index, + length, + ) + + def encode(self) -> Expr: + raise TealInputError("A ReferenceType cannot be encoded") + + +ReferenceType.__module__ = "pyteal.abi" + + +class AccountTypeSpec(ReferenceTypeSpec): + def new_instance(self) -> "Account": + return Account() + + def annotation_type(self) -> "type[Account]": + return Account + + def __str__(self) -> str: + return "account" + + def __eq__(self, other: object) -> bool: + return isinstance(other, AccountTypeSpec) + + +AccountTypeSpec.__module__ = "pyteal.abi" + + +class Account(ReferenceType): + def __init__(self) -> None: + super().__init__(AccountTypeSpec()) + + def address(self) -> Expr: + """Get the address of the account.""" + return Txn.accounts[self.stored_value.load()] + + def params(self) -> AccountParamObject: + """Get information about the account.""" + return AccountParamObject(self.referenced_index()) + + def asset_holding(self, asset: "Expr | Asset") -> AssetHoldingObject: + """Get information about an asset held by this account. + + Args: + asset: An identifier for the asset. It must be one of the following: an abi.Asset + reference object, an expression holding an index into Txn.ForeignAssets that + corresponds to the asset (in which case it must evaluate to uint64), or since v4, an + expression holding an asset ID that appears in Txn.ForeignAssets (in which case it + must evaluate to uint64). + """ + if isinstance(asset, Asset): + asset_ref = asset.referenced_index() + else: + asset_ref = asset + return AssetHoldingObject(asset_ref, self.referenced_index()) + + +Account.__module__ = "pyteal.abi" + + +class AssetTypeSpec(ReferenceTypeSpec): + def new_instance(self) -> "Asset": + return Asset() + + def annotation_type(self) -> "type[Asset]": + return Asset + + def __str__(self) -> str: + return "asset" + + def __eq__(self, other: object) -> bool: + return isinstance(other, AssetTypeSpec) + + +AssetTypeSpec.__module__ = "pyteal.abi" + + +class Asset(ReferenceType): + def __init__(self) -> None: + super().__init__(AssetTypeSpec()) + + def asset_id(self) -> Expr: + """Get the ID of the asset.""" + return Txn.assets[self.referenced_index()] + + def holding(self, account: Expr | Account) -> AssetHoldingObject: + """Get information about this asset held by an account. + + Args: + account: An identifier for the account. It must be one of the following: an abi.Account + reference object, an expression holding an index into Txn.Accounts that corresponds + to the account (in which case it must evaluate to uint64), or since v4, an + expression holding an account address that appears in Txn.Accounts or is Txn.Sender + (in which case it must evaluate to bytes). + """ + if isinstance(account, Account): + account_ref = account.referenced_index() + else: + account_ref = account + return AssetHoldingObject(self.referenced_index(), account_ref) + + def params(self) -> AssetParamObject: + """Get information about the asset's parameters.""" + return AssetParamObject(self.referenced_index()) + + +Asset.__module__ = "pyteal.abi" + + +class ApplicationTypeSpec(ReferenceTypeSpec): + def new_instance(self) -> "Application": + return Application() + + def annotation_type(self) -> "type[Application]": + return Application + + def __str__(self) -> str: + return "application" + + def __eq__(self, other: object) -> bool: + return isinstance(other, ApplicationTypeSpec) + + +ApplicationTypeSpec.__module__ = "pyteal.abi" + + +class Application(ReferenceType): + def __init__(self) -> None: + super().__init__(ApplicationTypeSpec()) + + def application_id(self) -> Expr: + """Get the ID of the application.""" + return Txn.applications[self.stored_value.load()] + + def params(self) -> AppParamObject: + """Get information about the application's parameters.""" + return AppParamObject(self.referenced_index()) + + +Application.__module__ = "pyteal.abi" + + +ReferenceTypeSpecs: Final[List[TypeSpec]] = [ + AccountTypeSpec(), + AssetTypeSpec(), + ApplicationTypeSpec(), +] diff --git a/pyteal/ast/abi/reference_type_test.py b/pyteal/ast/abi/reference_type_test.py new file mode 100644 index 000000000..99555ab12 --- /dev/null +++ b/pyteal/ast/abi/reference_type_test.py @@ -0,0 +1,308 @@ +import pytest + +import pyteal as pt +from pyteal import abi + +options = pt.CompileOptions(version=5) + + +def test_ReferenceTypeSpecs_list(): + assert abi.ReferenceTypeSpecs == [ + abi.AccountTypeSpec(), + abi.AssetTypeSpec(), + abi.ApplicationTypeSpec(), + ] + + +def test_ReferenceType_referenced_index(): + for value in (abi.Account(), abi.Asset(), abi.Application()): + expr = value.referenced_index() + assert expr.type_of() == pt.TealType.uint64 + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(expr, pt.Op.load, value.stored_value.slot), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_ReferenceType_encode(): + for value in (abi.Account(), abi.Asset(), abi.Application()): + with pytest.raises( + pt.TealInputError, match=r"A ReferenceType cannot be encoded$" + ): + value.encode() + + +def test_ReferenceType_decode(): + encoded = pt.Bytes("encoded") + for value in (abi.Account(), abi.Asset(), abi.Application()): + for start_index in (None, pt.Int(1)): + for end_index in (None, pt.Int(2)): + for length in (None, pt.Int(3)): + expr = value.decode( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + assert expr.type_of() == pt.TealType.none + assert expr.has_return() is False + + expected_decoding = value.stored_value.store( + pt.GetByte( + encoded, + start_index if start_index is not None else pt.Int(0), + ) + ) + expected, _ = expected_decoding.__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 + + +def test_Account_str(): + assert str(abi.AccountTypeSpec()) == "account" + + +def test_AccountTypeSpec_is_dynamic(): + assert not (abi.AccountTypeSpec()).is_dynamic() + + +def test_AccountTypeSpec_new_instance(): + assert isinstance(abi.AccountTypeSpec().new_instance(), abi.Account) + + +def test_AccountTypeSpec_eq(): + assert abi.AccountTypeSpec() == abi.AccountTypeSpec() + + for otherType in ( + abi.ByteTypeSpec(), + abi.Uint8TypeSpec(), + abi.AddressTypeSpec(), + ): + assert abi.AccountTypeSpec() != otherType + + +def test_Account_typespec(): + assert abi.Account().type_spec() == abi.AccountTypeSpec() + + +def test_Account_address(): + value = abi.Account() + expr = value.address() + assert expr.type_of() == pt.TealType.bytes + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, value.stored_value.slot), + pt.TealOp(None, pt.Op.txnas, "Accounts"), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Account_params(): + value = abi.Account() + + params = value.params() + + assert type(params) is pt.AccountParamObject + + expected = value.referenced_index() + actual = params._account + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual.__teal__(options) == expected.__teal__(options) + + +def test_Account_asset_holding(): + value = abi.Account() + + assets = ((pt.Int(6), pt.Int(6)), (a := abi.Asset(), a.referenced_index())) + + for asset, expected_asset in assets: + holding = value.asset_holding(asset) + + assert type(holding) is pt.AssetHoldingObject + + expected_account = value.referenced_index() + actual_account = holding._account + + actual_asset = holding._asset + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual_account.__teal__(options) == expected_account.__teal__( + options + ) + assert actual_asset.__teal__(options) == expected_asset.__teal__(options) + + +def test_Asset_str(): + assert str(abi.AssetTypeSpec()) == "asset" + + +def test_AssetTypeSpec_is_dynamic(): + assert not (abi.AssetTypeSpec()).is_dynamic() + + +def test_AssetTypeSpec_new_instance(): + assert isinstance(abi.AssetTypeSpec().new_instance(), abi.Asset) + + +def test_AssetTypeSpec_eq(): + assert abi.AssetTypeSpec() == abi.AssetTypeSpec() + + for otherType in ( + abi.ByteTypeSpec(), + abi.Uint8TypeSpec(), + abi.AddressTypeSpec(), + ): + assert abi.AssetTypeSpec() != otherType + + +def test_Asset_typespec(): + assert abi.Asset().type_spec() == abi.AssetTypeSpec() + + +def test_Asset_asset_id(): + value = abi.Asset() + expr = value.asset_id() + assert expr.type_of() == pt.TealType.uint64 + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, value.stored_value.slot), + pt.TealOp(None, pt.Op.txnas, "Assets"), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Asset_holding(): + value = abi.Asset() + + accounts = ( + (pt.Int(6), pt.Int(6)), + ( + pt.Addr("QSA6K5MNJPEGO5SDSWXBM3K4UEI3Q2NCPS2OUXVJI5QPCHMVI27MFRSHKI"), + pt.Addr("QSA6K5MNJPEGO5SDSWXBM3K4UEI3Q2NCPS2OUXVJI5QPCHMVI27MFRSHKI"), + ), + (a := abi.Account(), a.referenced_index()), + ) + + for account, expected_account in accounts: + holding = value.holding(account) + + assert type(holding) is pt.AssetHoldingObject + + expected_asset = value.referenced_index() + actual_asset = holding._asset + + actual_account = holding._account + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual_asset.__teal__(options) == expected_asset.__teal__(options) + assert actual_account.__teal__(options) == expected_account.__teal__( + options + ) + + +def test_Asset_params(): + value = abi.Asset() + + params = value.params() + + assert type(params) is pt.AssetParamObject + + expected = value.referenced_index() + actual = params._asset + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual.__teal__(options) == expected.__teal__(options) + + +def test_Application_str(): + assert str(abi.ApplicationTypeSpec()) == "application" + + +def test_ApplicationTypeSpec_is_dynamic(): + assert not (abi.ApplicationTypeSpec()).is_dynamic() + + +def test_ApplicationTypeSpec_new_instance(): + assert isinstance(abi.ApplicationTypeSpec().new_instance(), abi.Application) + + +def test_ApplicationTypeSpec_eq(): + assert abi.ApplicationTypeSpec() == abi.ApplicationTypeSpec() + + for otherType in ( + abi.ByteTypeSpec(), + abi.Uint8TypeSpec(), + abi.AddressTypeSpec(), + ): + assert abi.ApplicationTypeSpec() != otherType + + +def test_Application_typespec(): + assert abi.Application().type_spec() == abi.ApplicationTypeSpec() + + +def test_Application_application_id(): + value = abi.Application() + expr = value.application_id() + assert expr.type_of() == pt.TealType.uint64 + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, value.stored_value.slot), + pt.TealOp(None, pt.Op.txnas, "Applications"), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Application_params(): + value = abi.Application() + + params = value.params() + + assert type(params) is pt.AppParamObject + + expected = value.referenced_index() + actual = params._app + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual.__teal__(options) == expected.__teal__(options) diff --git a/pyteal/ast/abi/string.py b/pyteal/ast/abi/string.py new file mode 100644 index 000000000..1a6d5eed9 --- /dev/null +++ b/pyteal/ast/abi/string.py @@ -0,0 +1,116 @@ +from typing import Union, Sequence, cast +from collections.abc import Sequence as CollectionSequence + +from pyteal.ast.abi.uint import Byte +from pyteal.ast.abi.type import ComputedValue, BaseType +from pyteal.ast.abi.array_dynamic import DynamicArray, DynamicArrayTypeSpec +from pyteal.ast.abi.uint import ByteTypeSpec, Uint16TypeSpec + +from pyteal.ast.int import Int +from pyteal.ast.expr import Expr +from pyteal.ast.bytes import Bytes +from pyteal.ast.unaryexpr import Itob, Len +from pyteal.ast.substring import Suffix +from pyteal.ast.naryexpr import Concat + +from pyteal.errors import TealInputError + + +def _encoded_string(s: Expr): + return Concat(Suffix(Itob(Len(s)), Int(6)), s) + + +class StringTypeSpec(DynamicArrayTypeSpec): + def __init__(self) -> None: + super().__init__(ByteTypeSpec()) + + def new_instance(self) -> "String": + return String() + + def annotation_type(self) -> "type[String]": + return String + + def __str__(self) -> str: + return "string" + + def __eq__(self, other: object) -> bool: + return isinstance(other, StringTypeSpec) + + +StringTypeSpec.__module__ = "pyteal.abi" + + +class String(DynamicArray[Byte]): + def __init__(self) -> None: + super().__init__(StringTypeSpec()) + + def type_spec(self) -> StringTypeSpec: + return StringTypeSpec() + + def get(self) -> Expr: + """Return the value held by this String as a PyTeal expression. + + The expression will have the type TealType.bytes. + """ + return Suffix( + self.stored_value.load(), Int(Uint16TypeSpec().byte_length_static()) + ) + + def set( + self, + value: Union[ + str, + bytes, + Expr, + Sequence[Byte], + DynamicArray[Byte], + ComputedValue[DynamicArray[Byte]], + "String", + ComputedValue["String"], + ], + ) -> Expr: + """Set the value of this String to the input value. + + The behavior of this method depends on the input argument type: + + * :code:`str`: set the value to the Python string. + * :code:`bytes`: set the value to the Python byte string. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.bytes. + * :code:`Sequence[Byte]`: set the bytes of this String to those contained in this Python sequence (e.g. a list or tuple). + * :code:`DynamicArray[Byte]`: copy the bytes from a DynamicArray. + * :code:`ComputedValue[DynamicArray[Byte]]`: copy the bytes from a DynamicArray produced by a ComputedValue. + * :code:`String`: copy the value from another String. + * :code:`ComputedValue[String]`: copy the value from a String produced by a ComputedValue. + + Args: + value: The new value this String should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this String. + """ + + match value: + case ComputedValue(): + return self._set_with_computed_type(value) + case BaseType(): + if value.type_spec() == StringTypeSpec() or ( + value.type_spec() == DynamicArrayTypeSpec(ByteTypeSpec()) + ): + return self.stored_value.store(value.stored_value.load()) + + raise TealInputError( + f"Got {value} with type spec {value.type_spec()}, expected {StringTypeSpec}" + ) + case str() | bytes(): + return self.stored_value.store(_encoded_string(Bytes(value))) + case Expr(): + return self.stored_value.store(_encoded_string(value)) + case CollectionSequence(): + return super().set(cast(Sequence[Byte], value)) + + raise TealInputError( + f"Got {type(value)}, expected DynamicArray, ComputedValue, String, str, bytes, Expr" + ) + + +String.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/string_test.py b/pyteal/ast/abi/string_test.py new file mode 100644 index 000000000..7bb742412 --- /dev/null +++ b/pyteal/ast/abi/string_test.py @@ -0,0 +1,249 @@ +import pytest + +import pyteal as pt +from pyteal import abi +from pyteal.ast.abi.util import substring_for_decoding +from pyteal.ast.abi.type_test import ContainerType +from pyteal.util import escapeStr + +options = pt.CompileOptions(version=5) + + +def test_StringTypeSpec_str(): + assert str(abi.StringTypeSpec()) == "string" + + +def test_StringTypeSpec_is_dynamic(): + assert (abi.StringTypeSpec()).is_dynamic() + + +def test_StringTypeSpec_new_instance(): + assert isinstance(abi.StringTypeSpec().new_instance(), abi.String) + + +def test_StringTypeSpec_eq(): + assert abi.StringTypeSpec() == abi.StringTypeSpec() + + for otherType in ( + abi.ByteTypeSpec(), + abi.StaticArrayTypeSpec(abi.ByteTypeSpec(), 1), + abi.DynamicArrayTypeSpec(abi.Uint8TypeSpec()), + abi.DynamicArrayTypeSpec(abi.ByteTypeSpec()), + ): + assert abi.StringTypeSpec() != otherType + + +def test_String_encode(): + value = abi.String() + expr = value.encode() + assert expr.type_of() == pt.TealType.bytes + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [pt.TealOp(expr, pt.Op.load, value.stored_value.slot)] + ) + actual, _ = expr.__teal__(options) + assert actual == expected + + +def test_DynamicArray_decode(): + encoded = pt.Bytes("encoded") + stringType = abi.StringTypeSpec() + for start_index in (None, pt.Int(1)): + for end_index in (None, pt.Int(2)): + for length in (None, pt.Int(3)): + value = stringType.new_instance() + + if end_index is not None and length is not None: + with pytest.raises(pt.TealInputError): + value.decode( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + continue + + expr = value.decode( + encoded, start_index=start_index, end_index=end_index, length=length + ) + assert expr.type_of() == pt.TealType.none + assert expr.has_return() is False + + expectedExpr = value.stored_value.store( + substring_for_decoding( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + ) + 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 + + +def test_String_get(): + value = abi.String() + expr = value.get() + assert expr.type_of() == pt.TealType.bytes + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(expr, pt.Op.load, value.stored_value.slot), + pt.TealOp(None, pt.Op.extract, 2, 0), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_String_set_static(): + + for value_to_set in ("stringy", "😀", "0xDEADBEEF"): + value = abi.String() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, escapeStr(value_to_set)), + pt.TealOp(None, pt.Op.len), + pt.TealOp(None, pt.Op.itob), + pt.TealOp(None, pt.Op.extract, 6, 0), + pt.TealOp(None, pt.Op.byte, escapeStr(value_to_set)), + pt.TealOp(None, pt.Op.concat), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + for value_to_set in (bytes(32), b"alphabet_soup"): + value = abi.String() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + teal_val = f"0x{value_to_set.hex()}" + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, teal_val), + pt.TealOp(None, pt.Op.len), + pt.TealOp(None, pt.Op.itob), + pt.TealOp(None, pt.Op.extract, 6, 0), + pt.TealOp(None, pt.Op.byte, teal_val), + pt.TealOp(None, pt.Op.concat), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(42) + + +def test_String_set_expr(): + for value_to_set in (pt.Bytes("hi"), pt.Bytes("base16", "0xdeadbeef")): + value = abi.String() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + vts, _ = value_to_set.__teal__(options) + expected = pt.TealSimpleBlock( + [ + vts.ops[0], + pt.TealOp(None, pt.Op.len), + pt.TealOp(None, pt.Op.itob), + pt.TealOp(None, pt.Op.extract, 6, 0), + vts.ops[0], + pt.TealOp(None, pt.Op.concat), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_String_set_copy(): + value = abi.String() + other = abi.String() + expr = value.set(other) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, other.stored_value.slot), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(abi.Address()) + + +def test_String_set_computed(): + bv = pt.Bytes("base16", "0x0004DEADBEEF") + computed_value = ContainerType(abi.StringTypeSpec(), bv) + + value = abi.String() + expr = value.set(computed_value) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + _, byte_ops = bv.__teal__(options) + expected = pt.TealSimpleBlock( + [ + byte_ops.ops[0], + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(ContainerType(abi.ByteTypeSpec(), pt.Int(0x01))) diff --git a/pyteal/ast/abi/transaction.py b/pyteal/ast/abi/transaction.py new file mode 100644 index 000000000..c392f8b64 --- /dev/null +++ b/pyteal/ast/abi/transaction.py @@ -0,0 +1,270 @@ +from enum import Enum +from typing import Union, cast, List, Final +from pyteal.ast.abi.type import BaseType, ComputedValue, TypeSpec +from pyteal.ast.expr import Expr +from pyteal.ast.int import Int +from pyteal.ast.txn import TxnObject, TxnType +from pyteal.ast.gtxn import Gtxn +from pyteal.types import TealType +from pyteal.errors import TealInputError, TealInternalError + + +class TransactionType(Enum): + Any = "txn" + Payment = "pay" + KeyRegistration = "keyreg" + AssetConfig = "acfg" + AssetTransfer = "axfer" + AssetFreeze = "afrz" + ApplicationCall = "appl" + + +TransactionType.__module__ = "pyteal.abi" + + +class TransactionTypeSpec(TypeSpec): + def __init__(self) -> None: + super().__init__() + + def new_instance(self) -> "Transaction": + return Transaction() + + def annotation_type(self) -> "type[Transaction]": + return Transaction + + def is_dynamic(self) -> bool: + return False + + def byte_length_static(self) -> int: + raise TealInputError("Transaction Types don't have a static size") + + def storage_type(self) -> TealType: + return TealType.uint64 + + def txn_type_enum(self) -> Expr: + """Get the integer transaction type value this TransactionTypeSpec represents. + + See :any:`TxnType` for the complete list. + + If this is a generic TransactionTypeSpec, i.e. type :code:`txn`, this method will raise an error, since this type does not represent a single transaction type. + """ + raise TealInternalError( + "abi.TransactionTypeSpec does not represent a specific transaction type" + ) + + def __eq__(self, other: object) -> bool: + return type(self) is type(other) + + def __str__(self) -> str: + return TransactionType.Any.value + + +TransactionTypeSpec.__module__ = "pyteal.abi" + + +class Transaction(BaseType): + def __init__(self, spec: TransactionTypeSpec = None) -> None: + if spec is None: + super().__init__(TransactionTypeSpec()) + else: + super().__init__(spec) + + def type_spec(self) -> TransactionTypeSpec: + return cast(TransactionTypeSpec, super().type_spec()) + + def get(self) -> TxnObject: + return Gtxn[self.index()] + + def _set_index( + self, value: Union[int, Expr, "Transaction", ComputedValue["Transaction"]] + ) -> Expr: + match value: + case ComputedValue(): + return self._set_with_computed_type(value) + case BaseType(): + return self.stored_value.store(self.stored_value.load()) + case int(): + return self.stored_value.store(Int(value)) + case Expr(): + return self.stored_value.store(value) + case _: + raise TealInputError(f"Cant store a {type(value)} in a Transaction") + + def index(self) -> Expr: + return self.stored_value.load() + + def decode( + self, + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None, + ) -> Expr: + raise TealInputError("A Transaction cannot be decoded") + + def encode(self) -> Expr: + raise TealInputError("A Transaction cannot be encoded") + + +Transaction.__module__ = "pyteal.abi" + + +class PaymentTransactionTypeSpec(TransactionTypeSpec): + def new_instance(self) -> "PaymentTransaction": + return PaymentTransaction() + + def annotation_type(self) -> "type[PaymentTransaction]": + return PaymentTransaction + + def txn_type_enum(self) -> Expr: + return TxnType.Payment + + def __str__(self) -> str: + return TransactionType.Payment.value + + +PaymentTransactionTypeSpec.__module__ = "pyteal.abi" + + +class PaymentTransaction(Transaction): + def __init__(self): + super().__init__(PaymentTransactionTypeSpec()) + + +PaymentTransaction.__module__ = "pyteal.abi" + + +class KeyRegisterTransactionTypeSpec(TransactionTypeSpec): + def new_instance(self) -> "KeyRegisterTransaction": + return KeyRegisterTransaction() + + def annotation_type(self) -> "type[KeyRegisterTransaction]": + return KeyRegisterTransaction + + def txn_type_enum(self) -> Expr: + return TxnType.KeyRegistration + + def __str__(self) -> str: + return TransactionType.KeyRegistration.value + + +KeyRegisterTransactionTypeSpec.__module__ = "pyteal.abi" + + +class KeyRegisterTransaction(Transaction): + def __init__(self): + super().__init__(KeyRegisterTransactionTypeSpec()) + + +KeyRegisterTransaction.__module__ = "pyteal.abi" + + +class AssetConfigTransactionTypeSpec(TransactionTypeSpec): + def new_instance(self) -> "AssetConfigTransaction": + return AssetConfigTransaction() + + def annotation_type(self) -> "type[AssetConfigTransaction]": + return AssetConfigTransaction + + def txn_type_enum(self) -> Expr: + return TxnType.AssetConfig + + def __str__(self) -> str: + return TransactionType.AssetConfig.value + + +AssetConfigTransactionTypeSpec.__module__ = "pyteal.abi" + + +class AssetConfigTransaction(Transaction): + def __init__(self): + super().__init__(AssetConfigTransactionTypeSpec()) + + +AssetConfigTransaction.__module__ = "pyteal.abi" + + +class AssetFreezeTransactionTypeSpec(TransactionTypeSpec): + def new_instance(self) -> "AssetFreezeTransaction": + return AssetFreezeTransaction() + + def annotation_type(self) -> "type[AssetFreezeTransaction]": + return AssetFreezeTransaction + + def txn_type_enum(self) -> Expr: + return TxnType.AssetFreeze + + def __str__(self) -> str: + return TransactionType.AssetFreeze.value + + +AssetFreezeTransactionTypeSpec.__module__ = "pyteal.abi" + + +class AssetFreezeTransaction(Transaction): + def __init__(self): + super().__init__(AssetFreezeTransactionTypeSpec()) + + +AssetFreezeTransaction.__module__ = "pyteal.abi" + + +class AssetTransferTransactionTypeSpec(TransactionTypeSpec): + def new_instance(self) -> "AssetTransferTransaction": + return AssetTransferTransaction() + + def annotation_type(self) -> "type[AssetTransferTransaction]": + return AssetTransferTransaction + + def txn_type_enum(self) -> Expr: + return TxnType.AssetTransfer + + def __str__(self) -> str: + return TransactionType.AssetTransfer.value + + +AssetTransferTransactionTypeSpec.__module__ = "pyteal.abi" + + +class AssetTransferTransaction(Transaction): + def __init__(self): + super().__init__(AssetTransferTransactionTypeSpec()) + + +AssetTransferTransaction.__module__ = "pyteal.abi" + + +class ApplicationCallTransactionTypeSpec(TransactionTypeSpec): + def new_instance(self) -> "ApplicationCallTransaction": + return ApplicationCallTransaction() + + def annotation_type(self) -> "type[ApplicationCallTransaction]": + return ApplicationCallTransaction + + def txn_type_enum(self) -> Expr: + return TxnType.ApplicationCall + + def __str__(self) -> str: + return TransactionType.ApplicationCall.value + + +ApplicationCallTransactionTypeSpec.__module__ = "pyteal.abi" + + +class ApplicationCallTransaction(Transaction): + def __init__(self): + super().__init__(ApplicationCallTransactionTypeSpec()) + + +ApplicationCallTransaction.__module__ = "pyteal.abi" + +TransactionTypeSpecs: Final[List[TypeSpec]] = [ + TransactionTypeSpec(), + PaymentTransactionTypeSpec(), + KeyRegisterTransactionTypeSpec(), + AssetConfigTransactionTypeSpec(), + AssetFreezeTransactionTypeSpec(), + AssetTransferTransactionTypeSpec(), + ApplicationCallTransactionTypeSpec(), +] diff --git a/pyteal/ast/abi/transaction_test.py b/pyteal/ast/abi/transaction_test.py new file mode 100644 index 000000000..5803b8f32 --- /dev/null +++ b/pyteal/ast/abi/transaction_test.py @@ -0,0 +1,142 @@ +from dataclasses import dataclass +from typing import List +import pyteal as pt +from pyteal import abi +import pytest + +from pyteal.errors import TealInputError + +options = pt.CompileOptions(version=5) + + +@dataclass +class TransactionTypeTest: + ts: abi.TransactionTypeSpec + t: abi.Transaction + s: str + txn_type_enum: pt.Expr | None + + +TransactionValues: List[TransactionTypeTest] = [ + TransactionTypeTest(abi.TransactionTypeSpec(), abi.Transaction(), "txn", None), + TransactionTypeTest( + abi.KeyRegisterTransactionTypeSpec(), + abi.KeyRegisterTransaction(), + "keyreg", + pt.TxnType.KeyRegistration, + ), + TransactionTypeTest( + abi.PaymentTransactionTypeSpec(), + abi.PaymentTransaction(), + "pay", + pt.TxnType.Payment, + ), + TransactionTypeTest( + abi.AssetConfigTransactionTypeSpec(), + abi.AssetConfigTransaction(), + "acfg", + pt.TxnType.AssetConfig, + ), + TransactionTypeTest( + abi.AssetFreezeTransactionTypeSpec(), + abi.AssetFreezeTransaction(), + "afrz", + pt.TxnType.AssetFreeze, + ), + TransactionTypeTest( + abi.AssetTransferTransactionTypeSpec(), + abi.AssetTransferTransaction(), + "axfer", + pt.TxnType.AssetTransfer, + ), + TransactionTypeTest( + abi.ApplicationCallTransactionTypeSpec(), + abi.ApplicationCallTransaction(), + "appl", + pt.TxnType.ApplicationCall, + ), +] + + +def test_Transaction_str(): + for tv in TransactionValues: + assert str(tv.ts) == tv.s + + +def test_TransactionTypeSpec_is_dynamic(): + for tv in TransactionValues: + assert not (tv.ts).is_dynamic() + + +def test_TransactionTypeSpec_new_instance(): + for tv in TransactionValues: + assert isinstance(tv.ts.new_instance(), abi.Transaction) + + +def test_TransactionTypeSpec_txn_type_enum(): + for tv in TransactionValues: + if tv.txn_type_enum is None: + with pytest.raises( + pt.TealInternalError, + match=r"abi.TransactionTypeSpec does not represent a specific transaction type$", + ): + tv.ts.txn_type_enum() + else: + assert tv.ts.txn_type_enum() is tv.txn_type_enum + + +def test_TransactionTypeSpec_eq(): + for tv in TransactionValues: + assert tv.ts == tv.ts + + for otherType in ( + abi.ByteTypeSpec(), + abi.Uint8TypeSpec(), + abi.AddressTypeSpec(), + ): + assert tv.ts != otherType + + +def test_Transaction_typespec(): + for tv in TransactionValues: + assert tv.t.type_spec() == tv.ts + + +def test_Transaction_decode(): + for tv in TransactionValues: + with pytest.raises(TealInputError): + tv.t.decode("") + + +def test_Transaction_encode(): + for tv in TransactionValues: + with pytest.raises(TealInputError): + tv.t.encode() + + +def test_Transaction_get(): + for tv in TransactionValues: + expr = tv.t.get() + assert isinstance(expr, pt.TxnObject) + + +def test_Transaction__set_index(): + for tv in TransactionValues: + val_to_set = 2 + expr = tv.t._set_index(val_to_set) + + assert expr.type_of() == pt.TealType.none + assert expr.has_return() is False + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(expr, pt.Op.int, val_to_set), + pt.TealOp(None, pt.Op.store, tv.t.stored_value.slot), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected diff --git a/pyteal/ast/abi/tuple.py b/pyteal/ast/abi/tuple.py new file mode 100644 index 000000000..cfcab1d83 --- /dev/null +++ b/pyteal/ast/abi/tuple.py @@ -0,0 +1,486 @@ +from typing import ( + List, + Sequence, + Dict, + Generic, + TypeVar, + cast, + overload, + Any, +) + +from pyteal.types import TealType +from pyteal.errors import TealInputError +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 +from pyteal.ast.naryexpr import Concat +from pyteal.ast.scratchvar import ScratchVar + +from pyteal.ast.abi.type import TypeSpec, BaseType, ComputedValue +from pyteal.ast.abi.bool import ( + Bool, + BoolTypeSpec, + _consecutive_bool_instance_num, + _consecutive_bool_type_spec_num, + _bool_sequence_length, + _encode_bool_sequence, + _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 + + +def _encode_tuple(values: Sequence[BaseType]) -> Expr: + heads: List[Expr] = [] + head_length_static: int = 0 + + dynamicValueIndexToHeadIndex: Dict[int, int] = dict() + ignoreNext = 0 + for i, elem in enumerate(values): + if ignoreNext > 0: + ignoreNext -= 1 + continue + + elemType = elem.type_spec() + + if elemType == BoolTypeSpec(): + numBools = _consecutive_bool_instance_num(values, i) + ignoreNext = numBools - 1 + head_length_static += _bool_sequence_length(numBools) + heads.append( + _encode_bool_sequence(cast(Sequence[Bool], values[i : i + numBools])) + ) + continue + + if elemType.is_dynamic(): + head_length_static += 2 + dynamicValueIndexToHeadIndex[i] = len(heads) + heads.append(Seq()) # a placeholder + continue + + head_length_static += elemType.byte_length_static() + heads.append(elem.encode()) + + tail_offset = Uint16() + tail_offset_accumulator = Uint16() + tail_holder = ScratchVar(TealType.bytes) + encoded_tail = ScratchVar(TealType.bytes) + + firstDynamicTail = True + for i, elem in enumerate(values): + if elem.type_spec().is_dynamic(): + if firstDynamicTail: + firstDynamicTail = False + updateVars = Seq( + tail_holder.store(encoded_tail.load()), + tail_offset.set(head_length_static), + ) + else: + updateVars = Seq( + tail_holder.store(Concat(tail_holder.load(), encoded_tail.load())), + tail_offset.set(tail_offset_accumulator), + ) + + notLastDynamicValue = any( + [nextValue.type_spec().is_dynamic() for nextValue in values[i + 1 :]] + ) + if notLastDynamicValue: + updateAccumulator = tail_offset_accumulator.set( + tail_offset.get() + Len(encoded_tail.load()) + ) + else: + updateAccumulator = Seq() + + heads[dynamicValueIndexToHeadIndex[i]] = Seq( + encoded_tail.store(elem.encode()), + updateVars, + updateAccumulator, + tail_offset.encode(), + ) + + toConcat = heads + if not firstDynamicTail: + toConcat.append(tail_holder.load()) + + if len(toConcat) == 0: + return Bytes("") + + return Concat(*toConcat) + + +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() + + valueType = value_types[index] + if output.type_spec() != valueType: + raise TypeError("Output type does not match value type") + + 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)) + + if valueType.is_dynamic(): + hasNextDynamicValue = False + nextDynamicValueOffset = offset + 2 + ignoreNext = 0 + for i, typeAfter in enumerate(value_types[index + 1 :], start=index + 1): + 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 + + 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 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 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 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) + + +class TupleTypeSpec(TypeSpec): + def __init__(self, *value_type_specs: TypeSpec) -> None: + super().__init__() + self.value_specs = list(value_type_specs) + + def value_type_specs(self) -> List[TypeSpec]: + """Get the TypeSpecs for the values of this tuple.""" + return self.value_specs + + def length_static(self) -> int: + """Get the number of values this tuple holds.""" + return len(self.value_specs) + + def new_instance(self) -> "Tuple": + return Tuple(self) + + def annotation_type(self) -> "type[Tuple]": + vtses = self.value_type_specs() + + def annotater(): + return [x.annotation_type() for x in vtses] + + match len(vtses): + case 0: + return Tuple0 + case 1: + v0 = annotater()[0] + return Tuple1[v0] # type: ignore[valid-type] + case 2: + v0, v1 = annotater() + return Tuple2[v0, v1] # type: ignore[valid-type] + case 3: + v0, v1, v2 = annotater() + return Tuple3[v0, v1, v2] # type: ignore[valid-type] + case 4: + v0, v1, v2, v3 = annotater() + return Tuple4[v0, v1, v2, v3] # type: ignore[valid-type] + case 5: + v0, v1, v2, v3, v4 = annotater() + return Tuple5[v0, v1, v2, v3, v4] # type: ignore[valid-type] + + raise TypeError(f"Cannot annotate tuple of length {len(vtses)}") + + def is_dynamic(self) -> bool: + return any(type_spec.is_dynamic() for type_spec in self.value_type_specs()) + + def byte_length_static(self) -> int: + if self.is_dynamic(): + raise ValueError("Type is dynamic") + return _bool_aware_static_byte_length(self.value_type_specs()) + + def storage_type(self) -> TealType: + return TealType.bytes + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, TupleTypeSpec) + and self.value_type_specs() == other.value_type_specs() + ) + + def __str__(self) -> str: + return "({})".format(",".join(map(str, self.value_type_specs()))) + + +TupleTypeSpec.__module__ = "pyteal.abi" + + +class Tuple(BaseType): + def __init__(self, tuple_type_spec: TupleTypeSpec) -> None: + super().__init__(tuple_type_spec) + + def type_spec(self) -> TupleTypeSpec: + return cast(TupleTypeSpec, super().type_spec()) + + def decode( + self, + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None, + ) -> Expr: + extracted = substring_for_decoding( + encoded, start_index=start_index, end_index=end_index, length=length + ) + return self.stored_value.store(extracted) + + @overload + def set(self, *values: BaseType) -> Expr: + pass + + @overload + def set(self, values: ComputedValue["Tuple"]) -> Expr: + # TODO: should support values as a Tuple as well + pass + + def set(self, *values): + """ + set(*values: BaseType) -> Expr + set(values: ComputedValue[Tuple]) -> Expr + + Set the elements of this Tuple to the input values. + + The behavior of this method depends on the input argument type: + + * Variable number of :code:`BaseType` arguments: set the elements of this Tuple to the arguments to this method. A compiler error will occur if any argument does not match this Tuple's element type at the same index, or if the total argument count does not equal this Tuple's length. + * :code:`ComputedValue[Tuple]`: copy the elements from a Tuple produced by a ComputedValue. The element types and length produced by the ComputedValue must exactly match this Tuple's element types and length, otherwise an error will occur. + + Args: + values: The new elements this Tuple should have. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Tuple. + """ + if len(values) == 1 and isinstance(values[0], ComputedValue): + return self._set_with_computed_type(values[0]) + + for value in values: + if not isinstance(value, BaseType): + raise TealInputError(f"Expected BaseType, got {value}") + + myTypes = self.type_spec().value_type_specs() + if len(myTypes) != len(values): + raise TealInputError( + f"Incorrect length for values. Expected {len(myTypes)}, got {len(values)}" + ) + if not all(myTypes[i] == values[i].type_spec() for i in range(len(myTypes))): + raise TealInputError("Input values do not match type") + return self.stored_value.store(_encode_tuple(values)) + + def encode(self) -> Expr: + return self.stored_value.load() + + def length(self) -> Expr: + """Get the number of values this tuple holds as an Expr.""" + return Int(self.type_spec().length_static()) + + def __getitem__(self, index: int) -> "TupleElement[Any]": + """Retrieve an element by its index in this Tuple. + + Indexes start at 0. + + Args: + index: a Python integer containing the index to access. This function will raise an error + if its value is negative or if the index is equal to or greater than the length of + this Tuple. + + Returns: + A TupleElement that corresponds to the element at the given index. This type is a + ComputedValue. Due to Python type limitations, the parameterized type of the + TupleElement is Any. + """ + if not (0 <= index < self.type_spec().length_static()): + raise TealInputError(f"Index out of bounds: {index}") + return TupleElement(self, index) + + +Tuple.__module__ = "pyteal.abi" + +T = TypeVar("T", bound=BaseType) + + +class TupleElement(ComputedValue[T]): + """Represents the extraction of a specific element from a Tuple.""" + + def __init__(self, tuple: Tuple, index: int) -> None: + super().__init__() + self.tuple = tuple + self.index = index + + 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(), + self.index, + output, + ) + + +TupleElement.__module__ = "pyteal.abi" + +# Until Python 3.11 is released with support for PEP 646 -- Variadic Generics, it's not possible for +# the Tuple class to take an arbitrary number of template parameters. As a workaround, we define the +# following classes for specifically sized Tuples. If needed, more classes can be added for larger +# sizes. + + +def _tuple_raise_arg_mismatch(expected: int, typespec: TupleTypeSpec): + if len(typespec.value_specs) != expected: + raise TealInputError( + f"Expected TupleTypeSpec with {expected} elements, Got {len(typespec.value_specs)}" + ) + + +class Tuple0(Tuple): + """A Tuple with 0 values.""" + + def __init__(self) -> None: + super().__init__(TupleTypeSpec()) + + +Tuple0.__module__ = "pyteal.abi" + +T1 = TypeVar("T1", bound=BaseType) + + +class Tuple1(Tuple, Generic[T1]): + """A Tuple with 1 value.""" + + def __init__(self, value_type_spec: TupleTypeSpec) -> None: + _tuple_raise_arg_mismatch(1, value_type_spec) + super().__init__(value_type_spec) + + +Tuple1.__module__ = "pyteal.abi" + +T2 = TypeVar("T2", bound=BaseType) + + +class Tuple2(Tuple, Generic[T1, T2]): + """A Tuple with 2 values.""" + + def __init__(self, value_type_spec: TupleTypeSpec) -> None: + _tuple_raise_arg_mismatch(2, value_type_spec) + super().__init__(value_type_spec) + + +Tuple2.__module__ = "pyteal.abi" + +T3 = TypeVar("T3", bound=BaseType) + + +class Tuple3(Tuple, Generic[T1, T2, T3]): + """A Tuple with 3 values.""" + + def __init__( + self, + value_type_spec: TupleTypeSpec, + ) -> None: + _tuple_raise_arg_mismatch(3, value_type_spec) + super().__init__(value_type_spec) + + +Tuple3.__module__ = "pyteal.abi" + +T4 = TypeVar("T4", bound=BaseType) + + +class Tuple4(Tuple, Generic[T1, T2, T3, T4]): + """A Tuple with 4 values.""" + + def __init__( + self, + value_type_spec: TupleTypeSpec, + ) -> None: + _tuple_raise_arg_mismatch(4, value_type_spec) + super().__init__(value_type_spec) + + +Tuple4.__module__ = "pyteal.abi" + +T5 = TypeVar("T5", bound=BaseType) + + +class Tuple5(Tuple, Generic[T1, T2, T3, T4, T5]): + """A Tuple with 5 values.""" + + def __init__( + self, + value_type_spec: TupleTypeSpec, + ) -> None: + _tuple_raise_arg_mismatch(5, value_type_spec) + super().__init__(value_type_spec) + + +Tuple5.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/tuple_test.py b/pyteal/ast/abi/tuple_test.py new file mode 100644 index 000000000..e52f10890 --- /dev/null +++ b/pyteal/ast/abi/tuple_test.py @@ -0,0 +1,826 @@ +from typing import NamedTuple, List, Callable +import pytest + +import pyteal as pt +from pyteal import abi +from pyteal.ast.abi.tuple import _encode_tuple, _index_tuple, 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 + +options = pt.CompileOptions(version=5) + + +def test_encodeTuple(): + class EncodeTest(NamedTuple): + types: List[abi.BaseType] + expected: pt.Expr + + # variables used to construct the tests + uint64_a = abi.Uint64() + uint64_b = abi.Uint64() + uint16_a = abi.Uint16() + uint16_b = abi.Uint16() + bool_a = abi.Bool() + bool_b = abi.Bool() + tuple_a = abi.Tuple(abi.TupleTypeSpec(abi.BoolTypeSpec(), abi.BoolTypeSpec())) + dynamic_array_a = abi.DynamicArray(abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec())) + dynamic_array_b = abi.DynamicArray(abi.DynamicArrayTypeSpec(abi.Uint16TypeSpec())) + dynamic_array_c = abi.DynamicArray(abi.DynamicArrayTypeSpec(abi.BoolTypeSpec())) + tail_holder = pt.ScratchVar() + encoded_tail = pt.ScratchVar() + + tests: List[EncodeTest] = [ + EncodeTest(types=[], expected=pt.Bytes("")), + EncodeTest(types=[uint64_a], expected=uint64_a.encode()), + EncodeTest( + types=[uint64_a, uint64_b], + expected=pt.Concat(uint64_a.encode(), uint64_b.encode()), + ), + EncodeTest(types=[bool_a], expected=bool_a.encode()), + EncodeTest( + types=[bool_a, bool_b], expected=_encode_bool_sequence([bool_a, bool_b]) + ), + EncodeTest( + types=[bool_a, bool_b, uint64_a], + expected=pt.Concat( + _encode_bool_sequence([bool_a, bool_b]), uint64_a.encode() + ), + ), + EncodeTest( + types=[uint64_a, bool_a, bool_b], + expected=pt.Concat( + uint64_a.encode(), _encode_bool_sequence([bool_a, bool_b]) + ), + ), + EncodeTest( + types=[uint64_a, bool_a, bool_b, uint64_b], + expected=pt.Concat( + uint64_a.encode(), + _encode_bool_sequence([bool_a, bool_b]), + uint64_b.encode(), + ), + ), + EncodeTest( + types=[uint64_a, bool_a, uint64_b, bool_b], + expected=pt.Concat( + uint64_a.encode(), bool_a.encode(), uint64_b.encode(), bool_b.encode() + ), + ), + EncodeTest(types=[tuple_a], expected=tuple_a.encode()), + EncodeTest( + types=[uint64_a, tuple_a, bool_a, bool_b], + expected=pt.Concat( + uint64_a.encode(), + tuple_a.encode(), + _encode_bool_sequence([bool_a, bool_b]), + ), + ), + EncodeTest( + types=[dynamic_array_a], + expected=pt.Concat( + pt.Seq( + encoded_tail.store(dynamic_array_a.encode()), + tail_holder.store(encoded_tail.load()), + uint16_a.set(2), + uint16_a.encode(), + ), + tail_holder.load(), + ), + ), + EncodeTest( + types=[uint64_a, dynamic_array_a], + expected=pt.Concat( + uint64_a.encode(), + pt.Seq( + encoded_tail.store(dynamic_array_a.encode()), + tail_holder.store(encoded_tail.load()), + uint16_a.set(8 + 2), + uint16_a.encode(), + ), + tail_holder.load(), + ), + ), + EncodeTest( + types=[uint64_a, dynamic_array_a, uint64_b], + expected=pt.Concat( + uint64_a.encode(), + pt.Seq( + encoded_tail.store(dynamic_array_a.encode()), + tail_holder.store(encoded_tail.load()), + uint16_a.set(8 + 2 + 8), + uint16_a.encode(), + ), + uint64_b.encode(), + tail_holder.load(), + ), + ), + EncodeTest( + types=[uint64_a, dynamic_array_a, bool_a, bool_b], + expected=pt.Concat( + uint64_a.encode(), + pt.Seq( + encoded_tail.store(dynamic_array_a.encode()), + tail_holder.store(encoded_tail.load()), + uint16_a.set(8 + 2 + 1), + uint16_a.encode(), + ), + _encode_bool_sequence([bool_a, bool_b]), + tail_holder.load(), + ), + ), + EncodeTest( + types=[uint64_a, dynamic_array_a, uint64_b, dynamic_array_b], + expected=pt.Concat( + uint64_a.encode(), + pt.Seq( + encoded_tail.store(dynamic_array_a.encode()), + tail_holder.store(encoded_tail.load()), + uint16_a.set(8 + 2 + 8 + 2), + uint16_b.set(uint16_a.get() + pt.Len(encoded_tail.load())), + uint16_a.encode(), + ), + uint64_b.encode(), + pt.Seq( + encoded_tail.store(dynamic_array_b.encode()), + tail_holder.store( + pt.Concat(tail_holder.load(), encoded_tail.load()) + ), + uint16_a.set(uint16_b), + uint16_a.encode(), + ), + tail_holder.load(), + ), + ), + EncodeTest( + types=[ + uint64_a, + dynamic_array_a, + uint64_b, + dynamic_array_b, + bool_a, + bool_b, + dynamic_array_c, + ], + expected=pt.Concat( + uint64_a.encode(), + pt.Seq( + encoded_tail.store(dynamic_array_a.encode()), + tail_holder.store(encoded_tail.load()), + uint16_a.set(8 + 2 + 8 + 2 + 1 + 2), + uint16_b.set(uint16_a.get() + pt.Len(encoded_tail.load())), + uint16_a.encode(), + ), + uint64_b.encode(), + pt.Seq( + encoded_tail.store(dynamic_array_b.encode()), + tail_holder.store( + pt.Concat(tail_holder.load(), encoded_tail.load()) + ), + uint16_a.set(uint16_b), + uint16_b.set(uint16_a.get() + pt.Len(encoded_tail.load())), + uint16_a.encode(), + ), + _encode_bool_sequence([bool_a, bool_b]), + pt.Seq( + encoded_tail.store(dynamic_array_c.encode()), + tail_holder.store( + pt.Concat(tail_holder.load(), encoded_tail.load()) + ), + uint16_a.set(uint16_b), + uint16_a.encode(), + ), + tail_holder.load(), + ), + ), + ] + + for i, test in enumerate(tests): + expr = _encode_tuple(test.types) + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + expected, _ = test.expected.__teal__(options) + expected.addIncoming() + expected = pt.TealBlock.NormalizeBlocks(expected) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + if any(t.type_spec().is_dynamic() for t in test.types): + with pt.TealComponent.Context.ignoreExprEquality(): + with pt.TealComponent.Context.ignoreScratchSlotEquality(): + assert actual == expected, "Test at index {} failed".format(i) + + assert pt.TealBlock.MatchScratchSlotReferences( + pt.TealBlock.GetReferencedScratchSlots(actual), + pt.TealBlock.GetReferencedScratchSlots(expected), + ) + continue + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected, "Test at index {} failed".format(i) + + +def test_indexTuple(): + class IndexTest(NamedTuple): + types: List[abi.TypeSpec] + typeIndex: int + expected: Callable[[abi.BaseType], pt.Expr] + + # variables used to construct the tests + uint64_t = abi.Uint64TypeSpec() + byte_t = abi.ByteTypeSpec() + bool_t = abi.BoolTypeSpec() + tuple_t = abi.TupleTypeSpec(abi.BoolTypeSpec(), abi.BoolTypeSpec()) + dynamic_array_t1 = abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()) + dynamic_array_t2 = abi.DynamicArrayTypeSpec(abi.Uint16TypeSpec()) + + encoded = pt.Bytes("encoded") + + tests: List[IndexTest] = [ + IndexTest( + types=[uint64_t], + typeIndex=0, + expected=lambda output: output.decode(encoded), + ), + IndexTest( + types=[uint64_t, uint64_t], + typeIndex=0, + expected=lambda output: output.decode(encoded, length=pt.Int(8)), + ), + IndexTest( + types=[uint64_t, uint64_t], + typeIndex=1, + expected=lambda output: output.decode(encoded, start_index=pt.Int(8)), + ), + IndexTest( + types=[uint64_t, byte_t, uint64_t], + typeIndex=1, + expected=lambda output: output.decode( + 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( + encoded, start_index=pt.Int(9), length=pt.Int(8) + ), + ), + IndexTest( + types=[bool_t], + typeIndex=0, + expected=lambda output: output.decode_bit(encoded, pt.Int(0)), + ), + IndexTest( + types=[bool_t, bool_t], + typeIndex=0, + expected=lambda output: output.decode_bit(encoded, pt.Int(0)), + ), + IndexTest( + types=[bool_t, bool_t], + typeIndex=1, + expected=lambda output: output.decode_bit(encoded, pt.Int(1)), + ), + IndexTest( + types=[uint64_t, bool_t], + typeIndex=1, + expected=lambda output: output.decode_bit(encoded, pt.Int(8 * 8)), + ), + IndexTest( + types=[uint64_t, bool_t, bool_t], + typeIndex=1, + expected=lambda output: output.decode_bit(encoded, pt.Int(8 * 8)), + ), + IndexTest( + types=[uint64_t, bool_t, bool_t], + typeIndex=2, + expected=lambda output: output.decode_bit(encoded, pt.Int(8 * 8 + 1)), + ), + IndexTest( + types=[bool_t, uint64_t], + typeIndex=0, + expected=lambda output: output.decode_bit(encoded, pt.Int(0)), + ), + IndexTest( + types=[bool_t, uint64_t], + typeIndex=1, + expected=lambda output: output.decode(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)), + ), + IndexTest( + types=[bool_t, bool_t, uint64_t], + typeIndex=1, + expected=lambda output: output.decode_bit(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)), + ), + IndexTest( + types=[tuple_t], typeIndex=0, expected=lambda output: output.decode(encoded) + ), + IndexTest( + types=[byte_t, tuple_t], + typeIndex=1, + expected=lambda output: output.decode(encoded, start_index=pt.Int(1)), + ), + IndexTest( + types=[tuple_t, byte_t], + typeIndex=0, + expected=lambda output: output.decode( + encoded, + start_index=pt.Int(0), + length=pt.Int(tuple_t.byte_length_static()), + ), + ), + IndexTest( + types=[byte_t, tuple_t, byte_t], + typeIndex=1, + expected=lambda output: output.decode( + encoded, + start_index=pt.Int(1), + length=pt.Int(tuple_t.byte_length_static()), + ), + ), + IndexTest( + types=[dynamic_array_t1], + typeIndex=0, + expected=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(0)) + ), + ), + IndexTest( + types=[byte_t, dynamic_array_t1], + typeIndex=1, + expected=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(1)) + ), + ), + IndexTest( + types=[dynamic_array_t1, byte_t], + typeIndex=0, + expected=lambda output: output.decode( + 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( + 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( + encoded, + start_index=pt.ExtractUint16(encoded, pt.Int(1)), + end_index=pt.ExtractUint16(encoded, pt.Int(4)), + ), + ), + IndexTest( + types=[byte_t, dynamic_array_t1, byte_t, dynamic_array_t2], + typeIndex=3, + expected=lambda output: output.decode( + 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( + encoded, + start_index=pt.ExtractUint16(encoded, pt.Int(1)), + end_index=pt.ExtractUint16(encoded, pt.Int(4)), + ), + ), + IndexTest( + types=[byte_t, dynamic_array_t1, tuple_t, dynamic_array_t2], + typeIndex=3, + expected=lambda output: output.decode( + 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( + encoded, + start_index=pt.ExtractUint16(encoded, pt.Int(1)), + end_index=pt.ExtractUint16(encoded, pt.Int(4)), + ), + ), + IndexTest( + types=[byte_t, dynamic_array_t1, bool_t, bool_t, dynamic_array_t2], + typeIndex=4, + expected=lambda output: output.decode( + encoded, start_index=pt.ExtractUint16(encoded, pt.Int(4)) + ), + ), + ] + + for i, test in enumerate(tests): + output = test.types[test.typeIndex].new_instance() + expr = _index_tuple(test.types, encoded, test.typeIndex, output) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected, _ = test.expected(output).__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, "Test at index {} failed".format(i) + + with pytest.raises(ValueError): + _index_tuple(test.types, encoded, len(test.types), output) + + with pytest.raises(ValueError): + _index_tuple(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) + + +def test_TupleTypeSpec_eq(): + tupleA = abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.Uint32TypeSpec(), abi.BoolTypeSpec() + ) + tupleB = abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.Uint32TypeSpec(), abi.BoolTypeSpec() + ) + tupleC = abi.TupleTypeSpec( + abi.BoolTypeSpec(), abi.Uint64TypeSpec(), abi.Uint32TypeSpec() + ) + assert tupleA == tupleA + assert tupleA == tupleB + assert tupleA != tupleC + + +def test_TupleTypeSpec_value_type_specs(): + assert abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.Uint32TypeSpec(), abi.BoolTypeSpec() + ).value_type_specs() == [ + abi.Uint64TypeSpec(), + abi.Uint32TypeSpec(), + abi.BoolTypeSpec(), + ] + + +def test_TupleTypeSpec_length_static(): + tests: List[List[abi.TypeSpec]] = [ + [], + [abi.Uint64TypeSpec()], + [ + abi.TupleTypeSpec(abi.Uint64TypeSpec(), abi.Uint64TypeSpec()), + abi.Uint64TypeSpec(), + ], + [abi.BoolTypeSpec()] * 8, + ] + + for i, test in enumerate(tests): + actual = abi.TupleTypeSpec(*test).length_static() + expected = len(test) + assert actual == expected, "Test at index {} failed".format(i) + + +def test_TupleTypeSpec_new_instance(): + assert isinstance( + abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.Uint32TypeSpec(), abi.BoolTypeSpec() + ).new_instance(), + abi.Tuple, + ) + + +def test_TupleTypeSpec_is_dynamic(): + assert not abi.TupleTypeSpec().is_dynamic() + assert not abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.Uint32TypeSpec(), abi.BoolTypeSpec() + ).is_dynamic() + assert abi.TupleTypeSpec( + abi.Uint16TypeSpec(), abi.DynamicArrayTypeSpec(abi.Uint8TypeSpec()) + ).is_dynamic() + + +def test_TupleTypeSpec_str(): + assert str(abi.TupleTypeSpec()) == "()" + assert str(abi.TupleTypeSpec(abi.TupleTypeSpec())) == "(())" + assert str(abi.TupleTypeSpec(abi.TupleTypeSpec(), abi.TupleTypeSpec())) == "((),())" + assert ( + str( + abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.Uint32TypeSpec(), abi.BoolTypeSpec() + ) + ) + == "(uint64,uint32,bool)" + ) + assert ( + str( + abi.TupleTypeSpec( + abi.BoolTypeSpec(), abi.Uint64TypeSpec(), abi.Uint32TypeSpec() + ) + ) + == "(bool,uint64,uint32)" + ) + assert ( + str( + abi.TupleTypeSpec( + abi.Uint16TypeSpec(), abi.DynamicArrayTypeSpec(abi.Uint8TypeSpec()) + ) + ) + == "(uint16,uint8[])" + ) + + +def test_TupleTypeSpec_byte_length_static(): + assert abi.TupleTypeSpec().byte_length_static() == 0 + assert abi.TupleTypeSpec(abi.TupleTypeSpec()).byte_length_static() == 0 + assert ( + abi.TupleTypeSpec(abi.TupleTypeSpec(), abi.TupleTypeSpec()).byte_length_static() + == 0 + ) + assert ( + abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.Uint32TypeSpec(), abi.BoolTypeSpec() + ).byte_length_static() + == 8 + 4 + 1 + ) + assert ( + abi.TupleTypeSpec( + abi.Uint64TypeSpec(), + abi.Uint32TypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + ).byte_length_static() + == 8 + 4 + 1 + ) + assert ( + abi.TupleTypeSpec( + abi.Uint64TypeSpec(), + abi.Uint32TypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + abi.BoolTypeSpec(), + ).byte_length_static() + == 8 + 4 + 2 + ) + + with pytest.raises(ValueError): + abi.TupleTypeSpec( + abi.Uint16TypeSpec(), abi.DynamicArrayTypeSpec(abi.Uint8TypeSpec()) + ).byte_length_static() + + +def test_Tuple_decode(): + encoded = pt.Bytes("encoded") + tupleValue = abi.Tuple(abi.TupleTypeSpec(abi.Uint64TypeSpec())) + for start_index in (None, pt.Int(1)): + for end_index in (None, pt.Int(2)): + for length in (None, pt.Int(3)): + if end_index is not None and length is not None: + with pytest.raises(pt.TealInputError): + tupleValue.decode( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + continue + + expr = tupleValue.decode( + encoded, start_index=start_index, end_index=end_index, length=length + ) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expectedExpr = tupleValue.stored_value.store( + substring_for_decoding( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + ) + 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 + + +def test_Tuple_set(): + tupleValue = abi.Tuple( + abi.TupleTypeSpec( + abi.Uint8TypeSpec(), abi.Uint16TypeSpec(), abi.Uint32TypeSpec() + ) + ) + uint8 = abi.Uint8() + uint16 = abi.Uint16() + uint32 = abi.Uint32() + + with pytest.raises(pt.TealInputError): + tupleValue.set() + + with pytest.raises(pt.TealInputError): + tupleValue.set(uint8, uint16) + + with pytest.raises(pt.TealInputError): + tupleValue.set(uint8, uint16, uint32, uint32) + + with pytest.raises(pt.TealInputError): + tupleValue.set(uint8, uint32, uint16) + + with pytest.raises(pt.TealInputError): + tupleValue.set(uint8, uint16, uint16) + + expr = tupleValue.set(uint8, uint16, uint32) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expectedExpr = tupleValue.stored_value.store(_encode_tuple([uint8, uint16, uint32])) + 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 + + +def test_Tuple_set_Computed(): + tupleValue = abi.Tuple( + abi.TupleTypeSpec( + abi.Uint8TypeSpec(), abi.Uint16TypeSpec(), abi.Uint32TypeSpec() + ) + ) + computed = ContainerType( + tupleValue.type_spec(), pt.Bytes("internal representation") + ) + expr = tupleValue.set(computed) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.byte, '"internal representation"'), + pt.TealOp(None, pt.Op.store, tupleValue.stored_value.slot), + ] + ) + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + tupleValue.set(computed, computed) + + with pytest.raises(pt.TealInputError): + tupleValue.set( + ContainerType(abi.TupleTypeSpec(abi.ByteTypeSpec()), pt.Bytes(b"a")) + ) + + +def test_Tuple_encode(): + tupleValue = abi.Tuple(abi.TupleTypeSpec(abi.Uint64TypeSpec())) + expr = tupleValue.encode() + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [pt.TealOp(None, pt.Op.load, tupleValue.stored_value.slot)] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Tuple_length(): + tests: List[List[abi.TypeSpec]] = [ + [], + [abi.Uint64TypeSpec()], + [ + abi.TupleTypeSpec(abi.Uint64TypeSpec(), abi.Uint64TypeSpec()), + abi.Uint64TypeSpec(), + ], + [abi.BoolTypeSpec()] * 8, + ] + + for i, test in enumerate(tests): + tupleValue = abi.Tuple(abi.TupleTypeSpec(*test)) + expr = tupleValue.length() + assert expr.type_of() == pt.TealType.uint64 + assert not expr.has_return() + + expectedLength = len(test) + expected = pt.TealSimpleBlock([pt.TealOp(None, pt.Op.int, expectedLength)]) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected, "Test at index {} failed".format(i) + + +def test_Tuple_getitem(): + tests: List[List[abi.TypeSpec]] = [ + [], + [abi.Uint64TypeSpec()], + [ + abi.TupleTypeSpec(abi.Uint64TypeSpec(), abi.Uint64TypeSpec()), + abi.Uint64TypeSpec(), + ], + [abi.BoolTypeSpec()] * 8, + ] + + for i, test in enumerate(tests): + tupleValue = abi.Tuple(abi.TupleTypeSpec(*test)) + for j in range(len(test)): + element = tupleValue[j] + assert type(element) is TupleElement, "Test at index {} failed".format(i) + assert element.tuple is tupleValue, "Test at index {} failed".format(i) + assert element.index == j, "Test at index {} failed".format(i) + + with pytest.raises(pt.TealInputError): + tupleValue[-1] + + with pytest.raises(pt.TealInputError): + tupleValue[len(test)] + + +def test_TupleElement_store_into(): + tests: List[List[abi.TypeSpec]] = [ + [], + [abi.Uint64TypeSpec()], + [ + abi.TupleTypeSpec(abi.Uint64TypeSpec(), abi.Uint64TypeSpec()), + abi.Uint64TypeSpec(), + ], + [abi.BoolTypeSpec()] * 8, + ] + + for i, test in enumerate(tests): + tupleValue = abi.Tuple(abi.TupleTypeSpec(*test)) + for j in range(len(test)): + element = TupleElement(tupleValue, j) + output = test[j].new_instance() + + expr = element.store_into(output) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expectedExpr = _index_tuple(test, tupleValue.encode(), j, output) + 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, "Test at index {} failed".format(i) diff --git a/pyteal/ast/abi/type.py b/pyteal/ast/abi/type.py new file mode 100644 index 000000000..4b0b1e0f3 --- /dev/null +++ b/pyteal/ast/abi/type.py @@ -0,0 +1,237 @@ +from typing import TypeVar, Generic, Callable, Final, cast +from abc import ABC, abstractmethod + +from pyteal.errors import TealInputError +from pyteal.types import TealType +from pyteal.ast.expr import Expr +from pyteal.ast.scratchvar import ScratchVar +from pyteal.ast.seq import Seq + + +class TypeSpec(ABC): + """TypeSpec represents a specification for an ABI type. + + Essentially this is a factory that can produce specific instances of ABI types. + """ + + @abstractmethod + def new_instance(self) -> "BaseType": + """Create a new instance of the specified type.""" + pass + + @abstractmethod + def annotation_type(self) -> "type[BaseType]": + """Get the annotation type associated with this spec""" + 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 storage_type(self) -> TealType: + """Get the TealType that the underlying ScratchVar should hold for this type.""" + pass + + @abstractmethod + def __eq__(self, other: object) -> bool: + """Check if this type is considered equal to another ABI type. + + Args: + other: The object to compare to. If this is not a TypeSpec, this method will never + return true. + + Returns: + True if and only if self and other represent the same ABI type. + """ + pass + + @abstractmethod + def __str__(self) -> str: + """Get the string representation of this ABI type, used for creating method signatures.""" + pass + + +TypeSpec.__module__ = "pyteal.abi" + + +class BaseType(ABC): + """The abstract base class for all ABI type instances. + + The value of the type is contained in a unique ScratchVar that only this instance 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, spec: TypeSpec) -> None: + """Create a new BaseType.""" + super().__init__() + self._type_spec: Final = spec + self.stored_value: Final = ScratchVar(spec.storage_type()) + + def type_spec(self) -> TypeSpec: + """Get the TypeSpec for this ABI type instance.""" + return self._type_spec + + @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 + def decode( + self, + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None, + ) -> Expr: + """Decode a substring of the passed in encoded string and set it as this type's value. + + The arguments to this function are means to be as flexible as possible for the caller. + Multiple types of substrings can be specified based on the arguments, as listed below: + + * Entire string: if start_index, end_index, and length are all None, the entire encoded string + is decoded. + * Prefix: if start_index is None and one of end_index or length is provided, a prefix of the + encoded string is decoded. The range is 0 through end_index or length (they are equivalent). + * Suffix: if start_index is provided and end_index and length are None, a suffix of the encoded + string is decoded. The range is start_index through the end of the string. + * Substring specified with end_index: if start_index and end_index are provided and length is + None, a substring of the encoded string is decoded. The range is start_index through + end_index. + * Substring specified with length: if start_index and length are provided and end_index is + None, a substring of the encoded string is decoded. The range is start_index through + start_index+length. + + Args: + encoded: An expression containing the bytes to decode. Must evaluate to TealType.bytes. + start_index (optional): An expression containing the index to start decoding. Must + evaluate to TealType.uint64. Defaults to None. + end_index (optional): An expression containing the index to stop decoding. Must evaluate + to TealType.uint64. Defaults to None. + length (optional): An expression containing the length of the substring to decode. Must + evaluate to TealType.uint64. Defaults to None. + + Returns: + An expression that performs the necessary steps in order to decode the given string into + a value. + """ + pass + + def _set_with_computed_type(self, value: "ComputedValue[BaseType]") -> Expr: + target_type_spec = value.produced_type_spec() + if self.type_spec() != target_type_spec: + raise TealInputError( + f"Cannot set {self.type_spec()} with ComputedType of {target_type_spec}" + ) + return value.store_into(self) + + def __str__(self) -> str: + return str(self.type_spec()) + + +BaseType.__module__ = "pyteal.abi" + +T_co = TypeVar("T_co", bound=BaseType, covariant=True) + + +class ComputedValue(ABC, Generic[T_co]): + """Represents an ABI Type whose value must be computed by an expression.""" + + @abstractmethod + def produced_type_spec(self) -> TypeSpec: + """Get the ABI TypeSpec that this object produces.""" + pass + + @abstractmethod + def store_into(self, output: T_co) -> Expr: # type: ignore[misc] + """Compute the value and store it into an existing ABI type instance. + + NOTE: If you call this method multiple times, the computation to determine the value will be + repeated each time. For this reason, it is recommended to only issue a single call to either + :code:`store_into` or :code:`use`. + + Args: + output: The object where the computed value will be stored. This object must have the + same type as this class's produced type. + + Returns: + An expression which stores the computed value represented by this class into the output + object. + """ + pass + + def use(self, action: Callable[[T_co], Expr]) -> Expr: + """Compute the value and pass it to a callable expression. + + NOTE: If you call this method multiple times, the computation to determine the value will be + repeated each time. For this reason, it is recommended to only issue a single call to either + :code:`store_into` or :code:`use`. + + Args: + action: A callable object that will receive an instance of this class's produced type + with the computed value. The callable object may use that value as it sees fit, but + it must return an Expr to be included in the program's AST. + + Returns: + An expression which contains the returned expression from invoking `action` with the + computed value. + """ + newInstance = cast(T_co, self.produced_type_spec().new_instance()) + return Seq(self.store_into(newInstance), action(newInstance)) + + +ComputedValue.__module__ = "pyteal.abi" + + +class ReturnedValue(ComputedValue): + def __init__(self, type_spec: TypeSpec, computation_expr: Expr): + from pyteal.ast.subroutine import SubroutineCall + + self.type_spec = type_spec + if not isinstance(computation_expr, SubroutineCall): + raise TealInputError( + f"Expecting computation_expr to be SubroutineCall but get {type(computation_expr)}" + ) + self.computation = computation_expr + + def produced_type_spec(self) -> TypeSpec: + return self.type_spec + + def store_into(self, output: BaseType) -> Expr: + if output.type_spec() != self.produced_type_spec(): + raise TealInputError( + f"expected type_spec {self.produced_type_spec()} but get {output.type_spec()}" + ) + + declaration = self.computation.subroutine.get_declaration() + + if declaration.deferred_expr is None: + raise TealInputError( + "ABI return subroutine must have deferred_expr to be not-None." + ) + if declaration.deferred_expr.type_of() != output.type_spec().storage_type(): + raise TealInputError( + f"ABI return subroutine deferred_expr is expected to be typed {output.type_spec().storage_type()}, " + f"but has type {declaration.deferred_expr.type_of()}." + ) + return output.stored_value.slot.store(self.computation) + + +ReturnedValue.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/type_test.py b/pyteal/ast/abi/type_test.py new file mode 100644 index 000000000..56a9e6c76 --- /dev/null +++ b/pyteal/ast/abi/type_test.py @@ -0,0 +1,50 @@ +import pyteal as pt +from pyteal import abi + +options = pt.CompileOptions(version=5) + + +class ContainerType(abi.ComputedValue): + def __init__(self, type_spec: abi.TypeSpec, encodings: pt.Expr): + self.type_spec = type_spec + self.encodings = encodings + + def produced_type_spec(self) -> abi.TypeSpec: + return self.type_spec + + def store_into(self, output: abi.BaseType) -> pt.Expr: + if output.type_spec() != self.type_spec: + raise pt.TealInputError( + f"expected type_spec {self.type_spec} but get {output.type_spec()}" + ) + return output.stored_value.store(self.encodings) + + +def test_ComputedType_use(): + for value in (0, 1, 2, 3, 12345): + dummyComputedType = ContainerType(abi.Uint64TypeSpec(), pt.Int(value)) + expr = dummyComputedType.use(lambda output: pt.Int(2) * output.get()) + assert expr.type_of() == pt.TealType.uint64 + assert not expr.has_return() + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + assert type(actual) is pt.TealSimpleBlock + assert actual.ops[1].op == pt.Op.store + assert type(actual.ops[1].args[0]) is pt.ScratchSlot + actualSlot = actual.ops[1].args[0] + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, value), + pt.TealOp(None, pt.Op.store, actualSlot), + pt.TealOp(None, pt.Op.int, 2), + pt.TealOp(None, pt.Op.load, actualSlot), + pt.TealOp(None, pt.Op.mul), + ] + ) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected diff --git a/pyteal/ast/abi/uint.py b/pyteal/ast/abi/uint.py new file mode 100644 index 000000000..53d2cc797 --- /dev/null +++ b/pyteal/ast/abi/uint.py @@ -0,0 +1,341 @@ +from typing import ( + Union, + Optional, + Final, + cast, +) +from abc import abstractmethod + +from pyteal.types import TealType +from pyteal.errors import TealInputError +from pyteal.ast.scratchvar import ScratchVar +from pyteal.ast.expr import Expr +from pyteal.ast.seq import Seq +from pyteal.ast.assert_ import Assert +from pyteal.ast.substring import Suffix +from pyteal.ast.int import Int +from pyteal.ast.bytes import Bytes +from pyteal.ast.unaryexpr import Itob, Btoi +from pyteal.ast.binaryexpr import GetByte, ExtractUint16, ExtractUint32, ExtractUint64 +from pyteal.ast.ternaryexpr import SetByte +from pyteal.ast.abi.type import ComputedValue, TypeSpec, BaseType + +NUM_BITS_IN_BYTE = 8 + +SUPPORTED_UINT_SIZES = (8, 16, 32, 64) + + +def uint_storage_type(size: int) -> TealType: + if size <= 64: + return TealType.uint64 + return TealType.bytes + + +def uint_set(size: int, uint_var: ScratchVar, value: Union[int, Expr, "Uint"]) -> Expr: + if size > 64: + raise NotImplementedError( + "Uint operations have not yet been implemented for bit sizes larger than 64" + ) + + checked = False + if type(value) is int: + if value >= 2**size: + raise TealInputError("Value exceeds uint{} maximum: {}".format(size, value)) + value = Int(value) + checked = True + + if isinstance(value, Uint): + value = value.get() + checked = True + + if checked or size == 64: + return uint_var.store(cast(Expr, value)) + + return Seq( + uint_var.store(cast(Expr, value)), + Assert(uint_var.load() < Int(2**size)), + ) + + +def uint_decode( + size: int, + uint_var: ScratchVar, + encoded: Expr, + start_index: Optional[Expr], + end_index: Optional[Expr], + length: Optional[Expr], +) -> Expr: + if size > 64: + raise NotImplementedError( + "Uint operations have not yet been implemented for bit sizes larger than 64" + ) + + if size == 64: + if start_index is None: + if end_index is None and length is None: + return uint_var.store(Btoi(encoded)) + start_index = Int(0) + return uint_var.store(ExtractUint64(encoded, start_index)) + + if start_index is None: + start_index = Int(0) + + if size == 8: + return uint_var.store(GetByte(encoded, start_index)) + if size == 16: + return uint_var.store(ExtractUint16(encoded, start_index)) + if size == 32: + return uint_var.store(ExtractUint32(encoded, start_index)) + + raise ValueError("Unsupported uint size: {}".format(size)) + + +def uint_encode(size: int, uint_var: Expr | ScratchVar) -> Expr: + + if isinstance(uint_var, ScratchVar): + uint_var = uint_var.load() + + if size > 64: + raise NotImplementedError( + "Uint operations have not yet been implemented for bit sizes larger than 64" + ) + + if size == 8: + return SetByte(Bytes(b"\x00"), Int(0), uint_var) + if size == 16: + return Suffix(Itob(uint_var), Int(6)) + if size == 32: + return Suffix(Itob(uint_var), Int(4)) + if size == 64: + return Itob(uint_var) + + raise ValueError("Unsupported uint size: {}".format(size)) + + +class UintTypeSpec(TypeSpec): + def __init__(self, bit_size: int) -> None: + super().__init__() + if bit_size not in SUPPORTED_UINT_SIZES: + raise TypeError("Unsupported uint size: {}".format(bit_size)) + self.size: Final = bit_size + + @abstractmethod + def new_instance(self) -> "Uint": + pass + + @abstractmethod + def annotation_type(self) -> "type[Uint]": + pass + + def bit_size(self) -> int: + """Get the bit size of this uint type""" + return self.size + + def is_dynamic(self) -> bool: + return False + + def byte_length_static(self) -> int: + return self.bit_size() // NUM_BITS_IN_BYTE + + def storage_type(self) -> TealType: + return uint_storage_type(self.bit_size()) + + def __eq__(self, other: object) -> bool: + # NOTE: by this implementation, ByteTypeSpec() != Uint8TypeSpec() + return ( + type(self) is type(other) + and self.bit_size() == cast(UintTypeSpec, other).bit_size() + ) + + def __str__(self) -> str: + return "uint{}".format(self.bit_size()) + + +UintTypeSpec.__module__ = "pyteal.abi" + + +class ByteTypeSpec(UintTypeSpec): + def __init__(self) -> None: + super().__init__(8) + + def new_instance(self) -> "Byte": + return Byte() + + def annotation_type(self) -> "type[Byte]": + return Byte + + def __str__(self) -> str: + return "byte" + + +ByteTypeSpec.__module__ = "pyteal.abi" + + +class Uint8TypeSpec(UintTypeSpec): + def __init__(self) -> None: + super().__init__(8) + + def new_instance(self) -> "Uint8": + return Uint8() + + def annotation_type(self) -> "type[Uint8]": + return Uint8 + + +Uint8TypeSpec.__module__ = "pyteal.abi" + + +class Uint16TypeSpec(UintTypeSpec): + def __init__(self) -> None: + super().__init__(16) + + def new_instance(self) -> "Uint16": + return Uint16() + + def annotation_type(self) -> "type[Uint16]": + return Uint16 + + +Uint16TypeSpec.__module__ = "pyteal.abi" + + +class Uint32TypeSpec(UintTypeSpec): + def __init__(self) -> None: + super().__init__(32) + + def new_instance(self) -> "Uint32": + return Uint32() + + def annotation_type(self) -> "type[Uint32]": + return Uint32 + + +Uint32TypeSpec.__module__ = "pyteal.abi" + + +class Uint64TypeSpec(UintTypeSpec): + def __init__(self) -> None: + super().__init__(64) + + def new_instance(self) -> "Uint64": + return Uint64() + + def annotation_type(self) -> "type[Uint64]": + return Uint64 + + +Uint32TypeSpec.__module__ = "pyteal.abi" + + +class Uint(BaseType): + @abstractmethod + def __init__(self, spec: UintTypeSpec) -> None: + super().__init__(spec) + + def type_spec(self) -> UintTypeSpec: + return cast(UintTypeSpec, super().type_spec()) + + def get(self) -> Expr: + """Return the value held by this Uint as a PyTeal expression. + + The expression will have the type TealType.uint64. + """ + return self.stored_value.load() + + def set(self, value: Union[int, Expr, "Uint", ComputedValue["Uint"]]) -> Expr: + """Set the value of this Uint to the input value. + + There are a variety of ways to express the input value. Regardless of the type used to + indicate the input value, this Uint type can only hold values in the range :code:`[0,2^N)`, + where :code:`N` is the bit size of this Uint. + + The behavior of this method depends on the input argument type: + + * :code:`int`: set the value to a Python integer. A compiler error will occur if this value overflows or underflows this integer type. + * :code:`Expr`: set the value to the result of a PyTeal expression, which must evaluate to a TealType.uint64. The program will fail if the evaluated value overflows or underflows this integer type. + * :code:`Uint`: copy the value from another Uint. The argument's type must exactly match this integer's type, otherwise an error will occur. For example, it's not possible to set a Uint64 to a Uint8, or vice versa. + * :code:`ComputedValue[Uint]`: copy the value from a Uint produced by a ComputedValue. The type produced by the ComputedValue must exactly match this integer's type, otherwise an error will occur. + + Args: + value: The new value this Uint should take. This must follow the above constraints. + + Returns: + An expression which stores the given value into this Uint. + """ + if isinstance(value, ComputedValue): + return self._set_with_computed_type(value) + + if isinstance(value, BaseType) and not ( + isinstance(value.type_spec(), UintTypeSpec) + and self.type_spec().bit_size() + == cast(UintTypeSpec, value.type_spec()).bit_size() + ): + raise TealInputError( + "Type {} is not assignable to type {}".format( + value.type_spec(), self.type_spec() + ) + ) + return uint_set(self.type_spec().bit_size(), self.stored_value, value) + + def decode( + self, + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None, + ) -> Expr: + return uint_decode( + self.type_spec().bit_size(), + self.stored_value, + encoded, + start_index, + end_index, + length, + ) + + def encode(self) -> Expr: + return uint_encode(self.type_spec().bit_size(), self.stored_value) + + +Uint.__module__ = "pyteal.abi" + + +class Byte(Uint): + def __init__(self) -> None: + super().__init__(ByteTypeSpec()) + + +Byte.__module__ = "pyteal.abi" + + +class Uint8(Uint): + def __init__(self) -> None: + super().__init__(Uint8TypeSpec()) + + +Uint8.__module__ = "pyteal.abi" + + +class Uint16(Uint): + def __init__(self) -> None: + super().__init__(Uint16TypeSpec()) + + +Uint16.__module__ = "pyteal.abi" + + +class Uint32(Uint): + def __init__(self) -> None: + super().__init__(Uint32TypeSpec()) + + +Uint32.__module__ = "pyteal.abi" + + +class Uint64(Uint): + def __init__(self) -> None: + super().__init__(Uint64TypeSpec()) + + +Uint64.__module__ = "pyteal.abi" diff --git a/pyteal/ast/abi/uint_test.py b/pyteal/ast/abi/uint_test.py new file mode 100644 index 000000000..3af64f865 --- /dev/null +++ b/pyteal/ast/abi/uint_test.py @@ -0,0 +1,339 @@ +from typing import List, Tuple, NamedTuple, Callable, Union, Optional +from pyteal.ast.abi.type_test import ContainerType +import pyteal as pt +from pyteal import abi + +import pytest + +options = pt.CompileOptions(version=5) + + +class UintTestData(NamedTuple): + uintType: abi.UintTypeSpec + instanceType: type + expectedBits: int + maxValue: int + checkUpperBound: bool + expectedDecoding: Callable[ + [pt.Expr, Optional[pt.Expr], Optional[pt.Expr], Optional[pt.Expr]], pt.Expr + ] + expectedEncoding: Callable[[abi.Uint], pt.Expr] + + +def noneToInt0(value: Union[None, pt.Expr]): + if value is None: + return pt.Int(0) + return value + + +testData = [ + UintTestData( + uintType=abi.Uint8TypeSpec(), + instanceType=abi.Uint8, + expectedBits=8, + maxValue=2**8 - 1, + checkUpperBound=True, + expectedDecoding=lambda encoded, start_index, end_index, length: pt.GetByte( + encoded, noneToInt0(start_index) + ), + expectedEncoding=lambda uintType: pt.SetByte( + pt.Bytes(b"\x00"), pt.Int(0), uintType.get() + ), + ), + UintTestData( + uintType=abi.Uint16TypeSpec(), + instanceType=abi.Uint16, + expectedBits=16, + maxValue=2**16 - 1, + checkUpperBound=True, + expectedDecoding=lambda encoded, start_index, end_index, length: pt.ExtractUint16( + encoded, noneToInt0(start_index) + ), + expectedEncoding=lambda uintType: pt.Suffix(pt.Itob(uintType.get()), pt.Int(6)), + ), + UintTestData( + uintType=abi.Uint32TypeSpec(), + instanceType=abi.Uint32, + expectedBits=32, + maxValue=2**32 - 1, + checkUpperBound=True, + expectedDecoding=lambda encoded, start_index, end_index, length: pt.ExtractUint32( + encoded, noneToInt0(start_index) + ), + expectedEncoding=lambda uintType: pt.Suffix(pt.Itob(uintType.get()), pt.Int(4)), + ), + UintTestData( + uintType=abi.Uint64TypeSpec(), + instanceType=abi.Uint64, + expectedBits=64, + maxValue=2**64 - 1, + checkUpperBound=False, + expectedDecoding=lambda encoded, start_index, end_index, length: pt.Btoi( + encoded + ) + if start_index is None and end_index is None and length is None + else pt.ExtractUint64(encoded, noneToInt0(start_index)), + expectedEncoding=lambda uintType: pt.Itob(uintType.get()), + ), +] + + +def test_UintTypeSpec_bits(): + for test in testData: + assert test.uintType.bit_size() == test.expectedBits + assert test.uintType.byte_length_static() * 8 == test.expectedBits + + +def test_UintTypeSpec_str(): + for test in testData: + assert str(test.uintType) == "uint{}".format(test.expectedBits) + assert str(abi.ByteTypeSpec()) == "byte" + + +def test_UintTypeSpec_is_dynamic(): + for test in testData: + assert not test.uintType.is_dynamic() + assert not abi.ByteTypeSpec().is_dynamic() + + +def test_UintTypeSpec_eq(): + for i, test in enumerate(testData): + assert test.uintType == test.uintType + + for j, otherTest in enumerate(testData): + if i == j: + continue + assert test.uintType != otherTest.uintType + + for otherType in ( + abi.BoolTypeSpec(), + abi.StaticArrayTypeSpec(test.uintType, 1), + abi.DynamicArrayTypeSpec(test.uintType), + ): + assert test.uintType != otherType + + assert abi.ByteTypeSpec() != abi.Uint8TypeSpec() + assert abi.Uint8TypeSpec() != abi.ByteTypeSpec() + + +def test_UintTypeSpec_storage_type(): + for test in testData: + assert test.uintType.storage_type() == pt.TealType.uint64 + assert abi.BoolTypeSpec().storage_type() == pt.TealType.uint64 + + +def test_UintTypeSpec_new_instance(): + for test in testData: + assert isinstance(test.uintType.new_instance(), test.instanceType) + assert isinstance(abi.ByteTypeSpec().new_instance(), abi.Byte) + + +def test_Uint_set_static(): + for test in testData: + for value_to_set in (0, 1, 100, test.maxValue): + value = test.uintType.new_instance() + expr = value.set(value_to_set) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, value_to_set), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(test.maxValue + 1) + + with pytest.raises(pt.TealInputError): + value.set(-1) + + +def test_Uint_set_expr(): + for test in testData: + value = test.uintType.new_instance() + expr = value.set(pt.Int(10) + pt.Int(1)) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + upperBoundCheck = [] + if test.checkUpperBound: + upperBoundCheck = [ + pt.TealOp(None, pt.Op.load, value.stored_value.slot), + pt.TealOp(None, pt.Op.int, test.maxValue + 1), + pt.TealOp(None, pt.Op.lt), + pt.TealOp(None, pt.Op.assert_), + ] + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 10), + pt.TealOp(None, pt.Op.int, 1), + pt.TealOp(None, pt.Op.add), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + + upperBoundCheck + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_Uint_set_copy(): + for test in testData: + value = test.uintType.new_instance() + other = test.uintType.new_instance() + expr = value.set(other) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, other.stored_value.slot), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(abi.Bool()) + + +def test_Uint_set_computed(): + byte_computed_value = ContainerType(abi.ByteTypeSpec(), pt.Int(0x22)) + + for test in testData: + computed_value = ContainerType(test.uintType, pt.Int(0x44)) + value = test.uintType.new_instance() + expr = value.set(computed_value) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 0x44), + pt.TealOp(None, pt.Op.store, value.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + with pytest.raises(pt.TealInputError): + value.set(byte_computed_value) + + +def test_Uint_get(): + for test in testData: + value = test.uintType.new_instance() + expr = value.get() + assert expr.type_of() == pt.TealType.uint64 + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [pt.TealOp(expr, pt.Op.load, value.stored_value.slot)] + ) + + actual, _ = expr.__teal__(options) + + assert actual == expected + + +def test_Uint_decode(): + encoded = pt.Bytes("encoded") + for test in testData: + for start_index in (None, pt.Int(1)): + for end_index in (None, pt.Int(2)): + for length in (None, pt.Int(3)): + value = test.uintType.new_instance() + expr = value.decode( + encoded, + start_index=start_index, + end_index=end_index, + length=length, + ) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expectedDecoding = value.stored_value.store( + test.expectedDecoding(encoded, start_index, end_index, length) + ) + expected, _ = expectedDecoding.__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 + + +def test_Uint_encode(): + for test in testData: + value = test.uintType.new_instance() + expr = value.encode() + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + expected, _ = test.expectedEncoding(value).__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 + + +def test_ByteUint8_mutual_conversion(): + cases: List[Tuple[abi.UintTypeSpec, abi.UintTypeSpec]] = [ + (abi.Uint8TypeSpec(), abi.ByteTypeSpec()), + (abi.ByteTypeSpec(), abi.Uint8TypeSpec()), + ] + for type_a, type_b in cases: + type_b_instance = type_b.new_instance() + other = type_a.new_instance() + expr = type_b_instance.set(other) + + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.load, other.stored_value.slot), + pt.TealOp(None, pt.Op.store, type_b_instance.stored_value.slot), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected diff --git a/pyteal/ast/abi/util.py b/pyteal/ast/abi/util.py new file mode 100644 index 000000000..b98d9a5aa --- /dev/null +++ b/pyteal/ast/abi/util.py @@ -0,0 +1,475 @@ +from typing import ( + Any, + Literal, + Optional, + Sequence, + TypeVar, + Union, + cast, + get_args, + get_origin, +) + +import algosdk.abi + +from pyteal.errors import TealInputError +from pyteal.ast.expr import Expr +from pyteal.ast.int import Int +from pyteal.ast.substring import Extract, Substring, Suffix +from pyteal.ast.abi.type import TypeSpec, BaseType + + +def substring_for_decoding( + encoded: Expr, + *, + start_index: Expr = None, + end_index: Expr = None, + length: Expr = None, +) -> Expr: + """A helper function for getting the substring to decode according to the rules of BaseType.decode.""" + if length is not None and end_index is not None: + raise TealInputError("length and end_index are mutually exclusive arguments") + + if start_index is not None: + if length is not None: + # substring from start_index to start_index + length + return Extract(encoded, start_index, length) + + if end_index is not None: + # substring from start_index to end_index + return Substring(encoded, start_index, end_index) + + # substring from start_index to end of string + return Suffix(encoded, start_index) + + if length is not None: + # substring from 0 to length + return Extract(encoded, Int(0), length) + + if end_index is not None: + # substring from 0 to end_index + return Substring(encoded, Int(0), end_index) + + # the entire string + return encoded + + +def int_literal_from_annotation(annotation: Any) -> int: + """Extract an integer from a Literal type annotation. + + Args: + annotation: A Literal type annotation. E.g., `Literal[4]`. This must contain only a single + integer value. + + Returns: + The integer that the Literal represents. + """ + origin = get_origin(annotation) + args = get_args(annotation) + + if origin is not Literal: + raise TypeError("Expected literal for argument. Got: {}".format(origin)) + + if len(args) != 1 or type(args[0]) is not int: + raise TypeError( + "Expected single integer argument for Literal. Got: {}".format(args) + ) + + return args[0] + + +def type_spec_from_annotation(annotation: Any) -> TypeSpec: + """Convert an ABI type annotation into the corresponding TypeSpec. + + For example, calling this function with the input `abi.StaticArray[abi.Bool, Literal[5]]` would + return `abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 5)`. + + Args: + annotation. An annotation representing an ABI type instance. + + Raises: + TypeError: if the input annotation does not represent a valid ABI type instance or its + arguments are invalid. + + Returns: + The TypeSpec that corresponds to the input annotation. + """ + from pyteal.ast.abi.bool import BoolTypeSpec, Bool + from pyteal.ast.abi.uint import ( + ByteTypeSpec, + Byte, + Uint8TypeSpec, + Uint8, + Uint16TypeSpec, + Uint16, + Uint32TypeSpec, + Uint32, + Uint64TypeSpec, + Uint64, + ) + from pyteal.ast.abi.array_dynamic import DynamicArrayTypeSpec, DynamicArray + from pyteal.ast.abi.array_static import StaticArrayTypeSpec, StaticArray + from pyteal.ast.abi.tuple import ( + TupleTypeSpec, + Tuple, + Tuple0, + Tuple1, + Tuple2, + Tuple3, + Tuple4, + Tuple5, + ) + from pyteal.ast.abi.string import StringTypeSpec, String + from pyteal.ast.abi.address import AddressTypeSpec, Address + from pyteal.ast.abi.transaction import ( + Transaction, + TransactionTypeSpec, + PaymentTransaction, + PaymentTransactionTypeSpec, + KeyRegisterTransaction, + KeyRegisterTransactionTypeSpec, + AssetConfigTransaction, + AssetConfigTransactionTypeSpec, + AssetFreezeTransaction, + AssetFreezeTransactionTypeSpec, + AssetTransferTransaction, + AssetTransferTransactionTypeSpec, + ApplicationCallTransaction, + ApplicationCallTransactionTypeSpec, + ) + from pyteal.ast.abi.reference_type import ( + AccountTypeSpec, + Account, + AssetTypeSpec, + Asset, + ApplicationTypeSpec, + Application, + ) + + origin = get_origin(annotation) + if origin is None: + origin = annotation + + args = get_args(annotation) + + if origin is Account: + if len(args) != 0: + raise TypeError("Account expects 0 arguments. Got: {}".format(args)) + return AccountTypeSpec() + + if origin is Asset: + if len(args) != 0: + raise TypeError("Asset expects 0 arguments. Got: {}".format(args)) + return AssetTypeSpec() + + if origin is Application: + if len(args) != 0: + raise TypeError("Application expects 0 arguments. Got: {}".format(args)) + return ApplicationTypeSpec() + + if origin is Bool: + if len(args) != 0: + raise TypeError("Bool expects 0 type arguments. Got: {}".format(args)) + return BoolTypeSpec() + + if origin is Byte: + if len(args) != 0: + raise TypeError("Byte expects 0 type arguments. Got: {}".format(args)) + return ByteTypeSpec() + + if origin is Uint8: + if len(args) != 0: + raise TypeError("Uint8 expects 0 type arguments. Got: {}".format(args)) + return Uint8TypeSpec() + + if origin is Uint16: + if len(args) != 0: + raise TypeError("Uint16 expects 0 type arguments. Got: {}".format(args)) + return Uint16TypeSpec() + + if origin is Uint32: + if len(args) != 0: + raise TypeError("Uint32 expects 0 type arguments. Got: {}".format(args)) + return Uint32TypeSpec() + + if origin is Uint64: + if len(args) != 0: + raise TypeError("Uint64 expects 0 type arguments. Got: {}".format(args)) + return Uint64TypeSpec() + + if origin is String: + if len(args) != 0: + raise TypeError("String expects 0 arguments. Got: {}".format(args)) + return StringTypeSpec() + + if origin is Address: + if len(args) != 0: + raise TypeError("Address expects 0 arguments. Got: {}".format(args)) + return AddressTypeSpec() + + if origin is DynamicArray: + if len(args) != 1: + raise TypeError( + "DynamicArray expects 1 type argument. Got: {}".format(args) + ) + value_type_spec = type_spec_from_annotation(args[0]) + return DynamicArrayTypeSpec(value_type_spec) + + if origin is StaticArray: + if len(args) != 2: + raise TypeError("StaticArray expects 1 type argument. Got: {}".format(args)) + value_type_spec = type_spec_from_annotation(args[0]) + array_length = int_literal_from_annotation(args[1]) + return StaticArrayTypeSpec(value_type_spec, array_length) + + if origin is Tuple: + return TupleTypeSpec(*(type_spec_from_annotation(arg) for arg in args)) + + if origin is Tuple0: + if len(args) != 0: + raise TypeError("Tuple0 expects 0 type arguments. Got: {}".format(args)) + return TupleTypeSpec() + + if origin is Tuple1: + if len(args) != 1: + raise TypeError("Tuple1 expects 1 type argument. Got: {}".format(args)) + return TupleTypeSpec(*(type_spec_from_annotation(arg) for arg in args)) + + if origin is Tuple2: + if len(args) != 2: + raise TypeError("Tuple2 expects 2 type arguments. Got: {}".format(args)) + return TupleTypeSpec(*(type_spec_from_annotation(arg) for arg in args)) + + if origin is Tuple3: + if len(args) != 3: + raise TypeError("Tuple3 expects 3 type arguments. Got: {}".format(args)) + return TupleTypeSpec(*(type_spec_from_annotation(arg) for arg in args)) + + if origin is Tuple4: + if len(args) != 4: + raise TypeError("Tuple4 expects 4 type arguments. Got: {}".format(args)) + return TupleTypeSpec(*(type_spec_from_annotation(arg) for arg in args)) + + if origin is Tuple5: + if len(args) != 5: + raise TypeError("Tuple5 expects 5 type arguments. Got: {}".format(args)) + return TupleTypeSpec(*(type_spec_from_annotation(arg) for arg in args)) + + if origin is Transaction: + if len(args) != 0: + raise TypeError("Transaction expects 0 type arguments. Got {}".format(args)) + return TransactionTypeSpec() + + if origin is PaymentTransaction: + if len(args) != 0: + raise TypeError( + "PaymentTransaction expects 0 type arguments. Got {}".format(args) + ) + return PaymentTransactionTypeSpec() + + if origin is KeyRegisterTransaction: + if len(args) != 0: + raise TypeError( + "KeyRegisterTransaction expects 0 type arguments. Got {}".format(args) + ) + return KeyRegisterTransactionTypeSpec() + + if origin is AssetConfigTransaction: + if len(args) != 0: + raise TypeError( + "AssetConfigTransaction expects 0 type arguments. Got {}".format(args) + ) + return AssetConfigTransactionTypeSpec() + + if origin is AssetFreezeTransaction: + if len(args) != 0: + raise TypeError( + "AssetFreezeTransaction expects 0 type arguments. Got {}".format(args) + ) + return AssetFreezeTransactionTypeSpec() + + if origin is AssetTransferTransaction: + if len(args) != 0: + raise TypeError( + "AssetTransferTransaction expects 0 type arguments. Got {}".format(args) + ) + return AssetTransferTransactionTypeSpec() + + if origin is ApplicationCallTransaction: + if len(args) != 0: + raise TypeError( + "ApplicationCallTransaction expects 0 type arguments. Got {}".format( + args + ) + ) + return ApplicationCallTransactionTypeSpec() + + raise TypeError("Unknown annotation origin: {}".format(origin)) + + +T = TypeVar("T", bound=BaseType) + + +def contains_type_spec(ts: TypeSpec, targets: Sequence[TypeSpec]) -> bool: + from pyteal.ast.abi.array_dynamic import DynamicArrayTypeSpec + from pyteal.ast.abi.array_static import StaticArrayTypeSpec + from pyteal.ast.abi.tuple import TupleTypeSpec + + stack: list[TypeSpec] = [ts] + + while stack: + current = stack.pop() + if current in targets: + return True + + match current: + case TupleTypeSpec(): + stack.extend(current.value_type_specs()) + case DynamicArrayTypeSpec(): + stack.append(current.value_type_spec()) + case StaticArrayTypeSpec(): + stack.append(current.value_type_spec()) + + return False + + +def size_of(t: type[T]) -> int: + """Get the size in bytes of an ABI type. Must be a static type""" + + ts = type_spec_from_annotation(t) + if ts.is_dynamic(): + raise TealInputError("Cannot get size of dynamic type") + + return ts.byte_length_static() + + +def make(t: type[T]) -> T: + """Create a new instance of an ABI type. The type to create is determined by the input argument, + which must be a fully-specified type's class. Fully-specified means that every generic argument + is given a value. + + For example: + .. code-block:: python + + # both of these are equivalent + a = abi.make(abi.Tuple2[abi.Uint64, abi.StaticArray[abi.Bool, Literal[8]]]) + b = abi.TupleTypeSpec(abi.Uint64TypeSpec(), abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 8)) + + This is purely a convenience method over instantiating the type directly, which can be cumbersome + due to the lengthy TypeSpec class names. + + Args: + t: A fully-specified subclass of abi.BaseType. + + Returns: + A new instance of the given type class. + """ + return cast(T, type_spec_from_annotation(t).new_instance()) + + +def algosdk_from_type_spec(t: TypeSpec) -> algosdk.abi.ABIType: + from pyteal.ast.abi import ReferenceTypeSpecs, TransactionTypeSpecs + + if t in TransactionTypeSpecs: + raise TealInputError( + f"cannot map ABI transaction type spec {t!r} to an appropriate algosdk ABI type" + ) + + if t in ReferenceTypeSpecs: + raise TealInputError( + f"cannot map ABI reference type spec {t!r} to an appropriate algosdk ABI type" + ) + + return algosdk.abi.ABIType.from_string(str(t)) + + +def algosdk_from_annotation(t: type[T]) -> algosdk.abi.ABIType: + return algosdk_from_type_spec(type_spec_from_annotation(t)) + + +def type_spec_from_algosdk(t: Union[algosdk.abi.ABIType, str]) -> TypeSpec: + + from pyteal.ast.abi.reference_type import ReferenceTypeSpecs + from pyteal.ast.abi.transaction import TransactionTypeSpecs + + from pyteal.ast.abi.array_dynamic import DynamicArrayTypeSpec + from pyteal.ast.abi.array_static import StaticArrayTypeSpec + from pyteal.ast.abi.tuple import TupleTypeSpec + from pyteal.ast.abi.bool import BoolTypeSpec + from pyteal.ast.abi.string import StringTypeSpec + from pyteal.ast.abi.address import AddressTypeSpec + from pyteal.ast.abi.uint import ( + ByteTypeSpec, + Uint8TypeSpec, + Uint16TypeSpec, + Uint32TypeSpec, + Uint64TypeSpec, + ) + + match t: + # Currently reference and transaction types are only strings + case str(): + if algosdk.abi.is_abi_reference_type(t): + ref_dict: dict[str, TypeSpec] = { + str(rts): rts for rts in ReferenceTypeSpecs + } + if t in ref_dict: + return ref_dict[t] + else: + raise TealInputError(f"Invalid reference type: {t}") + + elif algosdk.abi.is_abi_transaction_type(t): + txn_dict: dict[str, TypeSpec] = { + str(tts): tts for tts in TransactionTypeSpecs + } + if t in txn_dict: + return txn_dict[t] + else: + raise TealInputError(f"Invalid transaction type: {t}") + else: + raise TealInputError(f"Invalid ABI type: {t}") + + case algosdk.abi.ABIType(): + match t: + case algosdk.abi.ArrayDynamicType(): + return DynamicArrayTypeSpec(type_spec_from_algosdk(t.child_type)) + case algosdk.abi.ArrayStaticType(): + return StaticArrayTypeSpec( + type_spec_from_algosdk(t.child_type), t.static_length + ) + case algosdk.abi.TupleType(): + return TupleTypeSpec( + *[type_spec_from_algosdk(ct) for ct in t.child_types] + ) + case algosdk.abi.UintType(): + match t.bit_size: + case 8: + return Uint8TypeSpec() + case 16: + return Uint16TypeSpec() + case 32: + return Uint32TypeSpec() + case 64: + return Uint64TypeSpec() + case algosdk.abi.ByteType(): + return ByteTypeSpec() + case algosdk.abi.BoolType(): + return BoolTypeSpec() + case algosdk.abi.StringType(): + return StringTypeSpec() + case algosdk.abi.AddressType(): + return AddressTypeSpec() + case algosdk.abi.UfixedType(): + raise TealInputError("Ufixed not supported") + + raise TealInputError(f"Invalid Type: {t}") + + +def type_specs_from_signature(sig: str) -> tuple[list[TypeSpec], Optional[TypeSpec]]: + sdk_method = algosdk.abi.Method.from_signature(sig) + + return_type = None + if sdk_method.returns.type != algosdk.abi.Returns.VOID: + return_type = type_spec_from_algosdk(sdk_method.returns.type) + + return [type_spec_from_algosdk(arg.type) for arg in sdk_method.args], return_type diff --git a/pyteal/ast/abi/util_test.py b/pyteal/ast/abi/util_test.py new file mode 100644 index 000000000..cd02e6f27 --- /dev/null +++ b/pyteal/ast/abi/util_test.py @@ -0,0 +1,675 @@ +from typing import NamedTuple, List, Literal, Optional, Union, Any, cast +from inspect import isabstract +import pytest + +import algosdk.abi + +import pyteal as pt +from pyteal import abi +from pyteal.ast.abi.util import ( + substring_for_decoding, + int_literal_from_annotation, + type_spec_from_algosdk, + type_spec_from_annotation, + type_specs_from_signature, +) +from pyteal.errors import TealInputError + +options = pt.CompileOptions(version=5) + + +def test_substringForDecoding(): + class SubstringTest(NamedTuple): + start_index: Optional[pt.Expr] + end_index: Optional[pt.Expr] + length: Optional[pt.Expr] + expected: Union[pt.Expr, Any] + + encoded = pt.Bytes("encoded") + + tests: List[SubstringTest] = [ + SubstringTest(start_index=None, end_index=None, length=None, expected=encoded), + SubstringTest( + start_index=None, + end_index=None, + length=pt.Int(4), + expected=pt.Extract(encoded, pt.Int(0), pt.Int(4)), + ), + SubstringTest( + start_index=None, + end_index=pt.Int(4), + length=None, + expected=pt.Substring(encoded, pt.Int(0), pt.Int(4)), + ), + SubstringTest( + start_index=None, + end_index=pt.Int(4), + length=pt.Int(5), + expected=pt.TealInputError, + ), + SubstringTest( + start_index=pt.Int(4), + end_index=None, + length=None, + expected=pt.Suffix(encoded, pt.Int(4)), + ), + SubstringTest( + start_index=pt.Int(4), + end_index=None, + length=pt.Int(5), + expected=pt.Extract(encoded, pt.Int(4), pt.Int(5)), + ), + SubstringTest( + start_index=pt.Int(4), + end_index=pt.Int(5), + length=None, + expected=pt.Substring(encoded, pt.Int(4), pt.Int(5)), + ), + SubstringTest( + start_index=pt.Int(4), + end_index=pt.Int(5), + length=pt.Int(6), + expected=pt.TealInputError, + ), + ] + + for i, test in enumerate(tests): + if not isinstance(test.expected, pt.Expr): + with pytest.raises(test.expected): + substring_for_decoding( + encoded, + start_index=test.start_index, + end_index=test.end_index, + length=test.length, + ) + continue + + expr = substring_for_decoding( + encoded, + start_index=test.start_index, + end_index=test.end_index, + length=test.length, + ) + assert expr.type_of() == pt.TealType.bytes + assert not expr.has_return() + + expected, _ = cast(pt.Expr, test.expected).__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, "Test at index {} failed".format(i) + + +def test_int_literal_from_annotation(): + class IntAnnotationTest(NamedTuple): + annotation: Any + expected: Union[int, Any] + + tests: List[IntAnnotationTest] = [ + IntAnnotationTest(annotation=Literal[0], expected=0), + IntAnnotationTest(annotation=Literal[1], expected=1), + IntAnnotationTest(annotation=Literal[10], expected=10), + # In Python 3.8, Literal[True] == Litearl[1], so the below test fails. + # It's not crucial, so I've commented it out until we no longer support 3.8 + # IntAnnotationTest(annotation=Literal[True], expected=TypeError), + IntAnnotationTest(annotation=Literal["test"], expected=TypeError), + IntAnnotationTest(annotation=Literal[b"test"], expected=TypeError), + IntAnnotationTest(annotation=Literal[None], expected=TypeError), + IntAnnotationTest(annotation=Literal[0, 1], expected=TypeError), + IntAnnotationTest(annotation=Literal, expected=TypeError), + ] + + for i, test in enumerate(tests): + if type(test.expected) is not int: + with pytest.raises(test.expected): + int_literal_from_annotation(test.annotation) + continue + + actual = int_literal_from_annotation(test.annotation) + assert actual == test.expected, "Test at index {} failed".format(i) + + +def test_type_spec_from_annotation(): + class TypeAnnotationTest(NamedTuple): + annotation: Any + expected: Union[abi.TypeSpec, Any] + + tests: List[TypeAnnotationTest] = [ + TypeAnnotationTest(annotation=abi.Bool, expected=abi.BoolTypeSpec()), + TypeAnnotationTest(annotation=abi.Byte, expected=abi.ByteTypeSpec()), + TypeAnnotationTest(annotation=abi.Uint8, expected=abi.Uint8TypeSpec()), + TypeAnnotationTest(annotation=abi.Uint16, expected=abi.Uint16TypeSpec()), + TypeAnnotationTest(annotation=abi.Uint32, expected=abi.Uint32TypeSpec()), + TypeAnnotationTest(annotation=abi.Uint64, expected=abi.Uint64TypeSpec()), + TypeAnnotationTest( + annotation=abi.DynamicArray[abi.Uint32], + expected=abi.DynamicArrayTypeSpec(abi.Uint32TypeSpec()), + ), + TypeAnnotationTest( + annotation=abi.DynamicArray[abi.Uint64], + expected=abi.DynamicArrayTypeSpec(abi.Uint64TypeSpec()), + ), + TypeAnnotationTest( + annotation=abi.DynamicArray[abi.DynamicArray[abi.Uint32]], + expected=abi.DynamicArrayTypeSpec( + abi.DynamicArrayTypeSpec(abi.Uint32TypeSpec()) + ), + ), + TypeAnnotationTest( + annotation=abi.DynamicArray, + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.StaticArray[abi.Uint32, Literal[0]], + expected=abi.StaticArrayTypeSpec(abi.Uint32TypeSpec(), 0), + ), + TypeAnnotationTest( + annotation=abi.StaticArray[abi.Uint32, Literal[10]], + expected=abi.StaticArrayTypeSpec(abi.Uint32TypeSpec(), 10), + ), + TypeAnnotationTest( + annotation=abi.StaticArray[abi.Bool, Literal[500]], + expected=abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 500), + ), + TypeAnnotationTest( + annotation=abi.StaticArray[abi.Bool, Literal[-1]], + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.StaticArray[abi.Bool, int], + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.StaticArray, + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.StaticArray[ + abi.StaticArray[abi.Bool, Literal[500]], Literal[5] + ], + expected=abi.StaticArrayTypeSpec( + abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 500), 5 + ), + ), + TypeAnnotationTest( + annotation=abi.DynamicArray[abi.StaticArray[abi.Bool, Literal[500]]], + expected=abi.DynamicArrayTypeSpec( + abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 500) + ), + ), + TypeAnnotationTest(annotation=abi.Tuple, expected=abi.TupleTypeSpec()), + TypeAnnotationTest(annotation=abi.Tuple0, expected=abi.TupleTypeSpec()), + TypeAnnotationTest( + annotation=abi.Tuple1[abi.Uint32], + expected=abi.TupleTypeSpec(abi.Uint32TypeSpec()), + ), + TypeAnnotationTest( + annotation=abi.Tuple1, + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.Tuple2[abi.Uint32, abi.Uint16], + expected=abi.TupleTypeSpec(abi.Uint32TypeSpec(), abi.Uint16TypeSpec()), + ), + TypeAnnotationTest( + annotation=abi.Tuple2, + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.Tuple3[abi.Uint32, abi.Uint16, abi.Byte], + expected=abi.TupleTypeSpec( + abi.Uint32TypeSpec(), abi.Uint16TypeSpec(), abi.ByteTypeSpec() + ), + ), + TypeAnnotationTest( + annotation=abi.Tuple3, + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.Tuple3[ + abi.Tuple1[abi.Uint32], + abi.StaticArray[abi.Bool, Literal[55]], + abi.Tuple2[abi.Uint32, abi.Uint16], + ], + expected=abi.TupleTypeSpec( + abi.TupleTypeSpec(abi.Uint32TypeSpec()), + abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 55), + abi.TupleTypeSpec(abi.Uint32TypeSpec(), abi.Uint16TypeSpec()), + ), + ), + TypeAnnotationTest( + annotation=abi.Tuple4[abi.Uint32, abi.Uint16, abi.Byte, abi.Bool], + expected=abi.TupleTypeSpec( + abi.Uint32TypeSpec(), + abi.Uint16TypeSpec(), + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + ), + ), + TypeAnnotationTest( + annotation=abi.Tuple4, + expected=TypeError, + ), + TypeAnnotationTest( + annotation=abi.Tuple5[ + abi.Uint32, abi.Uint16, abi.Byte, abi.Bool, abi.Tuple0 + ], + expected=abi.TupleTypeSpec( + abi.Uint32TypeSpec(), + abi.Uint16TypeSpec(), + abi.ByteTypeSpec(), + abi.BoolTypeSpec(), + abi.TupleTypeSpec(), + ), + ), + TypeAnnotationTest( + annotation=abi.Tuple5, + expected=TypeError, + ), + TypeAnnotationTest( + annotation=List[abi.Uint16], + expected=TypeError, + ), + ] + + for i, test in enumerate(tests): + if not isinstance(test.expected, abi.TypeSpec): + with pytest.raises(test.expected): + type_spec_from_annotation(test.annotation) + continue + + actual = type_spec_from_annotation(test.annotation) + assert actual == test.expected, "Test at index {} failed".format(i) + + +def test_type_spec_from_annotation_is_exhaustive(): + # This test is to make sure there are no new subclasses of BaseType that type_spec_from_annotation + # is not aware of. + + subclasses = abi.BaseType.__subclasses__() + while len(subclasses) > 0: + subclass = subclasses.pop() + subclasses += subclass.__subclasses__() + + if isabstract(subclass): + # abstract class type annotations should not be supported + with pytest.raises(TypeError, match=r"^Unknown annotation origin"): + type_spec_from_annotation(subclass) + continue + + try: + # if subclass is not generic, this will succeed + type_spec_from_annotation(subclass) + except TypeError as e: + # if subclass is generic, we should get an error that is NOT "Unknown annotation origin" + assert "Unknown annotation origin" not in str(e) + + +def test_make(): + actual = abi.make(abi.Tuple2[abi.Uint64, abi.StaticArray[abi.Bool, Literal[8]]]) + expected_type_spec = abi.TupleTypeSpec( + abi.Uint64TypeSpec(), abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 8) + ) + + assert actual.type_spec() == expected_type_spec + assert type(actual) is abi.Tuple + + +def test_size_of(): + values = [ + (abi.Uint8, 1), + (abi.Address, 32), + (abi.StaticArray[abi.Uint16, Literal[10]], 2 * 10), + ] + + for (t, s) in values: + assert abi.size_of(t) == s + + with pytest.raises(TealInputError): + abi.size_of(abi.String) + + +ABI_TRANSLATION_TEST_CASES = [ + # Test for byte/bool/address/strings + (algosdk.abi.ByteType(), "byte", abi.ByteTypeSpec(), abi.Byte), + (algosdk.abi.BoolType(), "bool", abi.BoolTypeSpec(), abi.Bool), + ( + algosdk.abi.AddressType(), + "address", + abi.AddressTypeSpec(), + abi.Address, + ), + (algosdk.abi.StringType(), "string", abi.StringTypeSpec(), abi.String), + # Test for dynamic array type + ( + algosdk.abi.ArrayDynamicType(algosdk.abi.UintType(32)), + "uint32[]", + abi.DynamicArrayTypeSpec(abi.Uint32TypeSpec()), + abi.DynamicArray[abi.Uint32], + ), + ( + algosdk.abi.ArrayDynamicType( + algosdk.abi.ArrayDynamicType(algosdk.abi.ByteType()) + ), + "byte[][]", + abi.DynamicArrayTypeSpec(abi.DynamicArrayTypeSpec(abi.ByteTypeSpec())), + abi.DynamicArray[abi.DynamicArray[abi.Byte]], + ), + # TODO: Turn these tests on when PyTeal supports ufixedx + # cf https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md#types + # ( + # algosdk.abi.ArrayDynamicType(algosdk.abi.UfixedType(256, 64)), + # "ufixed256x64[]", + # abi.DynamicArrayTypeSpec(abi.UfixedTypeSpec(256, 64)), + # ), + # # Test for static array type + # ( + # algosdk.abi.ArrayStaticType(algosdk.abi.UfixedType(128, 10), 100), + # "ufixed128x10[100]", + # abi.ArrayStaticTypeSpec(abi.UfixedTypeSpec(128, 10), 100), + # ), + ( + algosdk.abi.ArrayStaticType( + algosdk.abi.ArrayStaticType(algosdk.abi.BoolType(), 256), + 100, + ), + "bool[256][100]", + abi.StaticArrayTypeSpec( + abi.StaticArrayTypeSpec(abi.BoolTypeSpec(), 256), + 100, + ), + abi.StaticArray[abi.StaticArray[abi.Bool, Literal[256]], Literal[100]], + ), + # Test for tuple + (algosdk.abi.TupleType([]), "()", abi.TupleTypeSpec(), abi.Tuple0), + ( + algosdk.abi.TupleType( + [ + algosdk.abi.UintType(16), + algosdk.abi.TupleType( + [ + algosdk.abi.ByteType(), + algosdk.abi.ArrayStaticType(algosdk.abi.AddressType(), 10), + ] + ), + ] + ), + "(uint16,(byte,address[10]))", + abi.TupleTypeSpec( + abi.Uint16TypeSpec(), + abi.TupleTypeSpec( + abi.ByteTypeSpec(), + abi.StaticArrayTypeSpec(abi.AddressTypeSpec(), 10), + ), + ), + abi.Tuple2[ + abi.Uint16, + abi.Tuple2[ + abi.Byte, + abi.StaticArray[abi.Address, Literal[10]], + ], + ], + ), + ( + algosdk.abi.TupleType( + [ + algosdk.abi.UintType(64), + algosdk.abi.TupleType( + [ + algosdk.abi.ByteType(), + algosdk.abi.ArrayStaticType(algosdk.abi.AddressType(), 10), + ] + ), + algosdk.abi.TupleType([]), + algosdk.abi.BoolType(), + ] + ), + "(uint64,(byte,address[10]),(),bool)", + abi.TupleTypeSpec( + abi.Uint64TypeSpec(), + abi.TupleTypeSpec( + abi.ByteTypeSpec(), + abi.StaticArrayTypeSpec(abi.AddressTypeSpec(), 10), + ), + abi.TupleTypeSpec(), + abi.BoolTypeSpec(), + ), + abi.Tuple4[ + abi.Uint64, + abi.Tuple2[ + abi.Byte, + abi.StaticArray[abi.Address, Literal[10]], + ], + abi.Tuple, + abi.Bool, + ], + ), + # TODO: Turn the following test on when PyTeal supports ufixedx + # cf https://github.com/algorandfoundation/ARCs/blob/main/ARCs/arc-0004.md#types + # ( + # algosdk.abi.TupleType( + # [ + # algosdk.abi.UfixedType(256, 16), + # algosdk.abi.TupleType( + # [ + # algosdk.abi.TupleType( + # [ + # algosdk.abi.StringType(), + # ] + # ), + # algosdk.abi.BoolType(), + # algosdk.abi.TupleType( + # [ + # algosdk.abi.AddressType(), + # algosdk.abi.UintType(8), + # ] + # ), + # ] + # ), + # ] + # ), + # "(ufixed256x16,((string),bool,(address,uint8)))", + # abi.TupleType( + # [ + # abi.UfixedType(256, 16), + # abi.TupleType( + # [ + # abi.TupleType( + # [ + # abi.StringType(), + # ] + # ), + # abi.BoolType(), + # abi.TupleType( + # [ + # abi.AddressType(), + # abi.UintType(8), + # ] + # ), + # ] + # ), + # ] + # ), + # ), + ( + "cannot map ABI transaction type spec MaybeValue: AccountParam.__module__ = "pyteal" + + +class AccountParamObject: + """Represents information about an account""" + + def __init__(self, account: Expr) -> None: + """Create a new AccountParamObject for the given account. + + Args: + account: An index into Txn.accounts that corresponds to the application to check or an + address available at runtime. May evaluate to uint64 or bytes, respectively. + """ + self._account: Final = account + + def balance(self) -> MaybeValue: + """Get the current balance in microAlgos for the account""" + return AccountParam.balance(self._account) + + def min_balance(self) -> MaybeValue: + """Get the minimum balance in microAlgos for the account.""" + return AccountParam.minBalance(self._account) + + def auth_address(self) -> MaybeValue: + """Get the authorizing address for the account. + + If the account is not rekeyed, the empty address is returned.""" + return AccountParam.authAddr(self._account) + + +AccountParamObject.__module__ = "pyteal" diff --git a/pyteal/ast/acct_test.py b/pyteal/ast/acct_test.py index 52fc89132..66958b6ae 100644 --- a/pyteal/ast/acct_test.py +++ b/pyteal/ast/acct_test.py @@ -1,4 +1,5 @@ import pyteal as pt +from pyteal.ast.maybe_test import assert_MaybeValue_equality options = pt.CompileOptions() teal4Options = pt.CompileOptions(version=4) @@ -73,3 +74,23 @@ def test_acct_param_auth_addr_valid(): with pt.TealComponent.Context.ignoreExprEquality(): assert actual == expected + + +def test_AccountParamObject(): + for account in ( + pt.Int(7), + pt.Addr("QSA6K5MNJPEGO5SDSWXBM3K4UEI3Q2NCPS2OUXVJI5QPCHMVI27MFRSHKI"), + ): + obj = pt.AccountParamObject(account) + + assert obj._account is account + + assert_MaybeValue_equality( + obj.balance(), pt.AccountParam.balance(account), teal6Options + ) + assert_MaybeValue_equality( + obj.min_balance(), pt.AccountParam.minBalance(account), teal6Options + ) + assert_MaybeValue_equality( + obj.auth_address(), pt.AccountParam.authAddr(account), teal6Options + ) diff --git a/pyteal/ast/app.py b/pyteal/ast/app.py index b71512c68..62dbc0b26 100644 --- a/pyteal/ast/app.py +++ b/pyteal/ast/app.py @@ -1,4 +1,4 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Final from enum import Enum from pyteal.types import TealType, require_type @@ -356,3 +356,57 @@ def address(cls, app: Expr) -> MaybeValue: AppParam.__module__ = "pyteal" + + +class AppParamObject: + """Represents information about an application's parameters""" + + def __init__(self, app: Expr) -> None: + """Create a new AppParamObject for the given application. + + Args: + app: An identifier for the app. It must be an index into Txn.ForeignApps that + corresponds to the app to check, or since v4, an application ID that appears in + Txn.ForeignApps or is the CurrentApplicationID. In either case, it must evaluate to + uint64. + """ + require_type(app, TealType.uint64) + self._app: Final = app + + def approval_program(self) -> MaybeValue: + """Get the bytecode of Approval Program for the application.""" + return AppParam.approvalProgram(self._app) + + def clear_state_program(self) -> MaybeValue: + return AppParam.clearStateProgram(self._app) + + def global_num_uint(self) -> MaybeValue: + """Get the number of uint64 values allowed in Global State for the application.""" + return AppParam.globalNumUint(self._app) + + def global_num_byte_slice(self) -> MaybeValue: + """Get the number of byte array values allowed in Global State for the application.""" + return AppParam.globalNumByteSlice(self._app) + + def local_num_uint(self) -> MaybeValue: + """Get the number of uint64 values allowed in Local State for the application.""" + return AppParam.localNumUint(self._app) + + def local_num_byte_slice(self) -> MaybeValue: + """Get the number of byte array values allowed in Local State for the application.""" + return AppParam.localNumByteSlice(self._app) + + def extra_program_pages(self) -> MaybeValue: + """Get the number of Extra Program Pages of code space for the application.""" + return AppParam.extraProgramPages(self._app) + + def creator_address(self) -> MaybeValue: + """Get the creator address for the application.""" + return AppParam.creator(self._app) + + def address(self) -> MaybeValue: + """Get the escrow address for the application.""" + return AppParam.address(self._app) + + +AppParamObject.__module__ = "pyteal" diff --git a/pyteal/ast/app_test.py b/pyteal/ast/app_test.py index 79a6c1491..1e7dcdd79 100644 --- a/pyteal/ast/app_test.py +++ b/pyteal/ast/app_test.py @@ -1,6 +1,7 @@ import pytest import pyteal as pt +from pyteal.ast.maybe_test import assert_MaybeValue_equality options = pt.CompileOptions() teal4Options = pt.CompileOptions(version=4) @@ -674,3 +675,40 @@ def test_app_param_address_valid(): def test_app_param_address_invalid(): with pytest.raises(pt.TealTypeError): pt.AppParam.address(pt.Txn.sender()) + + +def test_AppParamObject(): + for app in (pt.Int(1), pt.Int(100)): + obj = pt.AppParamObject(app) + + assert obj._app is app + + assert_MaybeValue_equality( + obj.approval_program(), pt.AppParam.approvalProgram(app), teal5Options + ) + assert_MaybeValue_equality( + obj.clear_state_program(), pt.AppParam.clearStateProgram(app), teal5Options + ) + assert_MaybeValue_equality( + obj.global_num_uint(), pt.AppParam.globalNumUint(app), teal5Options + ) + assert_MaybeValue_equality( + obj.global_num_byte_slice(), + pt.AppParam.globalNumByteSlice(app), + teal5Options, + ) + assert_MaybeValue_equality( + obj.local_num_uint(), pt.AppParam.localNumUint(app), teal5Options + ) + assert_MaybeValue_equality( + obj.local_num_byte_slice(), pt.AppParam.localNumByteSlice(app), teal5Options + ) + assert_MaybeValue_equality( + obj.extra_program_pages(), pt.AppParam.extraProgramPages(app), teal5Options + ) + assert_MaybeValue_equality( + obj.creator_address(), pt.AppParam.creator(app), teal5Options + ) + assert_MaybeValue_equality( + obj.address(), pt.AppParam.address(app), teal5Options + ) diff --git a/pyteal/ast/asset.py b/pyteal/ast/asset.py index 5e15d0361..e44732b23 100644 --- a/pyteal/ast/asset.py +++ b/pyteal/ast/asset.py @@ -1,3 +1,5 @@ +from typing import Final + from pyteal.types import TealType, require_type from pyteal.ir import Op from pyteal.ast.expr import Expr @@ -51,6 +53,41 @@ def frozen(cls, account: Expr, asset: Expr) -> MaybeValue: AssetHolding.__module__ = "pyteal" +class AssetHoldingObject: + """Represents information about an account's holding of an asset""" + + def __init__(self, asset: Expr, account: Expr) -> None: + """Create a new AssetParamObject for the given asset. + + Args: + asset: An identifier for the asset. It must be an index into Txn.ForeignAssets that + corresponds to the asset to check, or since v4, an asset ID that appears in + Txn.ForeignAssets. In either case, it must evaluate to uint64. + account: An identifier for the account. It must be an index into Txn.Accounts that + corresponds to the account to check (in which case it must evaluate to uint64), or + since v4, an account address that appears in Txn.Accounts or is Txn.Sender (in which + case it must evaluate to bytes). + """ + require_type(asset, TealType.uint64) + self._asset: Final = asset + require_type(account, TealType.anytype) + self._account: Final = account + + def balance(self) -> MaybeValue: + """Get the amount of the asset held by the account.""" + return AssetHolding.balance(self._account, self._asset) + + def frozen(self) -> MaybeValue: + """Check if the asset is frozen for the account. + + A value of 1 indicates frozen and 0 indicates not frozen. + """ + return AssetHolding.frozen(self._account, self._asset) + + +AssetHoldingObject.__module__ = "pyteal" + + class AssetParam: @classmethod def total(cls, asset: Expr) -> MaybeValue: @@ -259,3 +296,71 @@ def creator(cls, asset: Expr) -> MaybeValue: AssetParam.__module__ = "pyteal" + + +class AssetParamObject: + """Represents information about an asset's parameters""" + + def __init__(self, asset: Expr) -> None: + """Create a new AssetParamObject for the given asset. + + Args: + asset: An identifier for the asset. It must be an index into Txn.ForeignAssets that + corresponds to the asset to check, or since v4, an asset ID that appears in + Txn.ForeignAssets. In either case, it must evaluate to uint64. + """ + require_type(asset, TealType.uint64) + self._asset: Final = asset + + def total(self) -> MaybeValue: + """Get the total number of units of the asset.""" + return AssetParam.total(self._asset) + + def decimals(self) -> MaybeValue: + """Get the number of decimals for the asset.""" + return AssetParam.decimals(self._asset) + + def default_frozen(self) -> MaybeValue: + """Check if the asset is frozen by default.""" + return AssetParam.defaultFrozen(self._asset) + + def unit_name(self) -> MaybeValue: + """Get the unit name of the asset.""" + return AssetParam.unitName(self._asset) + + def name(self) -> MaybeValue: + """Get the name of the asset.""" + return AssetParam.name(self._asset) + + def url(self) -> MaybeValue: + """Get the URL of the asset.""" + return AssetParam.url(self._asset) + + def metadata_hash(self) -> MaybeValue: + """Get the arbitrary commitment for the asset. + + If set, this will be 32 bytes long.""" + return AssetParam.metadataHash(self._asset) + + def manager_address(self) -> MaybeValue: + """Get the manager address for the asset.""" + return AssetParam.manager(self._asset) + + def reserve_address(self) -> MaybeValue: + """Get the reserve address for the asset.""" + return AssetParam.reserve(self._asset) + + def freeze_address(self) -> MaybeValue: + """Get the freeze address for the asset.""" + return AssetParam.freeze(self._asset) + + def clawback_address(self) -> MaybeValue: + """Get the clawback address for the asset.""" + return AssetParam.clawback(self._asset) + + def creator_address(self) -> MaybeValue: + """Get the creator address for the asset.""" + return AssetParam.creator(self._asset) + + +AssetParamObject.__module__ = "pyteal" diff --git a/pyteal/ast/asset_test.py b/pyteal/ast/asset_test.py index 6ac9cf0e1..642afd076 100644 --- a/pyteal/ast/asset_test.py +++ b/pyteal/ast/asset_test.py @@ -1,6 +1,7 @@ import pytest import pyteal as pt +from pyteal.ast.maybe_test import assert_MaybeValue_equality teal2Options = pt.CompileOptions() teal4Options = pt.CompileOptions(version=4) @@ -706,3 +707,62 @@ def test_asset_param_creator_valid(): def test_asset_param_creator_invalid(): with pytest.raises(pt.TealTypeError): pt.AssetParam.creator(pt.Txn.sender()) + + +def test_AssetHoldingObject(): + for asset in (pt.Int(1), pt.Int(100)): + for account in ( + pt.Int(7), + pt.Addr("QSA6K5MNJPEGO5SDSWXBM3K4UEI3Q2NCPS2OUXVJI5QPCHMVI27MFRSHKI"), + ): + obj = pt.AssetHoldingObject(asset, account) + + assert obj._asset is asset + assert obj._account is account + + assert_MaybeValue_equality( + obj.balance(), pt.AssetHolding.balance(account, asset), teal5Options + ) + assert_MaybeValue_equality( + obj.frozen(), pt.AssetHolding.frozen(account, asset), teal5Options + ) + + +def test_AssetParamObject(): + for asset in (pt.Int(1), pt.Int(100)): + obj = pt.AssetParamObject(asset) + + assert obj._asset is asset + + assert_MaybeValue_equality( + obj.total(), pt.AssetParam.total(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.decimals(), pt.AssetParam.decimals(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.default_frozen(), pt.AssetParam.defaultFrozen(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.unit_name(), pt.AssetParam.unitName(asset), teal5Options + ) + assert_MaybeValue_equality(obj.name(), pt.AssetParam.name(asset), teal5Options) + assert_MaybeValue_equality(obj.url(), pt.AssetParam.url(asset), teal5Options) + assert_MaybeValue_equality( + obj.metadata_hash(), pt.AssetParam.metadataHash(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.manager_address(), pt.AssetParam.manager(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.reserve_address(), pt.AssetParam.reserve(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.freeze_address(), pt.AssetParam.freeze(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.clawback_address(), pt.AssetParam.clawback(asset), teal5Options + ) + assert_MaybeValue_equality( + obj.creator_address(), pt.AssetParam.creator(asset), teal5Options + ) diff --git a/pyteal/ast/binaryexpr.py b/pyteal/ast/binaryexpr.py index c6f879f7c..36835ec46 100644 --- a/pyteal/ast/binaryexpr.py +++ b/pyteal/ast/binaryexpr.py @@ -46,7 +46,9 @@ def __teal__(self, options: "CompileOptions"): ) def __str__(self): - return "({} {} {})".format(self.op, self.argLeft, self.argRight) + return "({} {} {})".format( + str(self.op).title().replace("_", ""), self.argLeft, self.argRight + ) def type_of(self): return self.outputType @@ -58,19 +60,7 @@ def has_return(self): BinaryExpr.__module__ = "pyteal" -def Add(left: Expr, right: Expr) -> BinaryExpr: - """Add two numbers. - - Produces left + right. - - Args: - left: Must evaluate to uint64. - right: Must evaluate to uint64. - """ - return BinaryExpr(Op.add, TealType.uint64, TealType.uint64, left, right) - - -def Minus(left: Expr, right: Expr) -> BinaryExpr: +def Minus(left: Expr, right: Expr) -> Expr: """Subtract two numbers. Produces left - right. @@ -82,19 +72,7 @@ def Minus(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.minus, TealType.uint64, TealType.uint64, left, right) -def Mul(left: Expr, right: Expr) -> BinaryExpr: - """Multiply two numbers. - - Produces left * right. - - Args: - left: Must evaluate to uint64. - right: Must evaluate to uint64. - """ - return BinaryExpr(Op.mul, TealType.uint64, TealType.uint64, left, right) - - -def Div(left: Expr, right: Expr) -> BinaryExpr: +def Div(left: Expr, right: Expr) -> Expr: """Divide two numbers. Produces left / right. @@ -106,7 +84,7 @@ def Div(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.div, TealType.uint64, TealType.uint64, left, right) -def Mod(left: Expr, right: Expr) -> BinaryExpr: +def Mod(left: Expr, right: Expr) -> Expr: """Modulo expression. Produces left % right. @@ -118,7 +96,7 @@ def Mod(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.mod, TealType.uint64, TealType.uint64, left, right) -def Exp(a: Expr, b: Expr) -> BinaryExpr: +def Exp(a: Expr, b: Expr) -> Expr: """Exponential expression. Produces a ** b. @@ -132,7 +110,7 @@ def Exp(a: Expr, b: Expr) -> BinaryExpr: return BinaryExpr(Op.exp, TealType.uint64, TealType.uint64, a, b) -def BitwiseAnd(left: Expr, right: Expr) -> BinaryExpr: +def BitwiseAnd(left: Expr, right: Expr) -> Expr: """Bitwise and expression. Produces left & right. @@ -144,7 +122,7 @@ def BitwiseAnd(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.bitwise_and, TealType.uint64, TealType.uint64, left, right) -def BitwiseOr(left: Expr, right: Expr) -> BinaryExpr: +def BitwiseOr(left: Expr, right: Expr) -> Expr: """Bitwise or expression. Produces left | right. @@ -156,7 +134,7 @@ def BitwiseOr(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.bitwise_or, TealType.uint64, TealType.uint64, left, right) -def BitwiseXor(left: Expr, right: Expr) -> BinaryExpr: +def BitwiseXor(left: Expr, right: Expr) -> Expr: """Bitwise xor expression. Produces left ^ right. @@ -168,7 +146,7 @@ def BitwiseXor(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.bitwise_xor, TealType.uint64, TealType.uint64, left, right) -def ShiftLeft(a: Expr, b: Expr) -> BinaryExpr: +def ShiftLeft(a: Expr, b: Expr) -> Expr: """Bitwise left shift expression. Produces a << b. This is equivalent to a times 2^b, modulo 2^64. @@ -182,7 +160,7 @@ def ShiftLeft(a: Expr, b: Expr) -> BinaryExpr: return BinaryExpr(Op.shl, TealType.uint64, TealType.uint64, a, b) -def ShiftRight(a: Expr, b: Expr) -> BinaryExpr: +def ShiftRight(a: Expr, b: Expr) -> Expr: """Bitwise right shift expression. Produces a >> b. This is equivalent to a divided by 2^b. @@ -196,7 +174,7 @@ def ShiftRight(a: Expr, b: Expr) -> BinaryExpr: return BinaryExpr(Op.shr, TealType.uint64, TealType.uint64, a, b) -def Eq(left: Expr, right: Expr) -> BinaryExpr: +def Eq(left: Expr, right: Expr) -> Expr: """Equality expression. Checks if left == right. @@ -208,7 +186,7 @@ def Eq(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.eq, right.type_of(), TealType.uint64, left, right) -def Neq(left: Expr, right: Expr) -> BinaryExpr: +def Neq(left: Expr, right: Expr) -> Expr: """Difference expression. Checks if left != right. @@ -220,7 +198,7 @@ def Neq(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.neq, right.type_of(), TealType.uint64, left, right) -def Lt(left: Expr, right: Expr) -> BinaryExpr: +def Lt(left: Expr, right: Expr) -> Expr: """Less than expression. Checks if left < right. @@ -232,7 +210,7 @@ def Lt(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.lt, TealType.uint64, TealType.uint64, left, right) -def Le(left: Expr, right: Expr) -> BinaryExpr: +def Le(left: Expr, right: Expr) -> Expr: """Less than or equal to expression. Checks if left <= right. @@ -244,7 +222,7 @@ def Le(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.le, TealType.uint64, TealType.uint64, left, right) -def Gt(left: Expr, right: Expr) -> BinaryExpr: +def Gt(left: Expr, right: Expr) -> Expr: """Greater than expression. Checks if left > right. @@ -256,7 +234,7 @@ def Gt(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.gt, TealType.uint64, TealType.uint64, left, right) -def Ge(left: Expr, right: Expr) -> BinaryExpr: +def Ge(left: Expr, right: Expr) -> Expr: """Greater than or equal to expression. Checks if left >= right. @@ -268,7 +246,7 @@ def Ge(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.ge, TealType.uint64, TealType.uint64, left, right) -def GetBit(value: Expr, index: Expr) -> BinaryExpr: +def GetBit(value: Expr, index: Expr) -> Expr: """Get the bit value of an expression at a specific index. The meaning of index differs if value is an integer or a byte string. @@ -292,7 +270,7 @@ def GetBit(value: Expr, index: Expr) -> BinaryExpr: ) -def GetByte(value: Expr, index: Expr) -> BinaryExpr: +def GetByte(value: Expr, index: Expr) -> Expr: """Extract a single byte as an integer from a byte string. Similar to GetBit, indexing begins at the first byte. For example, :code:`GetByte(Bytes("base16", "0xff0000"), Int(0))` @@ -309,7 +287,7 @@ def GetByte(value: Expr, index: Expr) -> BinaryExpr: ) -def BytesAdd(left: Expr, right: Expr) -> BinaryExpr: +def BytesAdd(left: Expr, right: Expr) -> Expr: """Add two numbers as bytes. Produces left + right, where left and right are interpreted as big-endian unsigned integers. @@ -324,7 +302,7 @@ def BytesAdd(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_add, TealType.bytes, TealType.bytes, left, right) -def BytesMinus(left: Expr, right: Expr) -> BinaryExpr: +def BytesMinus(left: Expr, right: Expr) -> Expr: """Subtract two numbers as bytes. Produces left - right, where left and right are interpreted as big-endian unsigned integers. @@ -339,7 +317,7 @@ def BytesMinus(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_minus, TealType.bytes, TealType.bytes, left, right) -def BytesDiv(left: Expr, right: Expr) -> BinaryExpr: +def BytesDiv(left: Expr, right: Expr) -> Expr: """Divide two numbers as bytes. Produces left / right, where left and right are interpreted as big-endian unsigned integers. @@ -356,7 +334,7 @@ def BytesDiv(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_div, TealType.bytes, TealType.bytes, left, right) -def BytesMul(left: Expr, right: Expr) -> BinaryExpr: +def BytesMul(left: Expr, right: Expr) -> Expr: """Multiply two numbers as bytes. Produces left * right, where left and right are interpreted as big-endian unsigned integers. @@ -371,7 +349,7 @@ def BytesMul(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_mul, TealType.bytes, TealType.bytes, left, right) -def BytesMod(left: Expr, right: Expr) -> BinaryExpr: +def BytesMod(left: Expr, right: Expr) -> Expr: """Modulo expression with bytes as arguments. Produces left % right, where left and right are interpreted as big-endian unsigned integers. @@ -388,7 +366,7 @@ def BytesMod(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_mod, TealType.bytes, TealType.bytes, left, right) -def BytesAnd(left: Expr, right: Expr) -> BinaryExpr: +def BytesAnd(left: Expr, right: Expr) -> Expr: """Bitwise and expression with bytes as arguments. Produces left & right. @@ -404,7 +382,7 @@ def BytesAnd(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_and, TealType.bytes, TealType.bytes, left, right) -def BytesOr(left: Expr, right: Expr) -> BinaryExpr: +def BytesOr(left: Expr, right: Expr) -> Expr: """Bitwise or expression with bytes as arguments. Produces left | right. @@ -420,7 +398,7 @@ def BytesOr(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_or, TealType.bytes, TealType.bytes, left, right) -def BytesXor(left: Expr, right: Expr) -> BinaryExpr: +def BytesXor(left: Expr, right: Expr) -> Expr: """Bitwise xor expression with bytes as arguments. Produces left ^ right. @@ -436,7 +414,7 @@ def BytesXor(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_xor, TealType.bytes, TealType.bytes, left, right) -def BytesEq(left: Expr, right: Expr) -> BinaryExpr: +def BytesEq(left: Expr, right: Expr) -> Expr: """Equality expression with bytes as arguments. Checks if left == right, where left and right are interpreted as big-endian unsigned integers. @@ -451,7 +429,7 @@ def BytesEq(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_eq, TealType.bytes, TealType.uint64, left, right) -def BytesNeq(left: Expr, right: Expr) -> BinaryExpr: +def BytesNeq(left: Expr, right: Expr) -> Expr: """Difference expression with bytes as arguments. Checks if left != right, where left and right are interpreted as big-endian unsigned integers. @@ -466,7 +444,7 @@ def BytesNeq(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_neq, TealType.bytes, TealType.uint64, left, right) -def BytesLt(left: Expr, right: Expr) -> BinaryExpr: +def BytesLt(left: Expr, right: Expr) -> Expr: """Less than expression with bytes as arguments. Checks if left < right, where left and right are interpreted as big-endian unsigned integers. @@ -481,7 +459,7 @@ def BytesLt(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_lt, TealType.bytes, TealType.uint64, left, right) -def BytesLe(left: Expr, right: Expr) -> BinaryExpr: +def BytesLe(left: Expr, right: Expr) -> Expr: """Less than or equal to expression with bytes as arguments. Checks if left <= right, where left and right are interpreted as big-endian unsigned integers. @@ -496,7 +474,7 @@ def BytesLe(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_le, TealType.bytes, TealType.uint64, left, right) -def BytesGt(left: Expr, right: Expr) -> BinaryExpr: +def BytesGt(left: Expr, right: Expr) -> Expr: """Greater than expression with bytes as arguments. Checks if left > right, where left and right are interpreted as big-endian unsigned integers. @@ -511,7 +489,7 @@ def BytesGt(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_gt, TealType.bytes, TealType.uint64, left, right) -def BytesGe(left: Expr, right: Expr) -> BinaryExpr: +def BytesGe(left: Expr, right: Expr) -> Expr: """Greater than or equal to expression with bytes as arguments. Checks if left >= right, where left and right are interpreted as big-endian unsigned integers. @@ -526,7 +504,7 @@ def BytesGe(left: Expr, right: Expr) -> BinaryExpr: return BinaryExpr(Op.b_ge, TealType.bytes, TealType.uint64, left, right) -def ExtractUint16(string: Expr, offset: Expr) -> BinaryExpr: +def ExtractUint16(string: Expr, offset: Expr) -> Expr: """Extract 2 bytes (16 bits) and convert them to an integer. The bytes starting at :code:`offset` up to but not including :code:`offset + 2` will be @@ -549,7 +527,7 @@ def ExtractUint16(string: Expr, offset: Expr) -> BinaryExpr: ) -def ExtractUint32(string: Expr, offset: Expr) -> BinaryExpr: +def ExtractUint32(string: Expr, offset: Expr) -> Expr: """Extract 4 bytes (32 bits) and convert them to an integer. The bytes starting at :code:`offset` up to but not including :code:`offset + 4` will be @@ -572,7 +550,7 @@ def ExtractUint32(string: Expr, offset: Expr) -> BinaryExpr: ) -def ExtractUint64(string: Expr, offset: Expr) -> BinaryExpr: +def ExtractUint64(string: Expr, offset: Expr) -> Expr: """Extract 8 bytes (64 bits) and convert them to an integer. The bytes starting at :code:`offset` up to but not including :code:`offset + 8` will be diff --git a/pyteal/ast/bytes.py b/pyteal/ast/bytes.py index e88163611..4c4fb4d6e 100644 --- a/pyteal/ast/bytes.py +++ b/pyteal/ast/bytes.py @@ -15,14 +15,18 @@ class Bytes(LeafExpr): @overload def __init__(self, arg1: Union[str, bytes, bytearray]) -> None: - ... + pass @overload def __init__(self, arg1: str, arg2: str) -> None: - ... + pass def __init__(self, arg1: Union[str, bytes, bytearray], arg2: str = None) -> None: - """Create a new byte string. + """ + __init__(arg1: Union[str, bytes, bytearray]) -> None + __init__(self, arg1: str, arg2: str) -> None + + Create a new byte string. Depending on the encoding, there are different arguments to pass: diff --git a/pyteal/ast/expr.py b/pyteal/ast/expr.py index f246f33ac..c2de1ca32 100644 --- a/pyteal/ast/expr.py +++ b/pyteal/ast/expr.py @@ -70,7 +70,7 @@ def __ne__(self, other): return Neq(self, other) def __add__(self, other): - from pyteal.ast.binaryexpr import Add + from pyteal.ast.naryexpr import Add return Add(self, other) @@ -80,7 +80,7 @@ def __sub__(self, other): return Minus(self, other) def __mul__(self, other): - from pyteal.ast.binaryexpr import Mul + from pyteal.ast.naryexpr import Mul return Mul(self, other) diff --git a/pyteal/ast/int.py b/pyteal/ast/int.py index 26ef8cff5..f00f33bd2 100644 --- a/pyteal/ast/int.py +++ b/pyteal/ast/int.py @@ -33,7 +33,7 @@ def __teal__(self, options: "CompileOptions"): return TealBlock.FromOp(options, op) def __str__(self): - return "(Int: {})".format(self.value) + return "(Int {})".format(self.value) def type_of(self): return TealType.uint64 @@ -59,7 +59,7 @@ def __teal__(self, options: "CompileOptions"): return TealBlock.FromOp(options, op) def __str__(self): - return "(IntEnum: {})".format(self.name) + return "(IntEnum {})".format(self.name) def type_of(self): return TealType.uint64 diff --git a/pyteal/ast/itxn.py b/pyteal/ast/itxn.py index 2f5d2f064..ca5baa242 100644 --- a/pyteal/ast/itxn.py +++ b/pyteal/ast/itxn.py @@ -1,12 +1,18 @@ from enum import Enum -from typing import Dict, TYPE_CHECKING, List, Union, cast +from typing import TYPE_CHECKING, cast +import algosdk +from pyteal.ast.abi.util import type_specs_from_signature +from pyteal.ast.int import EnumInt +from pyteal.ast.methodsig import MethodSignature from pyteal.types import TealType, require_type -from pyteal.errors import TealInputError, verifyTealVersion +from pyteal.errors import TealInputError, TealTypeError, verifyTealVersion from pyteal.ir import TealOp, Op, TealBlock from pyteal.ast.expr import Expr -from pyteal.ast.txn import TxnField, TxnExprBuilder, TxnaExprBuilder, TxnObject +from pyteal.ast.txn import TxnField, TxnExprBuilder, TxnType, TxnaExprBuilder, TxnObject from pyteal.ast.seq import Seq +from pyteal.ast.bytes import Bytes +from pyteal.ast import abi if TYPE_CHECKING: from pyteal.compiler import CompileOptions @@ -135,7 +141,7 @@ def Submit(cls) -> Expr: return InnerTxnActionExpr(InnerTxnAction.Submit) @classmethod - def SetField(cls, field: TxnField, value: Union[Expr, List[Expr]]) -> Expr: + def SetField(cls, field: TxnField, value: Expr | list[Expr]) -> Expr: """Set a field of the current inner transaction. :any:`InnerTxnBuilder.Begin` must be called before setting any fields on an inner @@ -181,7 +187,7 @@ def SetField(cls, field: TxnField, value: Union[Expr, List[Expr]]) -> Expr: ) @classmethod - def Execute(cls, fields: Dict[TxnField, Union[Expr, List[Expr]]]) -> Expr: + def Execute(cls, fields: dict[TxnField, Expr | list[Expr]]) -> Expr: """Performs a single transaction given fields passed in. A convenience method that accepts fields to submit a single inner transaction, which is equivalent to: @@ -202,7 +208,7 @@ def Execute(cls, fields: Dict[TxnField, Union[Expr, List[Expr]]]) -> Expr: return Seq(cls.Begin(), cls.SetFields(fields), cls.Submit()) @classmethod - def SetFields(cls, fields: Dict[TxnField, Union[Expr, List[Expr]]]) -> Expr: + def SetFields(cls, fields: dict[TxnField, Expr | list[Expr]]) -> Expr: """Set multiple fields of the current inner transaction. :any:`InnerTxnBuilder.Begin` must be called before setting any fields on an inner @@ -221,6 +227,185 @@ def SetFields(cls, fields: Dict[TxnField, Union[Expr, List[Expr]]]) -> Expr: fieldsToSet = [cls.SetField(field, value) for field, value in fields.items()] return Seq(fieldsToSet) + @classmethod + def MethodCall( + cls, + *, + app_id: Expr, + method_signature: str, + args: list[abi.BaseType | Expr | dict[TxnField, Expr | list[Expr]]], + extra_fields: dict[TxnField, Expr | list[Expr]] = {}, + ) -> Expr: + """Adds an ABI method call transaction to the current inner transaction group. + + :any:`InnerTxnBuilder.Begin` must be called before a MethodCall can be added. + + Requires Teal version 6 or higher. This operation is only permitted in application mode. + + Args: + app_id: An expression that evaluates to a `TealType.uint64` corresponding to the application being called. + method_signature: A string representing the method signature of the method we're calling. This is used to do + type checking on the arguments passed and to create the method selector passed as the first argument. + args: A list of arguments to pass to the application. The values in this list depend on the kind of argument you wish to pass: + + - For basic ABI arguments (not Reference or Transaction types): + If an ABI type is passed it **MUST** match the type specified in the `method_signature`. If an Expr is passed it must evaluate to `TealType.bytes` but beyond that no type checking is performed. + + - For Reference arguments: + Either the Reference type or an Expr that returns the type corresponding to the reference type are allowed. + (i.e. Asset is TealType.uint64, Application is TealType.uint64, Account is TealType.bytes) + + - For Transaction arguments: + A dictionary containing TxnField to Expr that describe Transactions to be pre-pended to the transaction group being constructed. The `TxnField.type_enum` key MUST be set and MUST match the expected transaction type specified in the `method_signature`. + + extra_fields (optional): A dictionary whose keys are fields to set and whose values are the value each + field should take. Each value must evaluate to a type that is compatible with the + field being set. These fields are set on the ApplicationCallTransaction being constructed + """ + + require_type(app_id, TealType.uint64) + + # Default, always need these + fields_to_set = [ + cls.SetField(TxnField.type_enum, TxnType.ApplicationCall), + cls.SetField(TxnField.application_id, app_id), + ] + + # We only care about the args + arg_type_specs: list[abi.TypeSpec] + arg_type_specs, _ = type_specs_from_signature(method_signature) + + if len(args) != len(arg_type_specs): + raise TealInputError( + f"Expected {len(arg_type_specs)} arguments, got {len(args)}" + ) + + # Start app args with the method selector + app_args: list[Expr] = [MethodSignature(method_signature)] + + # Transactions are not included in the App Call + txns_to_pass: list[Expr] = [] + + # Reference Types are treated specially + accts: list[Expr] = [] + apps: list[Expr] = [] + assets: list[Expr] = [] + + for idx, method_arg_ts in enumerate(arg_type_specs): + arg = args[idx] + + if method_arg_ts in abi.TransactionTypeSpecs: + if not isinstance(arg, dict): + raise TealTypeError(arg, dict[TxnField, Expr | list[Expr]]) + + if TxnField.type_enum not in arg: + raise TealInputError( + f"Expected Transaction at arg {idx} to contain field type_enum" + ) + + if type(arg[TxnField.type_enum]) is not EnumInt: + raise TealTypeError(arg[TxnField.type_enum], EnumInt) + + txntype = cast(EnumInt, arg[TxnField.type_enum]).name + # If the arg is an unspecified transaction, no need to check the type_enum + if not type( + method_arg_ts + ) is abi.TransactionTypeSpec and txntype != str(method_arg_ts): + raise TealInputError( + f"Expected Transaction at arg {idx} to be {method_arg_ts}, got {txntype}" + ) + + txns_to_pass.append(InnerTxnBuilder.SetFields(arg)) + + elif method_arg_ts in abi.ReferenceTypeSpecs: + match method_arg_ts: + # For both acct and application, add index to + # app args _after_ appending since 0 is implicitly set + case abi.AccountTypeSpec(): + if isinstance(arg, Expr): + # require the address is passed + require_type(arg, TealType.bytes) + accts.append(arg) + elif isinstance(arg, abi.Account): + accts.append(arg.address()) + else: + raise TealTypeError(arg, abi.Account | Expr) + + app_args.append( + Bytes( + algosdk.abi.ABIType.from_string("uint8").encode( + len(accts) + ) + ) + ) + + case abi.ApplicationTypeSpec(): + if isinstance(arg, Expr): + # require the app id be passed + require_type(arg, TealType.uint64) + apps.append(arg) + elif isinstance(arg, abi.Application): + apps.append(arg.application_id()) + else: + raise TealTypeError(arg, abi.Application | Expr) + + app_args.append( + Bytes( + algosdk.abi.ABIType.from_string("uint8").encode( + len(apps) + ) + ) + ) + + # For assets, add to app_args prior to appending to assets array + case abi.AssetTypeSpec(): + app_args.append( + Bytes( + algosdk.abi.ABIType.from_string("uint8").encode( + len(assets) + ) + ) + ) + + if isinstance(arg, Expr): + require_type(arg, TealType.uint64) + assets.append(arg) + elif isinstance(arg, abi.Asset): + assets.append(arg.asset_id()) + else: + raise TealTypeError(arg, abi.Asset | Expr) + else: + if isinstance(arg, Expr): + # This should _always_ be bytes, since we assume its already abi encoded + require_type(arg, TealType.bytes) + app_args.append(arg) + elif isinstance(arg, abi.BaseType): + if arg.type_spec() != method_arg_ts: + raise TealTypeError(arg.type_spec(), method_arg_ts) + app_args.append(arg.encode()) + else: + raise TealTypeError(arg, abi.BaseType | Expr) + + if len(accts) > 0: + fields_to_set.append(cls.SetField(TxnField.accounts, accts)) + + if len(apps) > 0: + fields_to_set.append(cls.SetField(TxnField.applications, apps)) + + if len(assets) > 0: + fields_to_set.append(cls.SetField(TxnField.assets, assets)) + + fields_to_set.append(cls.SetField(TxnField.application_args, app_args)) + + return Seq( + # Add the transactions first + *[Seq(ttp, InnerTxnBuilder.Next()) for ttp in txns_to_pass], + # Set the fields for the app call in app args and foreign arrays + *fields_to_set, + # Add any remaining fields specified by the user + InnerTxnBuilder.SetFields(extra_fields), + ) + InnerTxnBuilder.__module__ = "pyteal" diff --git a/pyteal/ast/itxn_test.py b/pyteal/ast/itxn_test.py index 2e9c01a75..51f40fa62 100644 --- a/pyteal/ast/itxn_test.py +++ b/pyteal/ast/itxn_test.py @@ -1,6 +1,7 @@ import pytest import pyteal as pt +from pyteal.ast.txn import TxnField, TxnType from pyteal.types import types_match teal4Options = pt.CompileOptions(version=4) @@ -158,4 +159,284 @@ def test_InnerTxnBuilder_Execute(): expr.__teal__(teal4Options) +ITXN_METHOD_CASES = ( + ( + pt.Int(1), + "add(uint64,uint64)void", + [t1_1 := pt.Itob(pt.Int(1)), t1_2 := pt.Itob(pt.Int(1))], + {TxnField.fee: pt.Int(0)}, + pt.Seq( + pt.InnerTxnBuilder.SetFields( + { + pt.TxnField.type_enum: TxnType.ApplicationCall, + pt.TxnField.application_id: pt.Int(1), + pt.TxnField.application_args: [ + pt.MethodSignature("add(uint64,uint64)void"), + t1_1, + t1_2, + ], + pt.TxnField.fee: pt.Int(0), + } + ), + ), + None, + ), + ( + pt.Int(1), + "add(uint64,uint64)void", + [t2_1 := pt.abi.Uint64(), t2_2 := pt.abi.Uint64()], + {TxnField.fee: pt.Int(0)}, + pt.Seq( + pt.InnerTxnBuilder.SetFields( + { + pt.TxnField.type_enum: TxnType.ApplicationCall, + pt.TxnField.application_id: pt.Int(1), + pt.TxnField.application_args: [ + pt.MethodSignature("add(uint64,uint64)void"), + t2_1.encode(), + t2_2.encode(), + ], + pt.TxnField.fee: pt.Int(0), + } + ), + ), + None, + ), + ( + pt.Int(1), + "add(application,account,asset)void", + [ + t3_1 := pt.abi.Application(), + t3_2 := pt.abi.Account(), + t3_3 := pt.abi.Asset(), + ], + {TxnField.fee: pt.Int(0)}, + pt.Seq( + pt.InnerTxnBuilder.SetFields( + { + pt.TxnField.type_enum: TxnType.ApplicationCall, + pt.TxnField.application_id: pt.Int(1), + pt.TxnField.accounts: [t3_2.address()], + pt.TxnField.applications: [t3_1.application_id()], + pt.TxnField.assets: [t3_3.asset_id()], + pt.TxnField.application_args: [ + pt.MethodSignature("add(application,account,asset)void"), + pt.Bytes(b"\x01"), + pt.Bytes(b"\x01"), + pt.Bytes(b"\x00"), + ], + pt.TxnField.fee: pt.Int(0), + } + ), + ), + None, + ), + ( + pt.Int(1), + "add(application,account,asset)void", + [ + t4_1 := pt.Int(1), + t4_2 := pt.Global.zero_address(), + t4_3 := pt.Int(2), + ], + {TxnField.fee: pt.Int(0)}, + pt.Seq( + pt.InnerTxnBuilder.SetFields( + { + pt.TxnField.type_enum: TxnType.ApplicationCall, + pt.TxnField.application_id: pt.Int(1), + pt.TxnField.accounts: [t4_2], + pt.TxnField.applications: [t4_1], + pt.TxnField.assets: [t4_3], + pt.TxnField.application_args: [ + pt.MethodSignature("add(application,account,asset)void"), + pt.Bytes(b"\x01"), + pt.Bytes(b"\x01"), + pt.Bytes(b"\x00"), + ], + pt.TxnField.fee: pt.Int(0), + } + ), + ), + None, + ), + ( + pt.Int(1), + "add(pay,txn,appl)void", + [ + t5_1 := {TxnField.type_enum: TxnType.Payment}, + t5_2 := {TxnField.type_enum: TxnType.AssetTransfer}, + t5_3 := {TxnField.type_enum: TxnType.ApplicationCall}, + ], + {TxnField.fee: pt.Int(0)}, + pt.Seq( + pt.InnerTxnBuilder.SetFields(t5_1), # type: ignore + pt.InnerTxnBuilder.Next(), + pt.InnerTxnBuilder.SetFields(t5_2), # type: ignore + pt.InnerTxnBuilder.Next(), + pt.InnerTxnBuilder.SetFields(t5_3), # type: ignore + pt.InnerTxnBuilder.Next(), + pt.InnerTxnBuilder.SetFields( + { + pt.TxnField.type_enum: TxnType.ApplicationCall, + pt.TxnField.application_id: pt.Int(1), + pt.TxnField.application_args: [ + pt.MethodSignature("add(pay,txn,appl)void"), + ], + pt.TxnField.fee: pt.Int(0), + } + ), + ), + None, + ), + # Error cases + ( + pt.Int(1), + "add(pay,txn,appl)void", + [ + {}, + {TxnField.type_enum: TxnType.AssetTransfer}, + {TxnField.type_enum: TxnType.ApplicationCall}, + ], + None, + None, + pt.TealInputError, + ), + ( + pt.Int(1), + "add(pay,txn,appl)void", + [ + {TxnField.type_enum: pt.Int(10)}, + {TxnField.type_enum: TxnType.AssetTransfer}, + {TxnField.type_enum: TxnType.ApplicationCall}, + ], + None, + None, + pt.TealTypeError, + ), + ( + pt.Int(1), + "add(pay,txn,appl)void", + [ + {TxnField.type_enum: TxnType.ApplicationCall}, + {TxnField.type_enum: TxnType.AssetTransfer}, + {TxnField.type_enum: TxnType.ApplicationCall}, + ], + None, + None, + pt.TealInputError, + ), + ( + pt.Int(1), + "add(application,account,asset)void", + [ + pt.abi.Asset(), + pt.abi.Account(), + pt.abi.Asset(), + ], + None, + None, + pt.TealTypeError, + ), + ( + pt.Int(1), + "add(application)void", + [ + pt.Bytes(""), + ], + None, + None, + pt.TealTypeError, + ), + ( + pt.Int(1), + "add(asset)void", + [ + pt.Bytes(""), + ], + None, + None, + pt.TealTypeError, + ), + ( + pt.Int(1), + "add(account)void", + [ + pt.Int(1), + ], + None, + None, + pt.TealTypeError, + ), + ( + pt.Int(1), + "add(uint64,uint64)void", + [pt.abi.String(), pt.abi.Uint64()], + None, + None, + pt.TealTypeError, + ), + ( + pt.Int(1), + "add(uint64,uint64)void", + [pt.abi.Uint64()], + None, + None, + pt.TealInputError, + ), + ( + pt.Int(1), + "add(uint64,uint64)void", + [pt.abi.Uint64(), pt.abi.Uint64(), pt.abi.Uint64()], + None, + None, + pt.TealInputError, + ), +) + + +@pytest.mark.parametrize( + "app_id, sig, args, extra_fields, expected_expr, expected_error", ITXN_METHOD_CASES +) +def test_InnerTxnBuilder_method_call( + app_id: pt.Expr, + sig: str, + args: list[pt.abi.BaseType | pt.Expr | dict[pt.TxnField, pt.Expr | list[pt.Expr]]], + extra_fields: dict[pt.TxnField, pt.Expr | list[pt.Expr]], + expected_expr: pt.Expr, + expected_error: type[Exception], +): + + if expected_error is not None: + with pytest.raises(expected_error): + pt.InnerTxnBuilder.MethodCall( + app_id=app_id, + method_signature=sig, + args=args, + extra_fields=extra_fields, + ) + return + + expr: pt.Expr = pt.InnerTxnBuilder.MethodCall( + app_id=app_id, method_signature=sig, args=args, extra_fields=extra_fields + ) + assert expr.type_of() == pt.TealType.none + assert not expr.has_return() + + expected, _ = expected_expr.__teal__(teal6Options) + expected.addIncoming() + expected = pt.TealBlock.NormalizeBlocks(expected) + + actual, _ = expr.__teal__(teal6Options) + actual.addIncoming() + actual = pt.TealBlock.NormalizeBlocks(actual) + + with pt.TealComponent.Context.ignoreScratchSlotEquality(), pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + assert pt.TealBlock.MatchScratchSlotReferences( + pt.TealBlock.GetReferencedScratchSlots(actual), + pt.TealBlock.GetReferencedScratchSlots(expected), + ) + + # txn_test.py performs additional testing diff --git a/pyteal/ast/maybe_test.py b/pyteal/ast/maybe_test.py index fa57ba5ca..0b4da7471 100644 --- a/pyteal/ast/maybe_test.py +++ b/pyteal/ast/maybe_test.py @@ -3,6 +3,26 @@ options = pt.CompileOptions() +def assert_MaybeValue_equality( + actual: pt.MaybeValue, expected: pt.MaybeValue, options: pt.CompileOptions +): + actual_block, _ = actual.__teal__(options) + actual_block.addIncoming() + actual_block = pt.TealBlock.NormalizeBlocks(actual_block) + + expected_block, _ = expected.__teal__(options) + expected_block.addIncoming() + expected_block = pt.TealBlock.NormalizeBlocks(expected_block) + + with pt.TealComponent.Context.ignoreExprEquality(), pt.TealComponent.Context.ignoreScratchSlotEquality(): + assert actual_block == expected_block + + assert pt.TealBlock.MatchScratchSlotReferences( + pt.TealBlock.GetReferencedScratchSlots(actual_block), + pt.TealBlock.GetReferencedScratchSlots(expected_block), + ) + + def test_maybe_value(): ops = ( pt.Op.app_global_get_ex, diff --git a/pyteal/ast/methodsig.py b/pyteal/ast/methodsig.py index 9417890de..66a809fe8 100644 --- a/pyteal/ast/methodsig.py +++ b/pyteal/ast/methodsig.py @@ -33,7 +33,7 @@ def __teal__(self, options: "CompileOptions"): return TealBlock.FromOp(options, op) def __str__(self) -> str: - return "(method: {})".format(self.methodName) + return "(MethodSignature '{}')".format(self.methodName) def type_of(self) -> TealType: return TealType.bytes diff --git a/pyteal/ast/naryexpr.py b/pyteal/ast/naryexpr.py index 6b05cf68c..0af86d2bb 100644 --- a/pyteal/ast/naryexpr.py +++ b/pyteal/ast/naryexpr.py @@ -48,7 +48,7 @@ def __teal__(self, options: "CompileOptions"): return start, end def __str__(self): - ret_str = "(" + str(self.op) + ret_str = "(" + str(self.op).title().replace("_", "") for a in self.args: ret_str += " " + a.__str__() ret_str += ")" @@ -64,7 +64,35 @@ def has_return(self): NaryExpr.__module__ = "pyteal" -def And(*args: Expr) -> NaryExpr: +def Add(*args: Expr) -> Expr: + """Numerical addition. + + Produces the sum of all the input arguments. + + All arguments must be PyTeal expressions that evaluate to uint64, and there must be at least one + argument. + + Example: + ``Add(Int(1), Int(2), Int(3))`` + """ + return NaryExpr(Op.add, TealType.uint64, TealType.uint64, args) + + +def Mul(*args: Expr) -> Expr: + """Numerical multiplication. + + Produces the product of all the input arguments. + + All arguments must be PyTeal expressions that evaluate to uint64, and there must be at least one + argument. + + Example: + ``Mul(Int(2), Int(3), Int(4))`` + """ + return NaryExpr(Op.mul, TealType.uint64, TealType.uint64, args) + + +def And(*args: Expr) -> Expr: """Logical and expression. Produces 1 if all arguments are nonzero. Otherwise produces 0. @@ -78,7 +106,7 @@ def And(*args: Expr) -> NaryExpr: return NaryExpr(Op.logic_and, TealType.uint64, TealType.uint64, args) -def Or(*args: Expr) -> NaryExpr: +def Or(*args: Expr) -> Expr: """Logical or expression. Produces 1 if any argument is nonzero. Otherwise produces 0. @@ -89,7 +117,7 @@ def Or(*args: Expr) -> NaryExpr: return NaryExpr(Op.logic_or, TealType.uint64, TealType.uint64, args) -def Concat(*args: Expr) -> NaryExpr: +def Concat(*args: Expr) -> Expr: """Concatenate byte strings. Produces a new byte string consisting of the contents of each of the passed in byte strings diff --git a/pyteal/ast/pragma.py b/pyteal/ast/pragma.py new file mode 100644 index 000000000..0fd5acead --- /dev/null +++ b/pyteal/ast/pragma.py @@ -0,0 +1,63 @@ +from typing import TYPE_CHECKING, Any + +from pyteal.ast.expr import Expr +from pyteal.pragma import is_valid_compiler_version, pragma + +if TYPE_CHECKING: + from pyteal.compiler import CompileOptions + + +class Pragma(Expr): + """A meta expression which defines a pragma for a specific subsection of PyTeal code. + + This expression does not affect the underlying compiled TEAL code in any way.""" + + def __init__(self, child: Expr, *, compiler_version: str, **kwargs: Any) -> None: + """Define a pragma for a specific subsection of PyTeal code. + + The Pragma expression does not affect the underlying compiled TEAL code in any way, + it merely sets a pragma for the underlying expression. + + Args: + child: The expression to wrap. + compiler_version: Acceptable versions of the compiler. Will fail if the current PyTeal version + is not contained in the range. Follows the npm `semver range scheme `_ + for specifying compatible versions. + + For example: + + .. code-block:: python + + @Subroutine(TealType.uint64) + def example() -> Expr: + # this will fail during compilation if the current PyTeal version does not satisfy + # the version constraint + return Pragma( + Seq(...), + compiler_version="^0.14.0" + ) + """ + super().__init__() + + self.child = child + + if not is_valid_compiler_version(compiler_version): + raise ValueError("Invalid compiler version: {}".format(compiler_version)) + self.compiler_version = compiler_version + + def __teal__(self, options: "CompileOptions"): + pragma(compiler_version=self.compiler_version) + + return self.child.__teal__(options) + + def __str__(self): + return "(pragma {})".format(self.child) + + def type_of(self): + return self.child.type_of() + + def has_return(self): + return self.child.has_return() + + +Pragma.__module__ = "pyteal" diff --git a/pyteal/ast/pragma_test.py b/pyteal/ast/pragma_test.py new file mode 100644 index 000000000..7c8d5c917 --- /dev/null +++ b/pyteal/ast/pragma_test.py @@ -0,0 +1,59 @@ +import pytest +from tests.mock_version import mock_version # noqa: F401 + +import pyteal as pt + + +@pytest.mark.usefixtures("mock_version") +@pytest.mark.parametrize( + "version, compiler_version, should_error", + [ + # valid + ("0.12.0", "0.12.0", False), + ( + "1.0.0+AVM7.1", + "=1.0.0", + False, + ), + # invalid + ("0.13.0", "0.13.1", True), + ("1.2.3a2", "<0.8.0 || >=0.12.0", True), + ], +) +def test_pragma_expr(compiler_version, should_error): + program = pt.Pragma(pt.Approve(), compiler_version=compiler_version) + + if should_error: + with pytest.raises(pt.TealPragmaError): + pt.compileTeal(program, mode=pt.Mode.Application, version=6) + else: + pt.compileTeal(program, mode=pt.Mode.Application, version=6) + + +def test_pragma_expr_does_not_change(): + without_pragma = pt.Seq(pt.Pop(pt.Add(pt.Int(1), pt.Int(2))), pt.Return(pt.Int(1))) + pragma = pt.Pragma(without_pragma, compiler_version=">=0.0.0") + + compiled_with_pragma = pt.compileTeal(pragma, mode=pt.Mode.Application, version=6) + compiled_without_pragma = pt.compileTeal( + without_pragma, mode=pt.Mode.Application, version=6 + ) + + assert compiled_with_pragma == compiled_without_pragma + + +def test_pragma_expr_has_return(): + exprWithReturn = pt.Pragma(pt.Return(pt.Int(1)), compiler_version=">=0.0.0") + assert exprWithReturn.has_return() + + exprWithoutReturn = pt.Pragma(pt.Int(1), compiler_version=">=0.0.0") + assert not exprWithoutReturn.has_return() + + +@pytest.mark.parametrize( + "compiler_version", + ["not a version", ">=0.1.1,<0.3.0", "1.2.3aq"], # incorrect spec # invalid PEP 440 +) +def test_pragma_expr_invalid_compiler_version(compiler_version): + with pytest.raises(ValueError): + pt.Pragma(pt.Approve(), compiler_version=compiler_version) diff --git a/pyteal/ast/return_.py b/pyteal/ast/return_.py index be27155fe..a19d894f0 100644 --- a/pyteal/ast/return_.py +++ b/pyteal/ast/return_.py @@ -36,7 +36,7 @@ def __teal__(self, options: "CompileOptions"): options.version, "TEAL version too low to use subroutines", ) - returnType = options.currentSubroutine.returnType + returnType = options.currentSubroutine.return_type if returnType == TealType.none: if self.value is not None: raise TealCompileError( @@ -73,10 +73,7 @@ def __teal__(self, options: "CompileOptions"): ) op = Op.return_ - args = [] - if self.value is not None: - args.append(self.value) - + args = [] if self.value is None else [self.value] return TealBlock.FromOp(options, TealOp(self, op), *args) def __str__(self): diff --git a/pyteal/ast/router.py b/pyteal/ast/router.py new file mode 100644 index 000000000..ff7ed1f0f --- /dev/null +++ b/pyteal/ast/router.py @@ -0,0 +1,765 @@ +from dataclasses import dataclass, field, fields, astuple +from typing import cast, Optional, Callable +from enum import IntFlag + +from algosdk import abi as sdk_abi +from algosdk import encoding + +from pyteal.config import METHOD_ARG_NUM_CUTOFF +from pyteal.errors import ( + TealInputError, + TealInternalError, +) +from pyteal.types import TealType +from pyteal.compiler.compiler import compileTeal, DEFAULT_TEAL_VERSION, OptimizeOptions +from pyteal.ir.ops import Mode + +from pyteal.ast import abi +from pyteal.ast.subroutine import ( + OutputKwArgInfo, + SubroutineFnWrapper, + ABIReturnSubroutine, +) +from pyteal.ast.assert_ import Assert +from pyteal.ast.cond import Cond +from pyteal.ast.expr import Expr +from pyteal.ast.app import OnComplete +from pyteal.ast.int import Int, EnumInt +from pyteal.ast.seq import Seq +from pyteal.ast.methodsig import MethodSignature +from pyteal.ast.naryexpr import And, Or +from pyteal.ast.txn import Txn +from pyteal.ast.return_ import Approve, Reject + + +class CallConfig(IntFlag): + """ + CallConfig: a "bitset"-like class for more fine-grained control over + `call or create` for a method about an OnComplete case. + + This enumeration class allows for specifying one of the four following cases: + - CALL + - CREATE + - ALL + - NEVER + for a method call on one on_complete case. + """ + + NEVER = 0 + CALL = 1 + CREATE = 2 + ALL = 3 + + def approval_condition_under_config(self) -> Expr | int: + match self: + case CallConfig.NEVER: + return 0 + case CallConfig.CALL: + return Txn.application_id() != Int(0) + case CallConfig.CREATE: + return Txn.application_id() == Int(0) + case CallConfig.ALL: + return 1 + case _: + raise TealInternalError(f"unexpected CallConfig {self}") + + def clear_state_condition_under_config(self) -> int: + match self: + case CallConfig.NEVER: + return 0 + case CallConfig.CALL: + return 1 + case CallConfig.CREATE | CallConfig.ALL: + raise TealInputError( + "Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation" + ) + case _: + raise TealInputError(f"unexpected CallConfig {self}") + + +CallConfig.__module__ = "pyteal" + + +@dataclass(frozen=True) +class MethodConfig: + """ + MethodConfig keep track of one method's CallConfigs for all OnComplete cases. + + The `MethodConfig` implementation generalized contract method call such that the registered + method call is paired with certain OnCompletion conditions and creation conditions. + """ + + no_op: CallConfig = field(kw_only=True, default=CallConfig.NEVER) + opt_in: CallConfig = field(kw_only=True, default=CallConfig.NEVER) + close_out: CallConfig = field(kw_only=True, default=CallConfig.NEVER) + clear_state: CallConfig = field(kw_only=True, default=CallConfig.NEVER) + update_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER) + delete_application: CallConfig = field(kw_only=True, default=CallConfig.NEVER) + + def is_never(self) -> bool: + return all(map(lambda cc: cc == CallConfig.NEVER, astuple(self))) + + def approval_cond(self) -> Expr | int: + config_oc_pairs: list[tuple[CallConfig, EnumInt]] = [ + (self.no_op, OnComplete.NoOp), + (self.opt_in, OnComplete.OptIn), + (self.close_out, OnComplete.CloseOut), + (self.update_application, OnComplete.UpdateApplication), + (self.delete_application, OnComplete.DeleteApplication), + ] + if all(config == CallConfig.NEVER for config, _ in config_oc_pairs): + return 0 + elif all(config == CallConfig.ALL for config, _ in config_oc_pairs): + return 1 + else: + cond_list = [] + for config, oc in config_oc_pairs: + config_cond = config.approval_condition_under_config() + match config_cond: + case Expr(): + cond_list.append(And(Txn.on_completion() == oc, config_cond)) + case 1: + cond_list.append(Txn.on_completion() == oc) + case 0: + continue + case _: + raise TealInternalError( + f"unexpected condition_under_config: {config_cond}" + ) + return Or(*cond_list) + + def clear_state_cond(self) -> Expr | int: + return self.clear_state.clear_state_condition_under_config() + + +@dataclass(frozen=True) +class OnCompleteAction: + """ + OnComplete Action, registers bare calls to one single OnCompletion case. + """ + + action: Optional[Expr | SubroutineFnWrapper | ABIReturnSubroutine] = field( + kw_only=True, default=None + ) + call_config: CallConfig = field(kw_only=True, default=CallConfig.NEVER) + + def __post_init__(self): + if bool(self.call_config) ^ bool(self.action): + raise TealInputError( + f"action {self.action} and call_config {self.call_config!r} contradicts" + ) + + @staticmethod + def never() -> "OnCompleteAction": + return OnCompleteAction() + + @staticmethod + def create_only( + f: Expr | SubroutineFnWrapper | ABIReturnSubroutine, + ) -> "OnCompleteAction": + return OnCompleteAction(action=f, call_config=CallConfig.CREATE) + + @staticmethod + def call_only( + f: Expr | SubroutineFnWrapper | ABIReturnSubroutine, + ) -> "OnCompleteAction": + return OnCompleteAction(action=f, call_config=CallConfig.CALL) + + @staticmethod + def always( + f: Expr | SubroutineFnWrapper | ABIReturnSubroutine, + ) -> "OnCompleteAction": + return OnCompleteAction(action=f, call_config=CallConfig.ALL) + + def is_empty(self) -> bool: + return not self.action and self.call_config == CallConfig.NEVER + + +OnCompleteAction.__module__ = "pyteal" + + +@dataclass(frozen=True) +class BareCallActions: + """ + BareCallActions keep track of bare-call registrations to all OnCompletion cases. + """ + + close_out: OnCompleteAction = field(kw_only=True, default=OnCompleteAction.never()) + clear_state: OnCompleteAction = field( + kw_only=True, default=OnCompleteAction.never() + ) + delete_application: OnCompleteAction = field( + kw_only=True, default=OnCompleteAction.never() + ) + no_op: OnCompleteAction = field(kw_only=True, default=OnCompleteAction.never()) + opt_in: OnCompleteAction = field(kw_only=True, default=OnCompleteAction.never()) + update_application: OnCompleteAction = field( + kw_only=True, default=OnCompleteAction.never() + ) + + def is_empty(self) -> bool: + for action_field in fields(self): + action: OnCompleteAction = getattr(self, action_field.name) + if not action.is_empty(): + return False + return True + + def approval_construction(self) -> Optional[Expr]: + oc_action_pair: list[tuple[EnumInt, OnCompleteAction]] = [ + (OnComplete.NoOp, self.no_op), + (OnComplete.OptIn, self.opt_in), + (OnComplete.CloseOut, self.close_out), + (OnComplete.UpdateApplication, self.update_application), + (OnComplete.DeleteApplication, self.delete_application), + ] + if all(oca.is_empty() for _, oca in oc_action_pair): + return None + conditions_n_branches: list[CondNode] = list() + for oc, oca in oc_action_pair: + if oca.is_empty(): + continue + wrapped_handler = ASTBuilder.wrap_handler( + False, + cast(Expr | SubroutineFnWrapper | ABIReturnSubroutine, oca.action), + ) + match oca.call_config: + case CallConfig.ALL: + cond_body = wrapped_handler + case CallConfig.CALL | CallConfig.CREATE: + cond_body = Seq( + Assert( + cast( + Expr, oca.call_config.approval_condition_under_config() + ) + ), + wrapped_handler, + ) + case _: + raise TealInternalError( + f"Unexpected CallConfig: {oca.call_config!r}" + ) + conditions_n_branches.append( + CondNode( + Txn.on_completion() == oc, + cond_body, + ) + ) + return Cond(*[[n.condition, n.branch] for n in conditions_n_branches]) + + def clear_state_construction(self) -> Optional[Expr]: + if self.clear_state.is_empty(): + return None + + # call this to make sure we error if the CallConfig is CREATE or ALL + self.clear_state.call_config.clear_state_condition_under_config() + + return ASTBuilder.wrap_handler( + False, + cast( + Expr | SubroutineFnWrapper | ABIReturnSubroutine, + self.clear_state.action, + ), + ) + + +BareCallActions.__module__ = "pyteal" + + +@dataclass(frozen=True) +class CondNode: + condition: Expr + branch: Expr + + +CondNode.__module__ = "pyteal" + + +@dataclass +class ASTBuilder: + conditions_n_branches: list[CondNode] = field(default_factory=list) + + @staticmethod + def wrap_handler( + is_method_call: bool, handler: ABIReturnSubroutine | SubroutineFnWrapper | Expr + ) -> Expr: + """This is a helper function that handles transaction arguments passing in bare-app-call/abi-method handlers. + + If `is_method_call` is True, then it can only be `ABIReturnSubroutine`, + otherwise: + - both `ABIReturnSubroutine` and `Subroutine` takes 0 argument on the stack. + - all three cases have none (or void) type. + + On ABI method case, if the ABI method has more than 15 args, this function manages to de-tuple + the last (16-th) Txn app-arg into a list of ABI method arguments, and pass in to the ABI method. + + Args: + is_method_call: a boolean value that specify if the handler is an ABI method. + handler: an `ABIReturnSubroutine`, or `SubroutineFnWrapper` (for `Subroutine` case), or an `Expr`. + Returns: + Expr: + - for bare-appcall it returns an expression that the handler takes no txn arg and Approve + - for abi-method it returns the txn args correctly decomposed into ABI variables, + passed in ABIReturnSubroutine and logged, then approve. + """ + if not is_method_call: + match handler: + case Expr(): + if handler.type_of() != TealType.none: + raise TealInputError( + f"bare appcall handler should be TealType.none not {handler.type_of()}." + ) + return handler if handler.has_return() else Seq(handler, Approve()) + case SubroutineFnWrapper(): + if handler.type_of() != TealType.none: + raise TealInputError( + f"subroutine call should be returning TealType.none not {handler.type_of()}." + ) + if handler.subroutine.argument_count() != 0: + raise TealInputError( + f"subroutine call should take 0 arg for bare-app call. " + f"this subroutine takes {handler.subroutine.argument_count()}." + ) + return Seq(handler(), Approve()) + case ABIReturnSubroutine(): + if handler.type_of() != "void": + raise TealInputError( + f"abi-returning subroutine call should be returning void not {handler.type_of()}." + ) + if handler.subroutine.argument_count() != 0: + raise TealInputError( + f"abi-returning subroutine call should take 0 arg for bare-app call. " + f"this abi-returning subroutine takes {handler.subroutine.argument_count()}." + ) + return Seq(cast(Expr, handler()), Approve()) + case _: + raise TealInputError( + "bare appcall can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg" + ) + else: + if not isinstance(handler, ABIReturnSubroutine): + raise TealInputError( + f"method call should be only registering ABIReturnSubroutine, got {type(handler)}." + ) + if not handler.is_abi_routable(): + raise TealInputError( + f"method call ABIReturnSubroutine is not routable " + f"got {handler.subroutine.argument_count()} args with {len(handler.subroutine.abi_args)} ABI args." + ) + + # All subroutine args types + arg_type_specs = cast( + list[abi.TypeSpec], handler.subroutine.expected_arg_types + ) + + # All subroutine arg values, initialize here and use below instead of + # creating new instances on the fly so we dont have to think about splicing + # back in the transaction types + arg_vals = [typespec.new_instance() for typespec in arg_type_specs] + + # Only args that appear in app args + app_arg_vals: list[abi.BaseType] = [ + ats for ats in arg_vals if not isinstance(ats, abi.Transaction) + ] + + for aav in app_arg_vals: + # If we're here we know the top level isnt a Transaction but a transaction may + # be included in some collection type like a Tuple or Array, raise error + # as these are not supported + if abi.contains_type_spec(aav.type_spec(), abi.TransactionTypeSpecs): + raise TealInputError( + "A Transaction type may not be included in Tuples or Arrays" + ) + + # assign to a var here since we modify app_arg_vals later + tuplify = len(app_arg_vals) > METHOD_ARG_NUM_CUTOFF + + # only transaction args (these are omitted from app args) + txn_arg_vals: list[abi.Transaction] = [ + ats for ats in arg_vals if isinstance(ats, abi.Transaction) + ] + + # Tuple-ify any app args after the limit + if tuplify: + last_arg_specs_grouped: list[abi.TypeSpec] = [ + t.type_spec() for t in app_arg_vals[METHOD_ARG_NUM_CUTOFF - 1 :] + ] + app_arg_vals = app_arg_vals[: METHOD_ARG_NUM_CUTOFF - 1] + app_arg_vals.append( + abi.TupleTypeSpec(*last_arg_specs_grouped).new_instance() + ) + + # decode app args + decode_instructions: list[Expr] = [ + app_arg.decode(Txn.application_args[idx + 1]) + for idx, app_arg in enumerate(app_arg_vals) + ] + + # "decode" transaction types by setting the relative index + if len(txn_arg_vals) > 0: + txn_arg_len = len(txn_arg_vals) + # The transactions should appear in the group in the order they're specified in the method signature + # and should be relative to the current transaction. + + # ex: + # doit(axfer,pay,appl) + # would be 4 transactions + # current_idx-3 = axfer + # current_idx-2 = pay + # current_idx-1 = appl + # current_idx-0 = the txn that triggered the current eval (not specified but here for completeness) + + # since we're iterating in order of the txns appearance in the args we + # subtract the current index from the total length to get the offset. + # and subtract that from the current index to get the absolute position + # in the group + + txn_decode_instructions: list[Expr] = [] + + for idx, arg_val in enumerate(txn_arg_vals): + txn_decode_instructions.append( + arg_val._set_index(Txn.group_index() - Int(txn_arg_len - idx)) + ) + spec = arg_val.type_spec() + if type(spec) is not abi.TransactionTypeSpec: + # this is a specific transaction type + txn_decode_instructions.append( + Assert(arg_val.get().type_enum() == spec.txn_type_enum()) + ) + + decode_instructions += txn_decode_instructions + + # de-tuple into specific values using `store_into` on + # each element of the tuple'd arguments + if tuplify: + tupled_abi_vals: list[abi.BaseType] = arg_vals[ + METHOD_ARG_NUM_CUTOFF - 1 : + ] + tupled_arg: abi.Tuple = cast(abi.Tuple, app_arg_vals[-1]) + de_tuple_instructions: list[Expr] = [ + tupled_arg[idx].store_into(arg_val) + for idx, arg_val in enumerate(tupled_abi_vals) + ] + decode_instructions += de_tuple_instructions + + # NOTE: does not have to have return, can be void method + if handler.type_of() == "void": + return Seq( + *decode_instructions, + cast(Expr, handler(*arg_vals)), + Approve(), + ) + else: + output_temp: abi.BaseType = cast( + OutputKwArgInfo, handler.output_kwarg_info + ).abi_type.new_instance() + subroutine_call: abi.ReturnedValue = cast( + abi.ReturnedValue, handler(*arg_vals) + ) + return Seq( + *decode_instructions, + subroutine_call.store_into(output_temp), + abi.MethodReturn(output_temp), + Approve(), + ) + + def add_method_to_ast( + self, method_signature: str, cond: Expr | int, handler: ABIReturnSubroutine + ) -> None: + walk_in_cond = Txn.application_args[0] == MethodSignature(method_signature) + match cond: + case Expr(): + self.conditions_n_branches.append( + CondNode( + walk_in_cond, + Seq(Assert(cond), self.wrap_handler(True, handler)), + ) + ) + case 1: + self.conditions_n_branches.append( + CondNode(walk_in_cond, self.wrap_handler(True, handler)) + ) + case 0: + return + case _: + raise TealInputError("Invalid condition input for add_method_to_ast") + + def program_construction(self) -> Expr: + if not self.conditions_n_branches: + return Reject() + return Cond(*[[n.condition, n.branch] for n in self.conditions_n_branches]) + + +class Router: + """ + The Router class helps construct the approval and clear state programs for an ARC-4 compliant + application. + + Additionally, this class can produce an ARC-4 contract description object for the application. + + **WARNING:** The ABI Router is still taking shape and is subject to backwards incompatible changes. + + * Based on feedback, the API and usage patterns are likely to change. + * Expect migration issues in future PyTeal versions. + + For these reasons, we strongly recommend using :any:`pragma` to pin the version of PyTeal in your + source code. + """ + + def __init__( + self, + name: str, + bare_calls: BareCallActions = None, + descr: str = None, + ) -> None: + """ + Args: + name: the name of the smart contract, used in the JSON object. + bare_calls: the bare app call registered for each on_completion. + descr: a description of the smart contract, used in the JSON object. + """ + + self.name: str = name + self.descr = descr + + self.approval_ast = ASTBuilder() + self.clear_state_ast = ASTBuilder() + + self.methods: list[sdk_abi.Method] = [] + self.method_sig_to_selector: dict[str, bytes] = dict() + self.method_selector_to_sig: dict[bytes, str] = dict() + + if bare_calls and not bare_calls.is_empty(): + bare_call_approval = bare_calls.approval_construction() + if bare_call_approval: + self.approval_ast.conditions_n_branches.append( + CondNode( + Txn.application_args.length() == Int(0), + cast(Expr, bare_call_approval), + ) + ) + bare_call_clear = bare_calls.clear_state_construction() + if bare_call_clear: + self.clear_state_ast.conditions_n_branches.append( + CondNode( + Txn.application_args.length() == Int(0), + cast(Expr, bare_call_clear), + ) + ) + + def add_method_handler( + self, + method_call: ABIReturnSubroutine, + overriding_name: str = None, + method_config: MethodConfig = None, + description: str = None, + ) -> ABIReturnSubroutine: + """Add a method call handler to this Router. + + Args: + method_call: An ABIReturnSubroutine that implements the method body. + overriding_name (optional): A name for this method. Defaults to the function name of + method_call. + method_config (optional): An object describing the on completion actions and + creation/non-creation call statuses that are valid for calling this method. All + invalid configurations will be rejected. Defaults to :code:`MethodConfig(no_op=CallConfig.CALL)` + (i.e. only the no-op action during a non-creation call is accepted) if none is provided. + description (optional): A description for this method. Defaults to the docstring of + method_call, if there is one. + """ + if not isinstance(method_call, ABIReturnSubroutine): + raise TealInputError( + "for adding method handler, must be ABIReturnSubroutine" + ) + method_signature = method_call.method_signature(overriding_name) + if method_config is None: + method_config = MethodConfig(no_op=CallConfig.CALL) + if method_config.is_never(): + raise TealInputError( + f"registered method {method_signature} is never executed" + ) + method_selector = encoding.checksum(bytes(method_signature, "utf-8"))[:4] + + if method_signature in self.method_sig_to_selector: + raise TealInputError(f"re-registering method {method_signature} detected") + if method_selector in self.method_selector_to_sig: + raise TealInputError( + f"re-registering method {method_signature} has hash collision " + f"with {self.method_selector_to_sig[method_selector]}" + ) + + meth = method_call.method_spec() + if description is not None: + meth.desc = description + self.methods.append(meth) + + self.method_sig_to_selector[method_signature] = method_selector + self.method_selector_to_sig[method_selector] = method_signature + + method_approval_cond = method_config.approval_cond() + method_clear_state_cond = method_config.clear_state_cond() + self.approval_ast.add_method_to_ast( + method_signature, method_approval_cond, method_call + ) + self.clear_state_ast.add_method_to_ast( + method_signature, method_clear_state_cond, method_call + ) + return method_call + + def method( + self, + func: Callable = None, + /, + *, + name: str = None, + description: str = None, + no_op: CallConfig = None, + opt_in: CallConfig = None, + close_out: CallConfig = None, + clear_state: CallConfig = None, + update_application: CallConfig = None, + delete_application: CallConfig = None, + ): + """This is an alternative way to register a method, as supposed to :code:`add_method_handler`. + + This is a decorator that's meant to be used over a Python function, which is internally + wrapped with ABIReturnSubroutine. Additional keyword arguments on this decorator can be used + to specify the OnCompletion statuses that are valid for the registered method. + + NOTE: By default, all OnCompletion actions other than `no_op` are set to `CallConfig.NEVER`, + while `no_op` field is set to `CallConfig.CALL`. However, if you provide any keywords for + OnCompletion actions, then the `no_op` field will default to `CallConfig.NEVER`. + + Args: + func: A function that implements the method body. This should *NOT* be wrapped with the + :code:`ABIReturnSubroutine` decorator yet. + name (optional): A name for this method. Defaults to the function name of func. + description (optional): A description for this method. Defaults to the docstring of + func, if there is one. + no_op (optional): The allowed calls during :code:`OnComplete.NoOp`. + opt_in (optional): The allowed calls during :code:`OnComplete.OptIn`. + close_out (optional): The allowed calls during :code:`OnComplete.CloseOut`. + clear_state (optional): The allowed calls during :code:`OnComplete.ClearState`. + update_application (optional): The allowed calls during :code:`OnComplete.UpdateApplication`. + delete_application (optional): The allowed calls during :code:`OnComplete.DeleteApplication`. + """ + # we use `is None` extensively for CallConfig to distinguish 2 following cases + # - None + # - CallConfig.Never + # both cases evaluate to False in if statement. + def wrap(_func) -> ABIReturnSubroutine: + wrapped_subroutine = ABIReturnSubroutine(_func) + call_configs: MethodConfig + if ( + no_op is None + and opt_in is None + and close_out is None + and clear_state is None + and update_application is None + and delete_application is None + ): + call_configs = MethodConfig(no_op=CallConfig.CALL) + else: + + def none_to_never(x: None | CallConfig): + return CallConfig.NEVER if x is None else x + + _no_op = none_to_never(no_op) + _opt_in = none_to_never(opt_in) + _close_out = none_to_never(close_out) + _clear_state = none_to_never(clear_state) + _update_app = none_to_never(update_application) + _delete_app = none_to_never(delete_application) + + call_configs = MethodConfig( + no_op=_no_op, + opt_in=_opt_in, + close_out=_close_out, + clear_state=_clear_state, + update_application=_update_app, + delete_application=_delete_app, + ) + return self.add_method_handler( + wrapped_subroutine, name, call_configs, description + ) + + if not func: + return wrap + return wrap(func) + + def contract_construct(self) -> sdk_abi.Contract: + """A helper function in constructing a `Contract` object. + + It takes out the method spec from approval program methods, + and constructs an `Contract` object. + + Returns: + A Python SDK `Contract` object constructed from the registered methods on this router. + """ + + return sdk_abi.Contract(self.name, self.methods, self.descr) + + def build_program(self) -> tuple[Expr, Expr, sdk_abi.Contract]: + """ + Constructs ASTs for approval and clear-state programs from the registered methods and bare + app calls in the router, and also generates a Contract object to allow client read and call + the methods easily. + + Note that if no methods or bare app call actions have been registered to either the approval + or clear state programs, then that program will reject all transactions. + + Returns: + A tuple of three objects. + + * approval_program: an AST for approval program + * clear_state_program: an AST for clear-state program + * contract: a Python SDK Contract object to allow clients to make off-chain calls + """ + return ( + self.approval_ast.program_construction(), + self.clear_state_ast.program_construction(), + self.contract_construct(), + ) + + def compile_program( + self, + *, + version: int = DEFAULT_TEAL_VERSION, + assemble_constants: bool = False, + optimize: OptimizeOptions = None, + ) -> tuple[str, str, sdk_abi.Contract]: + """ + Constructs and compiles approval and clear-state programs from the registered methods and + bare app calls in the router, and also generates a Contract object to allow client read and call + the methods easily. + + This method combines :any:`Router.build_program` and :any:`compileTeal`. + + Note that if no methods or bare app call actions have been registered to either the approval + or clear state programs, then that program will reject all transactions. + + Returns: + A tuple of three objects. + + * approval_program: compiled approval program string + * clear_state_program: compiled clear-state program string + * contract: a Python SDK Contract object to allow clients to make off-chain calls + """ + ap, csp, contract = self.build_program() + ap_compiled = compileTeal( + ap, + Mode.Application, + version=version, + assembleConstants=assemble_constants, + optimize=optimize, + ) + csp_compiled = compileTeal( + csp, + Mode.Application, + version=version, + assembleConstants=assemble_constants, + optimize=optimize, + ) + return ap_compiled, csp_compiled, contract + + +Router.__module__ = "pyteal" diff --git a/pyteal/ast/router_test.py b/pyteal/ast/router_test.py new file mode 100644 index 000000000..c7abd54fd --- /dev/null +++ b/pyteal/ast/router_test.py @@ -0,0 +1,803 @@ +import pyteal as pt +from pyteal.ast.router import ASTBuilder +import pytest +import typing +import algosdk.abi as sdk_abi + + +options = pt.CompileOptions(version=5) + + +@pt.ABIReturnSubroutine +def add(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + """add takes 2 integers a,b and adds them, returning the sum""" + return output.set(a.get() + b.get()) + + +@pt.ABIReturnSubroutine +def sub(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + """replace me""" + return output.set(a.get() - b.get()) + + +@pt.ABIReturnSubroutine +def mul(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + return output.set(a.get() * b.get()) + + +@pt.ABIReturnSubroutine +def div(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + return output.set(a.get() / b.get()) + + +@pt.ABIReturnSubroutine +def mod(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + return output.set(a.get() % b.get()) + + +@pt.ABIReturnSubroutine +def qrem( + a: pt.abi.Uint64, + b: pt.abi.Uint64, + *, + output: pt.abi.Tuple2[pt.abi.Uint64, pt.abi.Uint64], +) -> pt.Expr: + return pt.Seq( + (q := pt.abi.Uint64()).set(a.get() / b.get()), + (rem := pt.abi.Uint64()).set(a.get() % b.get()), + output.set(q, rem), + ) + + +@pt.ABIReturnSubroutine +def reverse(a: pt.abi.String, *, output: pt.abi.String) -> pt.Expr: + idx = pt.ScratchVar() + buff = pt.ScratchVar() + + init = idx.store(pt.Int(0)) + cond = idx.load() < a.length() + _iter = idx.store(idx.load() + pt.Int(1)) + return pt.Seq( + buff.store(pt.Bytes("")), + pt.For(init, cond, _iter).Do( + a[idx.load()].use(lambda v: buff.store(pt.Concat(v.encode(), buff.load()))) + ), + output.set(buff.load()), + ) + + +@pt.ABIReturnSubroutine +def concat_strings( + b: pt.abi.DynamicArray[pt.abi.String], *, output: pt.abi.String +) -> pt.Expr: + idx = pt.ScratchVar() + buff = pt.ScratchVar() + + init = idx.store(pt.Int(0)) + cond = idx.load() < b.length() + _iter = idx.store(idx.load() + pt.Int(1)) + return pt.Seq( + buff.store(pt.Bytes("")), + pt.For(init, cond, _iter).Do( + b[idx.load()].use(lambda s: buff.store(pt.Concat(buff.load(), s.get()))) + ), + output.set(buff.load()), + ) + + +@pt.ABIReturnSubroutine +def many_args( + _a: pt.abi.Uint64, + _b: pt.abi.Uint64, + _c: pt.abi.Uint64, + _d: pt.abi.Uint64, + _e: pt.abi.Uint64, + _f: pt.abi.Uint64, + _g: pt.abi.Uint64, + _h: pt.abi.Uint64, + _i: pt.abi.Uint64, + _j: pt.abi.Uint64, + _k: pt.abi.Uint64, + _l: pt.abi.Uint64, + _m: pt.abi.Uint64, + _n: pt.abi.Uint64, + _o: pt.abi.Uint64, + _p: pt.abi.Uint64, + _q: pt.abi.Uint64, + _r: pt.abi.Uint64, + _s: pt.abi.Uint64, + _t: pt.abi.Uint64, + *, + output: pt.abi.Uint64, +) -> pt.Expr: + return output.set(_t.get()) + + +@pt.Subroutine(pt.TealType.none) +def safe_clear_state_delete(): + return ( + pt.If(pt.Txn.sender() == pt.Global.creator_address()) + .Then(pt.Approve()) + .Else(pt.Reject()) + ) + + +@pt.ABIReturnSubroutine +def dummy_doing_nothing(): + return pt.Seq(pt.Log(pt.Bytes("a message"))) + + +@pt.Subroutine(pt.TealType.uint64) +def returning_u64(): + return pt.Int(1) + + +@pt.Subroutine(pt.TealType.none) +def mult_over_u64_and_log(a: pt.Expr, b: pt.Expr): + return pt.Log(pt.Itob(a * b)) + + +@pt.ABIReturnSubroutine +def eine_constant(*, output: pt.abi.Uint64): + return output.set(1) + + +@pt.ABIReturnSubroutine +def take_abi_and_log(tb_logged: pt.abi.String): + return pt.Log(tb_logged.get()) + + +@pt.ABIReturnSubroutine +def not_registrable(lhs: pt.abi.Uint64, rhs: pt.Expr, *, output: pt.abi.Uint64): + return output.set(lhs.get() * rhs) + + +@pt.ABIReturnSubroutine +def txn_amount(t: pt.abi.PaymentTransaction, *, output: pt.abi.Uint64): + return output.set(t.get().amount()) + + +@pt.ABIReturnSubroutine +def multiple_txn( + appl: pt.abi.ApplicationCallTransaction, + axfer: pt.abi.AssetTransferTransaction, + pay: pt.abi.PaymentTransaction, + any_txn: pt.abi.Transaction, + *, + output: pt.abi.Uint64, +): + return output.set( + appl.get().fee() + axfer.get().fee() + pay.get().fee() + any_txn.get().fee() + ) + + +GOOD_SUBROUTINE_CASES: list[pt.ABIReturnSubroutine | pt.SubroutineFnWrapper] = [ + add, + sub, + mul, + div, + mod, + qrem, + reverse, + concat_strings, + many_args, + safe_clear_state_delete, + dummy_doing_nothing, + eine_constant, + take_abi_and_log, + txn_amount, + multiple_txn, +] + +ON_COMPLETE_CASES: list[pt.EnumInt] = [ + pt.OnComplete.NoOp, + pt.OnComplete.OptIn, + pt.OnComplete.ClearState, + pt.OnComplete.CloseOut, + pt.OnComplete.UpdateApplication, + pt.OnComplete.DeleteApplication, +] + + +def power_set(no_dup_list: list, length_override: int = None): + """ + This function serves as a generator for all possible elements in power_set + over `non_dup_list`, which is a list of non-duplicated elements (matches property of a set). + + The cardinality of a powerset is 2^|non_dup_list|, so we can iterate from 0 to 2^|non_dup_list| - 1 + to index each element in such power_set. + By binary representation of each index, we can see it as an allowance over each element in `no_dup_list`, + and generate a unique subset of `non_dup_list`, which yields as an element of power_set of `no_dup_list`. + + Args: + no_dup_list: a list of elements with no duplication + length_override: a number indicating the largest size of super_set element, + must be in range [1, len(no_dup_list)]. + """ + if length_override is None: + length_override = len(no_dup_list) + assert 1 <= length_override <= len(no_dup_list) + masks = [1 << i for i in range(length_override)] + for i in range(1 << len(no_dup_list)): + yield [elem for mask, elem in zip(masks, no_dup_list) if i & mask] + + +def full_ordered_combination_gen(non_dup_list: list, perm_length: int): + """ + This function serves as a generator for all possible vectors of length `perm_length`, + each of whose entries are one of the elements in `non_dup_list`, + which is a list of non-duplicated elements. + + Args: + non_dup_list: must be a list of elements with no duplication + perm_length: must be a non-negative number indicating resulting length of the vector + """ + if perm_length < 0: + raise pt.TealInputError("input permutation length must be non-negative") + elif len(set(non_dup_list)) != len(non_dup_list): + raise pt.TealInputError(f"input non_dup_list {non_dup_list} has duplications") + elif perm_length == 0: + yield [] + return + # we can index all possible cases of vectors with an index in range + # [0, |non_dup_list| ^ perm_length - 1] + # by converting an index into |non_dup_list|-based number, + # we can get the vector mapped by the index. + for index in range(len(non_dup_list) ** perm_length): + index_list_basis = [] + temp = index + for _ in range(perm_length): + index_list_basis.append(non_dup_list[temp % len(non_dup_list)]) + temp //= len(non_dup_list) + yield index_list_basis + + +def oncomplete_is_in_oc_list(sth: pt.EnumInt, oc_list: list[pt.EnumInt]): + return any(map(lambda x: str(x) == str(sth), oc_list)) + + +def assemble_helper(what: pt.Expr) -> pt.TealBlock: + assembled, _ = what.__teal__(options) + assembled.addIncoming() + assembled = pt.TealBlock.NormalizeBlocks(assembled) + return assembled + + +def camel_to_snake(name: str) -> str: + return "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_") + + +def test_call_config(): + for cc in pt.CallConfig: + approval_cond_on_cc: pt.Expr | int = cc.approval_condition_under_config() + match approval_cond_on_cc: + case pt.Expr(): + expected_cc = ( + (pt.Txn.application_id() == pt.Int(0)) + if cc == pt.CallConfig.CREATE + else (pt.Txn.application_id() != pt.Int(0)) + ) + with pt.TealComponent.Context.ignoreExprEquality(): + assert assemble_helper(approval_cond_on_cc) == assemble_helper( + expected_cc + ) + case int(): + assert approval_cond_on_cc == int(cc) & 1 + case _: + raise pt.TealInternalError( + f"unexpected approval_cond_on_cc {approval_cond_on_cc}" + ) + + if cc in (pt.CallConfig.CREATE, pt.CallConfig.ALL): + with pytest.raises( + pt.TealInputError, + match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$", + ): + cc.clear_state_condition_under_config() + continue + + clear_state_cond_on_cc: int = cc.clear_state_condition_under_config() + match clear_state_cond_on_cc: + case 0: + assert cc == pt.CallConfig.NEVER + case 1: + assert cc == pt.CallConfig.CALL + case _: + raise pt.TealInternalError( + f"unexpected clear_state_cond_on_cc {clear_state_cond_on_cc}" + ) + + +def test_method_config(): + never_mc = pt.MethodConfig(no_op=pt.CallConfig.NEVER) + assert never_mc.is_never() + assert never_mc.approval_cond() == 0 + assert never_mc.clear_state_cond() == 0 + + on_complete_pow_set = power_set(ON_COMPLETE_CASES) + approval_check_names_n_ocs = [ + (camel_to_snake(oc.name), oc) + for oc in ON_COMPLETE_CASES + if str(oc) != str(pt.OnComplete.ClearState) + ] + for on_complete_set in on_complete_pow_set: + oc_names = [camel_to_snake(oc.name) for oc in on_complete_set] + ordered_call_configs = full_ordered_combination_gen( + list(pt.CallConfig), len(on_complete_set) + ) + for call_configs in ordered_call_configs: + mc = pt.MethodConfig(**dict(zip(oc_names, call_configs))) + match mc.clear_state: + case pt.CallConfig.NEVER: + assert mc.clear_state_cond() == 0 + case pt.CallConfig.CALL: + assert mc.clear_state_cond() == 1 + case pt.CallConfig.CREATE | pt.CallConfig.ALL: + with pytest.raises( + pt.TealInputError, + match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$", + ): + mc.clear_state_cond() + if mc.is_never() or all( + getattr(mc, i) == pt.CallConfig.NEVER + for i, _ in approval_check_names_n_ocs + ): + assert mc.approval_cond() == 0 + continue + elif all( + getattr(mc, i) == pt.CallConfig.ALL + for i, _ in approval_check_names_n_ocs + ): + assert mc.approval_cond() == 1 + continue + list_of_cc = [ + ( + typing.cast( + pt.CallConfig, getattr(mc, i) + ).approval_condition_under_config(), + oc, + ) + for i, oc in approval_check_names_n_ocs + ] + list_of_expressions = [] + for expr_or_int, oc in list_of_cc: + match expr_or_int: + case pt.Expr(): + list_of_expressions.append( + pt.And(pt.Txn.on_completion() == oc, expr_or_int) + ) + case 0: + continue + case 1: + list_of_expressions.append(pt.Txn.on_completion() == oc) + with pt.TealComponent.Context.ignoreExprEquality(): + assert assemble_helper(mc.approval_cond()) == assemble_helper( + pt.Or(*list_of_expressions) + ) + + +def test_on_complete_action(): + with pytest.raises(pt.TealInputError) as contradict_err: + pt.OnCompleteAction(action=pt.Seq(), call_config=pt.CallConfig.NEVER) + assert "contradicts" in str(contradict_err) + assert pt.OnCompleteAction.never().is_empty() + assert pt.OnCompleteAction.call_only(pt.Seq()).call_config == pt.CallConfig.CALL + assert pt.OnCompleteAction.create_only(pt.Seq()).call_config == pt.CallConfig.CREATE + assert pt.OnCompleteAction.always(pt.Seq()).call_config == pt.CallConfig.ALL + + +def test_wrap_handler_bare_call(): + BARE_CALL_CASES = [ + dummy_doing_nothing, + safe_clear_state_delete, + pt.Approve(), + pt.Log(pt.Bytes("message")), + ] + for bare_call in BARE_CALL_CASES: + wrapped: pt.Expr = ASTBuilder.wrap_handler(False, bare_call) + expected: pt.Expr + match bare_call: + case pt.Expr(): + if bare_call.has_return(): + expected = bare_call + else: + expected = pt.Seq(bare_call, pt.Approve()) + case pt.SubroutineFnWrapper() | pt.ABIReturnSubroutine(): + expected = pt.Seq(bare_call(), pt.Approve()) + case _: + raise pt.TealInputError("how you got here?") + wrapped_assemble = assemble_helper(wrapped) + wrapped_helper = assemble_helper(expected) + with pt.TealComponent.Context.ignoreExprEquality(): + assert wrapped_assemble == wrapped_helper + + ERROR_CASES = [ + ( + pt.Int(1), + f"bare appcall handler should be TealType.none not {pt.TealType.uint64}.", + ), + ( + returning_u64, + f"subroutine call should be returning TealType.none not {pt.TealType.uint64}.", + ), + ( + mult_over_u64_and_log, + "subroutine call should take 0 arg for bare-app call. this subroutine takes 2.", + ), + ( + eine_constant, + f"abi-returning subroutine call should be returning void not {pt.abi.Uint64TypeSpec()}.", + ), + ( + take_abi_and_log, + "abi-returning subroutine call should take 0 arg for bare-app call. this abi-returning subroutine takes 1.", + ), + ( + 1, + "bare appcall can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg", + ), + ] + for error_case, error_msg in ERROR_CASES: + with pytest.raises(pt.TealInputError) as bug: + ASTBuilder.wrap_handler(False, error_case) + assert error_msg in str(bug) + + +def test_wrap_handler_method_call(): + with pytest.raises(pt.TealInputError) as bug: + ASTBuilder.wrap_handler(True, not_registrable) + assert "method call ABIReturnSubroutine is not routable" in str(bug) + + with pytest.raises(pt.TealInputError) as bug: + ASTBuilder.wrap_handler(True, safe_clear_state_delete) + assert "method call should be only registering ABIReturnSubroutine" in str(bug) + + ONLY_ABI_SUBROUTINE_CASES = list( + filter(lambda x: isinstance(x, pt.ABIReturnSubroutine), GOOD_SUBROUTINE_CASES) + ) + + for abi_subroutine in ONLY_ABI_SUBROUTINE_CASES: + wrapped: pt.Expr = ASTBuilder.wrap_handler(True, abi_subroutine) + actual: pt.TealBlock = assemble_helper(wrapped) + + args: list[pt.abi.BaseType] = [ + spec.new_instance() + for spec in typing.cast( + list[pt.abi.TypeSpec], abi_subroutine.subroutine.expected_arg_types + ) + ] + + app_args = [ + arg for arg in args if arg.type_spec() not in pt.abi.TransactionTypeSpecs + ] + + app_arg_cnt = len(app_args) + + txn_args: list[pt.abi.Transaction] = [ + arg for arg in args if arg.type_spec() in pt.abi.TransactionTypeSpecs + ] + + loading: list[pt.Expr] = [] + + if app_arg_cnt > pt.METHOD_ARG_NUM_CUTOFF: + sdk_last_arg = pt.abi.TupleTypeSpec( + *[arg.type_spec() for arg in app_args[pt.METHOD_ARG_NUM_CUTOFF - 1 :]] + ).new_instance() + + loading = [ + arg.decode(pt.Txn.application_args[index + 1]) + for index, arg in enumerate(app_args[: pt.METHOD_ARG_NUM_CUTOFF - 1]) + ] + + loading.append( + sdk_last_arg.decode(pt.Txn.application_args[pt.METHOD_ARG_NUM_CUTOFF]) + ) + else: + loading = [ + arg.decode(pt.Txn.application_args[index + 1]) + for index, arg in enumerate(app_args) + ] + + if len(txn_args) > 0: + for idx, txn_arg in enumerate(txn_args): + loading.append( + txn_arg._set_index( + pt.Txn.group_index() - pt.Int(len(txn_args) - idx) + ) + ) + if str(txn_arg.type_spec()) != "txn": + loading.append( + pt.Assert( + txn_arg.get().type_enum() + == txn_arg.type_spec().txn_type_enum() + ) + ) + + if app_arg_cnt > pt.METHOD_ARG_NUM_CUTOFF: + loading.extend( + [ + sdk_last_arg[idx].store_into(val) + for idx, val in enumerate(app_args[pt.METHOD_ARG_NUM_CUTOFF - 1 :]) + ] + ) + + evaluate: pt.Expr + if abi_subroutine.type_of() != "void": + output_temp = abi_subroutine.output_kwarg_info.abi_type.new_instance() + evaluate = pt.Seq( + abi_subroutine(*args).store_into(output_temp), + pt.abi.MethodReturn(output_temp), + ) + else: + evaluate = abi_subroutine(*args) + + expected = assemble_helper(pt.Seq(*loading, evaluate, pt.Approve())) + with pt.TealComponent.Context.ignoreScratchSlotEquality(), pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + assert pt.TealBlock.MatchScratchSlotReferences( + pt.TealBlock.GetReferencedScratchSlots(actual), + pt.TealBlock.GetReferencedScratchSlots(expected), + ) + + +def test_wrap_handler_method_txn_types(): + wrapped: pt.Expr = ASTBuilder.wrap_handler(True, multiple_txn) + actual: pt.TealBlock = assemble_helper(wrapped) + + args: list[pt.abi.Transaction] = [ + pt.abi.ApplicationCallTransaction(), + pt.abi.AssetTransferTransaction(), + pt.abi.PaymentTransaction(), + pt.abi.Transaction(), + ] + output_temp = pt.abi.Uint64() + expected_ast = pt.Seq( + args[0]._set_index(pt.Txn.group_index() - pt.Int(4)), + pt.Assert(args[0].get().type_enum() == pt.TxnType.ApplicationCall), + args[1]._set_index(pt.Txn.group_index() - pt.Int(3)), + pt.Assert(args[1].get().type_enum() == pt.TxnType.AssetTransfer), + args[2]._set_index(pt.Txn.group_index() - pt.Int(2)), + pt.Assert(args[2].get().type_enum() == pt.TxnType.Payment), + args[3]._set_index(pt.Txn.group_index() - pt.Int(1)), + multiple_txn(*args).store_into(output_temp), + pt.abi.MethodReturn(output_temp), + pt.Approve(), + ) + + expected = assemble_helper(expected_ast) + with pt.TealComponent.Context.ignoreScratchSlotEquality(), pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + assert pt.TealBlock.MatchScratchSlotReferences( + pt.TealBlock.GetReferencedScratchSlots(actual), + pt.TealBlock.GetReferencedScratchSlots(expected), + ) + + +def test_wrap_handler_method_call_many_args(): + wrapped: pt.Expr = ASTBuilder.wrap_handler(True, many_args) + actual: pt.TealBlock = assemble_helper(wrapped) + + args = [pt.abi.Uint64() for _ in range(20)] + last_arg = pt.abi.TupleTypeSpec( + *[pt.abi.Uint64TypeSpec() for _ in range(6)] + ).new_instance() + + output_temp = pt.abi.Uint64() + expected_ast = pt.Seq( + args[0].decode(pt.Txn.application_args[1]), + args[1].decode(pt.Txn.application_args[2]), + args[2].decode(pt.Txn.application_args[3]), + args[3].decode(pt.Txn.application_args[4]), + args[4].decode(pt.Txn.application_args[5]), + args[5].decode(pt.Txn.application_args[6]), + args[6].decode(pt.Txn.application_args[7]), + args[7].decode(pt.Txn.application_args[8]), + args[8].decode(pt.Txn.application_args[9]), + args[9].decode(pt.Txn.application_args[10]), + args[10].decode(pt.Txn.application_args[11]), + args[11].decode(pt.Txn.application_args[12]), + args[12].decode(pt.Txn.application_args[13]), + args[13].decode(pt.Txn.application_args[14]), + last_arg.decode(pt.Txn.application_args[15]), + last_arg[0].store_into(args[14]), + last_arg[1].store_into(args[15]), + last_arg[2].store_into(args[16]), + last_arg[3].store_into(args[17]), + last_arg[4].store_into(args[18]), + last_arg[5].store_into(args[19]), + many_args(*args).store_into(output_temp), + pt.abi.MethodReturn(output_temp), + pt.Approve(), + ) + expected = assemble_helper(expected_ast) + with pt.TealComponent.Context.ignoreScratchSlotEquality(), pt.TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + assert pt.TealBlock.MatchScratchSlotReferences( + pt.TealBlock.GetReferencedScratchSlots(actual), + pt.TealBlock.GetReferencedScratchSlots(expected), + ) + + +def test_contract_json_obj(): + abi_subroutines = list( + filter(lambda x: isinstance(x, pt.ABIReturnSubroutine), GOOD_SUBROUTINE_CASES) + ) + contract_name = "contract_name" + on_complete_actions = pt.BareCallActions( + clear_state=pt.OnCompleteAction.call_only(safe_clear_state_delete) + ) + router = pt.Router(contract_name, on_complete_actions) + method_list: list[sdk_abi.Method] = [] + for subroutine in abi_subroutines: + + doc = subroutine.subroutine.implementation.__doc__ + desc = None + if doc is not None and doc.strip() == "replace me": + desc = "dope description" + + router.add_method_handler(subroutine, description=desc) + + ms = subroutine.method_spec() + + # Manually replace it since the override is applied in the method handler + # not attached to the ABIReturnSubroutine itself + ms.desc = desc if desc is not None else ms.desc + + sig_method = sdk_abi.Method.from_signature(subroutine.method_signature()) + + assert ms.name == sig_method.name + + for idx, arg in enumerate(ms.args): + assert arg.type == sig_method.args[idx].type + + method_list.append(ms) + + sdk_contract = sdk_abi.Contract(contract_name, method_list) + contract = router.contract_construct() + assert contract == sdk_contract + + +def test_build_program_all_empty(): + router = pt.Router("test") + + approval, clear_state, contract = router.build_program() + + expected_empty_program = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 0), + pt.TealOp(None, pt.Op.return_), + ] + ) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert assemble_helper(approval) == expected_empty_program + assert assemble_helper(clear_state) == expected_empty_program + + expected_contract = sdk_abi.Contract("test", []) + assert contract == expected_contract + + +def test_build_program_approval_empty(): + router = pt.Router( + "test", + pt.BareCallActions(clear_state=pt.OnCompleteAction.call_only(pt.Approve())), + ) + + approval, clear_state, contract = router.build_program() + + expected_empty_program = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 0), + pt.TealOp(None, pt.Op.return_), + ] + ) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert assemble_helper(approval) == expected_empty_program + assert assemble_helper(clear_state) != expected_empty_program + + expected_contract = sdk_abi.Contract("test", []) + assert contract == expected_contract + + +def test_build_program_clear_state_empty(): + router = pt.Router( + "test", pt.BareCallActions(no_op=pt.OnCompleteAction.always(pt.Approve())) + ) + + approval, clear_state, contract = router.build_program() + + expected_empty_program = pt.TealSimpleBlock( + [ + pt.TealOp(None, pt.Op.int, 0), + pt.TealOp(None, pt.Op.return_), + ] + ) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert assemble_helper(approval) != expected_empty_program + assert assemble_helper(clear_state) == expected_empty_program + + expected_contract = sdk_abi.Contract("test", []) + assert contract == expected_contract + + +def test_build_program_clear_state_invalid_config(): + for config in (pt.CallConfig.CREATE, pt.CallConfig.ALL): + bareCalls = pt.BareCallActions( + clear_state=pt.OnCompleteAction(action=pt.Approve(), call_config=config) + ) + with pytest.raises( + pt.TealInputError, + match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$", + ): + pt.Router("test", bareCalls) + + router = pt.Router("test") + + @pt.ABIReturnSubroutine + def clear_state_method(): + return pt.Approve() + + with pytest.raises( + pt.TealInputError, + match=r"Only CallConfig.CALL or CallConfig.NEVER are valid for a clear state CallConfig, since clear state can never be invoked during creation$", + ): + router.add_method_handler( + clear_state_method, + method_config=pt.MethodConfig(clear_state=config), + ) + + +def test_build_program_clear_state_valid_config(): + action = pt.If(pt.Txn.fee() == pt.Int(4)).Then(pt.Approve()).Else(pt.Reject()) + config = pt.CallConfig.CALL + + router_with_bare_call = pt.Router( + "test", + pt.BareCallActions( + clear_state=pt.OnCompleteAction(action=action, call_config=config) + ), + ) + _, actual_clear_state_with_bare_call, _ = router_with_bare_call.build_program() + + expected_clear_state_with_bare_call = assemble_helper( + pt.Cond([pt.Txn.application_args.length() == pt.Int(0), action]) + ) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert ( + assemble_helper(actual_clear_state_with_bare_call) + == expected_clear_state_with_bare_call + ) + + router_with_method = pt.Router("test") + + @pt.ABIReturnSubroutine + def clear_state_method(): + return action + + router_with_method.add_method_handler( + clear_state_method, method_config=pt.MethodConfig(clear_state=config) + ) + + _, actual_clear_state_with_method, _ = router_with_method.build_program() + + expected_clear_state_with_method = assemble_helper( + pt.Cond( + [ + pt.Txn.application_args[0] + == pt.MethodSignature("clear_state_method()void"), + pt.Seq(clear_state_method(), pt.Approve()), + ] + ) + ) + + with pt.TealComponent.Context.ignoreExprEquality(): + assert ( + assemble_helper(actual_clear_state_with_method) + == expected_clear_state_with_method + ) diff --git a/pyteal/ast/seq.py b/pyteal/ast/seq.py index 81dee1d07..b3fe26c5b 100644 --- a/pyteal/ast/seq.py +++ b/pyteal/ast/seq.py @@ -13,15 +13,19 @@ class Seq(Expr): """A control flow expression to represent a sequence of expressions.""" @overload - def __init__(self, *exprs: Expr): - ... + def __init__(self, *exprs: Expr) -> None: + pass @overload - def __init__(self, exprs: List[Expr]): - ... + def __init__(self, exprs: List[Expr]) -> None: + pass def __init__(self, *exprs): - """Create a new Seq expression. + """ + __init__(*exprs: Expr) -> None + __init__(exprs: List[Expr]) -> None + + Create a new Seq expression. The new Seq expression will take on the return value of the final expression in the sequence. diff --git a/pyteal/ast/subroutine.py b/pyteal/ast/subroutine.py index 657134bee..0d0ee13ca 100644 --- a/pyteal/ast/subroutine.py +++ b/pyteal/ast/subroutine.py @@ -1,14 +1,17 @@ -from inspect import Parameter, get_annotations, isclass, signature -from types import MappingProxyType -from typing import Callable, List, Optional, Type, Union, TYPE_CHECKING +from dataclasses import dataclass +from inspect import isclass, Parameter, signature, get_annotations +from types import MappingProxyType, NoneType +from typing import Any, Callable, Final, Optional, TYPE_CHECKING, cast -from pyteal.errors import TealInputError, verifyTealVersion -from pyteal.ir import TealOp, Op, TealBlock -from pyteal.types import TealType +import algosdk.abi as sdk_abi +from pyteal.ast import abi from pyteal.ast.expr import Expr from pyteal.ast.seq import Seq from pyteal.ast.scratchvar import DynamicScratchVar, ScratchVar +from pyteal.errors import TealInputError, verifyTealVersion +from pyteal.ir import TealOp, Op, TealBlock +from pyteal.types import TealType if TYPE_CHECKING: from pyteal.compiler import CompileOptions @@ -24,169 +27,273 @@ class SubroutineDefinition: def __init__( self, implementation: Callable[..., Expr], - returnType: TealType, - nameStr: str = None, + return_type: TealType, + name_str: Optional[str] = None, + has_abi_output: bool = False, ) -> None: """ Args: implementation: The python function defining the subroutine - returnType: the TealType to be returned by the subroutine - nameStr (optional): the name that is used to identify the subroutine. + return_type: the TealType to be returned by the subroutine + name_str (optional): the name that is used to identify the subroutine. If omitted, the name defaults to the implementation's __name__ attribute + has_abi_output (optional): the boolean that tells if ABI output kwarg for subroutine is used. """ super().__init__() self.id = SubroutineDefinition.nextSubroutineId SubroutineDefinition.nextSubroutineId += 1 - self.returnType = returnType + self.return_type = return_type self.declaration: Optional["SubroutineDeclaration"] = None self.implementation: Callable = implementation - - impl_params, anns, arg_types, byrefs = self._validate() - self.implementationParams: MappingProxyType[str, Parameter] = impl_params - self.annotations: dict[str, Expr | ScratchVar] = anns - self.expected_arg_types: list[type[Expr | ScratchVar]] = arg_types - self.by_ref_args: set[str] = byrefs - - self.__name: str = nameStr if nameStr else self.implementation.__name__ + self.has_abi_output: bool = has_abi_output + + self.implementation_params: MappingProxyType[str, Parameter] + self.annotations: dict[str, type] + self.expected_arg_types: list[type[Expr] | type[ScratchVar] | abi.TypeSpec] + self.by_ref_args: set[str] + self.abi_args: dict[str, abi.TypeSpec] + self.output_kwarg: dict[str, abi.TypeSpec] + + ( + self.implementation_params, + self.annotations, + self.expected_arg_types, + self.by_ref_args, + self.abi_args, + self.output_kwarg, + ) = self._validate() + + self.__name: str = name_str if name_str else self.implementation.__name__ def _validate( - self, input_types: list[TealType] = None + self, input_types: list[TealType | None] = None ) -> tuple[ MappingProxyType[str, Parameter], - dict[str, Expr | ScratchVar], - list[type[Expr | ScratchVar]], + dict[str, type], + list[type[Expr] | type[ScratchVar] | abi.TypeSpec], set[str], + dict[str, abi.TypeSpec], + dict[str, abi.TypeSpec], ]: - implementation = self.implementation - if not callable(implementation): + """Validate the full function signature and annotations for subroutine definition. + + NOTE: `self.implementation` should be set before calling `_validate()` + + This function iterates through `sig.parameters.items()`, and checks each of subroutine arguments. + On each of the subroutine arguments, the following checks are performed: + - If argument is not POSITION_ONLY or not POSITIONAL_OR_KEYWORD, error + - If argument has default value, error + + After the previous checks, the function signature is correct in structure, + but we still need to check the argument types are matching requirements + (i.e., in {Expr, ScratchVar, inheritances of abi.BaseType}). + + Finally, this function outputs: + - `implementation_params` - ordered map from parameter name to inspect.Parameter + - `annotations` - map from parameter name to annotation (if available) + - `expected_arg_types` - an array of elements of Type[Expr], Type[ScratchVar] or abi.TypeSpec instances + It helps type-checking on SubroutineCall from `invoke` method. + - `by_ref_args` - a set of argument names, which are type annotated by ScratchVar. + We put the scratch slot id on the stack, rather than the value itself. + - `abi_args` - a set of argument names, which are type annotated by ABI types. + We load the ABI scratch space stored value to stack, and store them later in subroutine's local ABI values. + - `abi_output_kwarg` - a possibly empty dict which when non-empty contains exactly one key + `ABIReturnSubroutine.OUTPUT_ARG_NAME` with a value that gives abi-tye information about the output + + Args: + input_types (optional): for testing purposes - expected `TealType`s of each parameter + + Returns: + impl_params: a map from python function implementation's argument name, to argument's parameter. + annotations: a dict whose keys are names of type-annotated arguments, + and values are appearing type-annotations. + arg_types: a list of argument type inferred from python function implementation, + containing [type[Expr]| type[ScratchVar] | abi.TypeSpec]. + by_ref_args: a list of argument names that are passed in Subroutine with by-reference mechanism. + abi_args: a dict whose keys are names of ABI arguments, and values are their ABI type-specs. + abi_output_kwarg (might be empty): a dict whose key is the name of ABI output keyword argument, + and the value is the corresponding ABI type-spec. + NOTE: this dict might be empty, when we are defining a normal subroutine, + but it has at most one element when we define an ABI-returning subroutine. + """ + + if not callable(self.implementation): raise TealInputError("Input to SubroutineDefinition is not callable") impl_params: MappingProxyType[str, Parameter] = signature( - implementation + self.implementation ).parameters - anns: dict[str, Expr | ScratchVar] = get_annotations(implementation) - arg_types: list[type[Expr | ScratchVar]] = [] - byrefs: set[str] = set() + annotations: dict[str, type] = get_annotations(self.implementation) + arg_types: list[type[Expr] | type[ScratchVar] | abi.TypeSpec] = [] + by_ref_args: set[str] = set() + abi_args: dict[str, abi.TypeSpec] = {} + abi_output_kwarg: dict[str, abi.TypeSpec] = {} - sig_params = impl_params - if input_types is not None and len(input_types) != len(sig_params): + if "return" in annotations and annotations["return"] is not Expr: raise TealInputError( - "Provided number of input_types ({}) does not match detected number of parameters ({})".format( - len(input_types), len(sig_params) - ) + f"Function has return of disallowed type {annotations['return']}. Only Expr is allowed" ) - if "return" in anns and anns["return"] is not Expr: - raise TealInputError( - "Function has return of disallowed type {}. Only Expr is allowed".format( - anns["return"] - ) - ) - - for i, (name, param) in enumerate(impl_params.items()): + for name, param in impl_params.items(): if param.kind not in ( Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD, + ) and not ( + param.kind is Parameter.KEYWORD_ONLY + and self.has_abi_output + and name == ABIReturnSubroutine.OUTPUT_ARG_NAME ): raise TealInputError( - "Function has a parameter type that is not allowed in a subroutine: parameter {} with type {}".format( - name, param.kind - ) + f"Function has a parameter type that is not allowed in a subroutine: parameter {name} with type {param.kind}" ) if param.default != Parameter.empty: raise TealInputError( - "Function has a parameter with a default value, which is not allowed in a subroutine: {}".format( - name - ) + f"Function has a parameter with a default value, which is not allowed in a subroutine: {name}" ) - if input_types: - intype = input_types[i] - if not isinstance(intype, TealType): + expected_arg_type = self._validate_annotation(annotations, name) + + if param.kind is Parameter.KEYWORD_ONLY: + # this case is only entered when + # - `self.has_abi_output is True` + # - `name == ABIReturnSubroutine.OUTPUT_ARG_NAME` + if not isinstance(expected_arg_type, abi.TypeSpec): raise TealInputError( - "Function has input type {} for parameter {} which is not a TealType".format( - intype, name - ) + f"Function keyword parameter {name} has type {expected_arg_type}" ) - - expected_arg_type = self._validate_parameter_type(anns, name) + abi_output_kwarg[name] = expected_arg_type + continue arg_types.append(expected_arg_type) if expected_arg_type is ScratchVar: - byrefs.add(name) - return impl_params, anns, arg_types, byrefs + by_ref_args.add(name) + if isinstance(expected_arg_type, abi.TypeSpec): + abi_args[name] = expected_arg_type + + if input_types is not None: + input_arg_count = len(impl_params) - len(abi_output_kwarg) + if len(input_types) != input_arg_count: + raise TealInputError( + f"Provided number of input_types ({len(input_types)}) " + f"does not match detected number of input parameters ({input_arg_count})" + ) + for in_type, name in zip(input_types, impl_params): + if not isinstance(in_type, (TealType, NoneType)): + raise TealInputError( + f"Function has input type {in_type} for parameter {name} which is not a TealType" + ) + if in_type is None and name not in abi_args: + raise TealInputError( + f"input_type for {name} is unspecified i.e. None " + f"but this is only allowed for ABI arguments" + ) + + return ( + impl_params, + annotations, + arg_types, + by_ref_args, + abi_args, + abi_output_kwarg, + ) + + @staticmethod + def _is_abi_annotation(obj: Any) -> bool: + try: + abi.type_spec_from_annotation(obj) + return True + except TypeError: + return False @staticmethod - def _validate_parameter_type( - user_defined_annotations: dict, parameter_name: str - ) -> Type[Union[Expr, ScratchVar]]: + def _validate_annotation( + user_defined_annotations: dict[str, Any], parameter_name: str + ) -> type[Expr] | type[ScratchVar] | abi.TypeSpec: ptype = user_defined_annotations.get(parameter_name, None) if ptype is None: - # Without a type annotation, `SubroutineDefinition` presumes an implicit `Expr` declaration rather than these alternatives: + # Without a type annotation, `SubroutineDefinition` presumes an implicit `Expr` declaration + # rather than these alternatives: # * Throw error requiring type annotation. # * Defer parameter type checks until arguments provided during invocation. # # * Rationale: # * Provide an upfront, best-effort type check before invocation. - # * Preserve backwards compatibility with TEAL programs written when `Expr` is the only supported annotation type. + # * Preserve backwards compatibility with TEAL programs written + # when `Expr` is the only supported annotation type. # * `invoke` type checks provided arguments against parameter types to catch mismatches. return Expr - else: - if not isclass(ptype): - raise TealInputError( - "Function has parameter {} of declared type {} which is not a class".format( - parameter_name, ptype - ) - ) - - if ptype not in (Expr, ScratchVar): - raise TealInputError( - "Function has parameter {} of disallowed type {}. Only the types {} are allowed".format( - parameter_name, ptype, (Expr, ScratchVar) - ) - ) - + if ptype in (Expr, ScratchVar): return ptype + if SubroutineDefinition._is_abi_annotation(ptype): + return abi.type_spec_from_annotation(ptype) + if not isclass(ptype): + raise TealInputError( + f"Function has parameter {parameter_name} of declared type {ptype} which is not a class" + ) + raise TealInputError( + f"Function has parameter {parameter_name} of disallowed type {ptype}. " + f"Only the types {(Expr, ScratchVar, 'ABI')} are allowed" + ) - def getDeclaration(self) -> "SubroutineDeclaration": + def get_declaration(self) -> "SubroutineDeclaration": if self.declaration is None: # lazy evaluate subroutine - self.declaration = evaluateSubroutine(self) + self.declaration = evaluate_subroutine(self) return self.declaration def name(self) -> str: return self.__name - def argumentCount(self) -> int: - return len(self.implementationParams) + def argument_count(self) -> int: + return len(self.arguments()) - def arguments(self) -> List[str]: - return list(self.implementationParams.keys()) + def arguments(self) -> list[str]: + syntax_args = list(self.implementation_params.keys()) + syntax_args = [ + arg_name for arg_name in syntax_args if arg_name not in self.output_kwarg + ] + return syntax_args - def invoke(self, args: List[Union[Expr, ScratchVar]]) -> "SubroutineCall": - if len(args) != self.argumentCount(): + def invoke( + self, + args: list[Expr | ScratchVar | abi.BaseType], + ) -> "SubroutineCall": + if len(args) != self.argument_count(): raise TealInputError( - "Incorrect number of arguments for subroutine call. Expected {} arguments, got {}".format( - self.argumentCount(), len(args) - ) + f"Incorrect number of arguments for subroutine call. " + f"Expected {self.arguments()} arguments, got {len(args)} arguments" ) for i, arg in enumerate(args): - atype = self.expected_arg_types[i] - if not isinstance(arg, atype): + arg_type = self.expected_arg_types[i] + if arg_type is Expr and not isinstance(arg, Expr): raise TealInputError( - "supplied argument {} at index {} had type {} but was expecting type {}".format( - arg, i, type(arg), atype - ) + f"supplied argument {arg} at index {i} had type {type(arg)} but was expecting type {arg_type}" ) + elif arg_type is ScratchVar and not isinstance(arg, ScratchVar): + raise TealInputError( + f"supplied argument {arg} at index {i} had type {type(arg)} but was expecting type {arg_type}" + ) + elif isinstance(arg_type, abi.TypeSpec): + if not isinstance(arg, abi.BaseType): + raise TealInputError( + f"supplied argument at index {i} should be an ABI type but got {arg}" + ) + if arg.type_spec() != arg_type: + raise TealInputError( + f"supplied argument {arg} at index {i} " + f"should have ABI typespec {arg_type} but got {arg.type_spec()}" + ) - return SubroutineCall(self, args) + return SubroutineCall( + self, args, output_kwarg=OutputKwArgInfo.from_dict(self.output_kwarg) + ) def __str__(self): - return "subroutine#{}".format(self.id) + return f"subroutine#{self.id}" def __eq__(self, other): if isinstance(other, SubroutineDefinition): @@ -201,18 +308,22 @@ def __hash__(self): class SubroutineDeclaration(Expr): - def __init__(self, subroutine: SubroutineDefinition, body: Expr) -> None: + def __init__( + self, + subroutine: SubroutineDefinition, + body: Expr, + deferred_expr: Optional[Expr] = None, + ) -> None: super().__init__() self.subroutine = subroutine self.body = body + self.deferred_expr = deferred_expr def __teal__(self, options: "CompileOptions"): return self.body.__teal__(options) def __str__(self): - return '(SubroutineDeclaration "{}" {})'.format( - self.subroutine.name(), self.body - ) + return f'(SubroutineDeclaration "{self.subroutine.name()}" {self.body})' def type_of(self): return self.body.type_of() @@ -224,44 +335,75 @@ def has_return(self): SubroutineDeclaration.__module__ = "pyteal" +@dataclass +class OutputKwArgInfo: + name: str + abi_type: abi.TypeSpec + + @staticmethod + def from_dict(kwarg_info: dict[str, abi.TypeSpec]) -> Optional["OutputKwArgInfo"]: + match list(kwarg_info.keys()): + case []: + return None + case [k]: + return OutputKwArgInfo(k, kwarg_info[k]) + case _: + raise TealInputError( + f"illegal conversion kwarg_info length {len(kwarg_info)}." + ) + + +OutputKwArgInfo.__module__ = "pyteal" + + class SubroutineCall(Expr): def __init__( - self, subroutine: SubroutineDefinition, args: List[Union[Expr, ScratchVar]] + self, + subroutine: SubroutineDefinition, + args: list[Expr | ScratchVar | abi.BaseType], + *, + output_kwarg: OutputKwArgInfo = None, ) -> None: super().__init__() self.subroutine = subroutine self.args = args + self.output_kwarg = output_kwarg for i, arg in enumerate(args): - arg_type = None - - if not isinstance(arg, (Expr, ScratchVar)): + if isinstance(arg, Expr): + arg_type = arg.type_of() + elif isinstance(arg, ScratchVar): + arg_type = arg.type + elif isinstance(arg, abi.BaseType): + arg_type = cast(abi.BaseType, arg).stored_value.type + else: raise TealInputError( - "Subroutine argument {} at index {} was of unexpected Python type {}".format( - arg, i, type(arg) - ) + f"Subroutine argument {arg} at index {i} was of unexpected Python type {type(arg)}" ) - arg_type = arg.type_of() if isinstance(arg, Expr) else arg.type - if arg_type == TealType.none: raise TealInputError( - "Subroutine argument {} at index {} evaluates to TealType.none".format( - arg, i - ) + f"Subroutine argument {arg} at index {i} evaluates to TealType.none" ) def __teal__(self, options: "CompileOptions"): """ Generate the subroutine's start and end teal blocks. The subroutine's arguments are pushed on the stack to be picked up into local scratch variables. - There are 2 cases to consider for the pushed arg expression: + There are 4 cases to consider for the pushed arg expression: 1. (by-value) In the case of typical arguments of type Expr, the expression ITSELF is evaluated for the stack and will be stored in a local ScratchVar for subroutine evaluation 2. (by-reference) In the case of a by-reference argument of type ScratchVar, its SLOT INDEX is put on the stack and will be stored in a local DynamicScratchVar for subroutine evaluation + + 3. (ABI, or a special case in by-value) In this case, the storage of an ABI value are loaded + to the stack and will be stored in a local ABI value for subroutine evaluation + + 4. (ABI output keyword argument) In this case, we do not place ABI values (encoding) on the stack. + This is an *output-only* argument: in `evaluate_subroutine` an ABI typed instance for subroutine evaluation + will be generated, and gets in to construct the subroutine implementation. """ verifyTealVersion( Op.callsub.min_version, @@ -269,21 +411,31 @@ def __teal__(self, options: "CompileOptions"): "TEAL version too low to use SubroutineCall expression", ) - def handle_arg(arg): - return arg.index() if isinstance(arg, ScratchVar) else arg + def handle_arg(arg: Expr | ScratchVar | abi.BaseType) -> Expr: + if isinstance(arg, ScratchVar): + return arg.index() + elif isinstance(arg, Expr): + return arg + elif isinstance(arg, abi.BaseType): + return arg.stored_value.load() + else: + raise TealInputError( + f"cannot handle current arg: {arg} to put it on stack" + ) op = TealOp(self, Op.callsub, self.subroutine) - return TealBlock.FromOp(options, op, *(handle_arg(x) for x in self.args)) + return TealBlock.FromOp(options, op, *[handle_arg(x) for x in self.args]) def __str__(self): - ret_str = '(SubroutineCall "' + self.subroutine.name() + '" (' - for a in self.args: - ret_str += " " + a.__str__() - ret_str += "))" - return ret_str + arg_str_list = list(map(str, self.args)) + if self.output_kwarg: + arg_str_list.append( + f"{self.output_kwarg.name}={str(self.output_kwarg.abi_type)}" + ) + return f'(SubroutineCall {self.subroutine.name()} ({" ".join(arg_str_list)}))' def type_of(self): - return self.subroutine.returnType + return self.subroutine.return_type def has_return(self): return False @@ -295,20 +447,21 @@ def has_return(self): class SubroutineFnWrapper: def __init__( self, - fnImplementation: Callable[..., Expr], - returnType: TealType, - name: str = None, + fn_implementation: Callable[..., Expr], + return_type: TealType, + name: Optional[str] = None, ) -> None: self.subroutine = SubroutineDefinition( - fnImplementation, returnType=returnType, nameStr=name + fn_implementation, + return_type=return_type, + name_str=name, ) - def __call__(self, *args: Expr | ScratchVar, **kwargs) -> Expr: + def __call__(self, *args: Expr | ScratchVar | abi.BaseType, **kwargs: Any) -> Expr: if len(kwargs) != 0: raise TealInputError( - "Subroutine cannot be called with keyword arguments. Received keyword arguments: {}".format( - ",".join(kwargs.keys()) - ) + f"Subroutine cannot be called with keyword arguments. " + f"Received keyword arguments: {','.join(kwargs.keys())}" ) return self.subroutine.invoke(list(args)) @@ -316,15 +469,190 @@ def name(self) -> str: return self.subroutine.name() def type_of(self): - return self.subroutine.getDeclaration().type_of() + return self.subroutine.get_declaration().type_of() def has_return(self): - return self.subroutine.getDeclaration().has_return() + return self.subroutine.get_declaration().has_return() SubroutineFnWrapper.__module__ = "pyteal" +class ABIReturnSubroutine: + """Used to create a PyTeal Subroutine (returning an ABI value) from a python function. It's primarily intended to define ARC-4 Application entry points though it can also be used more generally. + + *Disclaimer*: ABIReturnSubroutine is still taking shape and is subject to backwards incompatible changes. + + * For ARC-4 Application entry point definition, feel encouraged to use ABIReturnSubroutine. Expect a best-effort attempt to minimize backwards incompatible changes along with a migration path. + * For general purpose subroutine definition usage, use at your own risk. Based on feedback, the API and usage patterns will change more freely and with less effort to provide migration paths. + + This class is meant to be used as a function decorator. For example: + + .. code-block:: python + + @ABIReturnSubroutine + def abi_sum(toSum: abi.DynamicArray[abi.Uint64], *, output: abi.Uint64) -> Expr: + i = ScratchVar(TealType.uint64) + valueAtIndex = abi.Uint64() + return Seq( + output.set(0), + For(i.store(Int(0)), i.load() < toSum.length(), i.store(i.load() + Int(1))).Do( + Seq( + toSum[i.load()].store_into(valueAtIndex), + output.set(output.get() + valueAtIndex.get()), + ) + ), + ) + + program = Seq( + (to_sum_arr := abi.make(abi.DynamicArray[abi.Uint64])).decode( + Txn.application_args[1] + ), + (res := abi.Uint64()).set(abi_sum(to_sum_arr)), + abi.MethodReturn(res), + Int(1), + ) + """ + + OUTPUT_ARG_NAME: Final[str] = "output" + + def __init__( + self, + fn_implementation: Callable[..., Expr], + ) -> None: + self.output_kwarg_info: Optional[OutputKwArgInfo] = self._get_output_kwarg_info( + fn_implementation + ) + self.subroutine = SubroutineDefinition( + fn_implementation, + return_type=TealType.none, + has_abi_output=self.output_kwarg_info is not None, + ) + + @classmethod + def _get_output_kwarg_info( + cls, fn_implementation: Callable[..., Expr] + ) -> Optional[OutputKwArgInfo]: + if not callable(fn_implementation): + raise TealInputError("Input to ABIReturnSubroutine is not callable") + sig = signature(fn_implementation) + fn_annotations = get_annotations(fn_implementation) + + potential_abi_arg_names = [ + k for k, v in sig.parameters.items() if v.kind == Parameter.KEYWORD_ONLY + ] + + match potential_abi_arg_names: + case []: + return None + case [name]: + if name != cls.OUTPUT_ARG_NAME: + raise TealInputError( + f"ABI return subroutine output-kwarg name must be `output` at this moment, " + f"while {name} is the keyword." + ) + annotation = fn_annotations.get(name, None) + if annotation is None: + raise TealInputError( + f"ABI return subroutine output-kwarg {name} must specify ABI type" + ) + type_spec = abi.type_spec_from_annotation(annotation) + return OutputKwArgInfo(name, type_spec) + case _: + raise TealInputError( + f"multiple output arguments ({len(potential_abi_arg_names)}) " + f"with type annotations {potential_abi_arg_names}" + ) + + def __call__( + self, *args: Expr | ScratchVar | abi.BaseType, **kwargs + ) -> abi.ReturnedValue | Expr: + if len(kwargs) != 0: + raise TealInputError( + f"Subroutine cannot be called with keyword arguments. " + f"Received keyword arguments: {', '.join(kwargs.keys())}" + ) + + invoked = self.subroutine.invoke(list(args)) + if self.output_kwarg_info is None: + if invoked.type_of() != TealType.none: + raise TealInputError( + "ABI subroutine with void type should be evaluated to TealType.none" + ) + return invoked + + self.subroutine.get_declaration() + + return abi.ReturnedValue( + self.output_kwarg_info.abi_type, + invoked, + ) + + def name(self) -> str: + return self.subroutine.name() + + def method_signature(self, overriding_name: str = None) -> str: + if not self.is_abi_routable(): + raise TealInputError( + "Only registrable methods may return a method signature" + ) + + ret_type = self.type_of() + if isinstance(ret_type, abi.TypeSpec) and abi.contains_type_spec( + ret_type, abi.TransactionTypeSpecs + abi.ReferenceTypeSpecs + ): + raise TealInputError( + f"Reference and Transaction types may not be used as return values, got {ret_type}" + ) + + args = [str(v) for v in self.subroutine.abi_args.values()] + if overriding_name is None: + overriding_name = self.name() + return f"{overriding_name}({','.join(args)}){self.type_of()}" + + def method_spec(self) -> sdk_abi.Method: + skip_names = ["return", "output"] + + args = [ + { + "type": str(abi.type_spec_from_annotation(val)), + "name": name, + } + for name, val in self.subroutine.annotations.items() + if name not in skip_names + ] + + spec = { + "name": self.name(), + "args": args, + "returns": {"type": str(self.type_of())}, + } + + if self.subroutine.implementation.__doc__ is not None: + spec["desc"] = " ".join( + [ + i.strip() + for i in self.subroutine.implementation.__doc__.split("\n") + if not (i.isspace() or len(i) == 0) + ] + ) + + return sdk_abi.Method.undictify(spec) + + def type_of(self) -> str | abi.TypeSpec: + return ( + "void" + if self.output_kwarg_info is None + else self.output_kwarg_info.abi_type + ) + + def is_abi_routable(self) -> bool: + return len(self.subroutine.abi_args) == self.subroutine.argument_count() + + +ABIReturnSubroutine.__module__ = "pyteal" + + class Subroutine: """Used to create a PyTeal subroutine from a Python function. @@ -342,20 +670,20 @@ def mySubroutine(a: Expr, b: Expr) -> Expr: ]) """ - def __init__(self, returnType: TealType, name: str = None) -> None: + def __init__(self, return_type: TealType, name: Optional[str] = None) -> None: """Define a new subroutine with the given return type. Args: - returnType: The type that the return value of this subroutine must conform to. + return_type: The type that the return value of this subroutine must conform to. TealType.none indicates that this subroutine does not return any value. """ - self.returnType = returnType + self.return_type = return_type self.name = name - def __call__(self, fnImplementation: Callable[..., Expr]) -> SubroutineFnWrapper: + def __call__(self, fn_implementation: Callable[..., Expr]) -> SubroutineFnWrapper: return SubroutineFnWrapper( - fnImplementation=fnImplementation, - returnType=self.returnType, + fn_implementation=fn_implementation, + return_type=self.return_type, name=self.name, ) @@ -363,7 +691,7 @@ def __call__(self, fnImplementation: Callable[..., Expr]) -> SubroutineFnWrapper Subroutine.__module__ = "pyteal" -def evaluateSubroutine(subroutine: SubroutineDefinition) -> SubroutineDeclaration: +def evaluate_subroutine(subroutine: SubroutineDefinition) -> SubroutineDeclaration: """ Puts together the data necessary to define the code for a subroutine. "evaluate" is used here to connote evaluating the PyTEAL AST into a SubroutineDeclaration, @@ -373,7 +701,7 @@ def evaluateSubroutine(subroutine: SubroutineDefinition) -> SubroutineDeclaratio 2 Argument Usages / Code-Paths - -------- ------ ---------- - Usage (A) for run-time: "argumentVars" --reverse--> "bodyOps" + Usage (A) for run-time: "argumentVars" --reverse--> "body_ops" These are "store" expressions that pick up parameters that have been pre-placed on the stack prior to subroutine invocation. The argumentVars are stored into local scratch space to be used by the TEAL subroutine. @@ -387,46 +715,92 @@ def evaluateSubroutine(subroutine: SubroutineDefinition) -> SubroutineDeclaratio - -------- ----- Type 1 (by-value): these have python type Expr Type 2 (by-reference): these have python type ScratchVar + Type 3 (ABI): these are ABI typed variables with scratch space storage, and still pass by value + Type 4 (ABI-output-arg): ABI typed variables with scratch space, a new ABI instance is generated inside function body, + not one of the cases in the previous three options Usage (A) "argumentVars" - Storing pre-placed stack variables into local scratch space: Type 1. (by-value) use ScratchVar.store() to pick the actual value into a local scratch space Type 2. (by-reference) ALSO use ScratchVar.store() to pick up from the stack NOTE: SubroutineCall.__teal__() has placed the _SLOT INDEX_ on the stack so this is stored into the local scratch space + Type 3. (ABI) abi_value.stored_value.store() to pick from the stack + Type 4. (ABI-output-arg) it is not really used here, since it is only generated internal of the subroutine Usage (B) "loadedArgs" - Passing through to an invoked PyTEAL subroutine AST: Type 1. (by-value) use ScratchVar.load() to have an Expr that can be compiled in python by the PyTEAL subroutine Type 2. (by-reference) use a DynamicScratchVar as the user will have written the PyTEAL in a way that satisfies the ScratchVar API. I.e., the user will write `x.load()` and `x.store(val)` as opposed to just `x`. + Type 3. (ABI) use abi_value itself after storing stack value into scratch space. + Type 4. (ABI-output-arg) generates a new instance of the ABI value, + and appends a return expression of stored value of the ABI keyword value. """ - def var_n_loaded(param): + def var_n_loaded( + param: str, + ) -> tuple[ScratchVar, ScratchVar | abi.BaseType | Expr]: + loaded_var: ScratchVar | abi.BaseType | Expr + argument_var: ScratchVar + if param in subroutine.by_ref_args: - argVar = DynamicScratchVar(TealType.anytype) - loaded = argVar + argument_var = DynamicScratchVar(TealType.anytype) + loaded_var = argument_var + elif param in subroutine.abi_args: + internal_abi_var = subroutine.abi_args[param].new_instance() + argument_var = internal_abi_var.stored_value + loaded_var = internal_abi_var else: - argVar = ScratchVar(TealType.anytype) - loaded = argVar.load() + argument_var = ScratchVar(TealType.anytype) + loaded_var = argument_var.load() - return argVar, loaded + return argument_var, loaded_var + + if len(subroutine.output_kwarg) > 1: + raise TealInputError( + f"ABI keyword argument num: {len(subroutine.output_kwarg)}. " + f"Exceeding abi output keyword argument max number 1." + ) args = subroutine.arguments() - argumentVars, loadedArgs = zip(*map(var_n_loaded, args)) if args else ([], []) + + arg_vars: list[ScratchVar] = [] + loaded_args: list[ScratchVar | Expr | abi.BaseType] = [] + for arg in args: + arg_var, loaded_arg = var_n_loaded(arg) + arg_vars.append(arg_var) + loaded_args.append(loaded_arg) + + abi_output_kwargs: dict[str, abi.BaseType] = {} + output_kwarg_info = OutputKwArgInfo.from_dict(subroutine.output_kwarg) + output_carrying_abi: Optional[abi.BaseType] = None + + if output_kwarg_info: + output_carrying_abi = output_kwarg_info.abi_type.new_instance() + abi_output_kwargs[output_kwarg_info.name] = output_carrying_abi # Arg usage "B" supplied to build an AST from the user-defined PyTEAL function: - subroutineBody = subroutine.implementation(*loadedArgs) + subroutine_body = subroutine.implementation(*loaded_args, **abi_output_kwargs) - if not isinstance(subroutineBody, Expr): + if not isinstance(subroutine_body, Expr): raise TealInputError( - "Subroutine function does not return a PyTeal expression. Got type {}".format( - type(subroutineBody) - ) + f"Subroutine function does not return a PyTeal expression. Got type {type(subroutine_body)}." ) + deferred_expr: Optional[Expr] = None + + # if there is an output keyword argument for ABI, place the storing on the stack + if output_carrying_abi: + if subroutine_body.type_of() != TealType.none: + raise TealInputError( + f"ABI returning subroutine definition should evaluate to TealType.none, " + f"while evaluate to {subroutine_body.type_of()}." + ) + deferred_expr = output_carrying_abi.stored_value.load() + # Arg usage "A" to be pick up and store in scratch parameters that have been placed on the stack # need to reverse order of argumentVars because the last argument will be on top of the stack - bodyOps = [var.slot.store() for var in argumentVars[::-1]] - bodyOps.append(subroutineBody) + body_ops = [var.slot.store() for var in arg_vars[::-1]] + body_ops.append(subroutine_body) - sd = SubroutineDeclaration(subroutine, Seq(bodyOps)) - sd.trace = subroutineBody.trace + sd = SubroutineDeclaration(subroutine, Seq(body_ops), deferred_expr) + sd.trace = subroutine_body.trace return sd diff --git a/pyteal/ast/subroutine_test.py b/pyteal/ast/subroutine_test.py index 509ccd990..2c90c70a1 100644 --- a/pyteal/ast/subroutine_test.py +++ b/pyteal/ast/subroutine_test.py @@ -1,11 +1,13 @@ from itertools import product +from typing import List, Literal, Optional, cast + import pytest -from typing import List +from dataclasses import dataclass import pyteal as pt -from pyteal.ast.subroutine import evaluateSubroutine +from pyteal.ast.subroutine import ABIReturnSubroutine, evaluate_subroutine -options = pt.CompileOptions(version=4) +options = pt.CompileOptions(version=5) def test_subroutine_definition(): @@ -57,7 +59,7 @@ def fnWithPartialExprAnnotations(a, b: pt.Expr) -> pt.Expr: for (fn, numArgs, name) in cases: definition = pt.SubroutineDefinition(fn, pt.TealType.none) - assert definition.argumentCount() == numArgs + assert definition.argument_count() == numArgs assert definition.name() == name if numArgs > 0: @@ -78,15 +80,204 @@ def fnWithPartialExprAnnotations(a, b: pt.Expr) -> pt.Expr: assert invocation.args == args +@dataclass +class ABISubroutineTC: + definition: pt.ABIReturnSubroutine + arg_instances: list[pt.Expr | pt.abi.BaseType] + name: str + ret_type: str | pt.abi.TypeSpec + signature: Optional[str] + + +def test_abi_subroutine_definition(): + @pt.ABIReturnSubroutine + def fn_0arg_0ret() -> pt.Expr: + return pt.Return() + + @pt.ABIReturnSubroutine + def fn_0arg_uint64_ret(*, output: pt.abi.Uint64) -> pt.Expr: + return output.set(1) + + @pt.ABIReturnSubroutine + def fn_1arg_0ret(a: pt.abi.Uint64) -> pt.Expr: + return pt.Return() + + @pt.ABIReturnSubroutine + def fn_1arg_1ret(a: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + return output.set(a) + + @pt.ABIReturnSubroutine + def fn_2arg_0ret( + a: pt.abi.Uint64, b: pt.abi.StaticArray[pt.abi.Byte, Literal[10]] + ) -> pt.Expr: + return pt.Return() + + @pt.ABIReturnSubroutine + def fn_2arg_1ret( + a: pt.abi.Uint64, + b: pt.abi.StaticArray[pt.abi.Byte, Literal[10]], + *, + output: pt.abi.Byte, + ) -> pt.Expr: + return output.set(b[a.get() % pt.Int(10)]) + + @pt.ABIReturnSubroutine + def fn_2arg_1ret_with_expr( + a: pt.Expr, + b: pt.abi.StaticArray[pt.abi.Byte, Literal[10]], + *, + output: pt.abi.Byte, + ) -> pt.Expr: + return output.set(b[a % pt.Int(10)]) + + @pt.ABIReturnSubroutine + def fn_w_tuple1arg( + a: pt.Expr, + b: pt.abi.Tuple1[pt.abi.Byte], + *, + output: pt.abi.Byte, + ) -> pt.Expr: + return output.set(pt.Int(1)) + + cases = ( + ABISubroutineTC(fn_0arg_0ret, [], "fn_0arg_0ret", "void", "fn_0arg_0ret()void"), + ABISubroutineTC( + fn_0arg_uint64_ret, + [], + "fn_0arg_uint64_ret", + pt.abi.Uint64TypeSpec(), + "fn_0arg_uint64_ret()uint64", + ), + ABISubroutineTC( + fn_1arg_0ret, + [pt.abi.Uint64()], + "fn_1arg_0ret", + "void", + "fn_1arg_0ret(uint64)void", + ), + ABISubroutineTC( + fn_1arg_1ret, + [pt.abi.Uint64()], + "fn_1arg_1ret", + pt.abi.Uint64TypeSpec(), + "fn_1arg_1ret(uint64)uint64", + ), + ABISubroutineTC( + fn_2arg_0ret, + [ + pt.abi.Uint64(), + pt.abi.StaticArray( + pt.abi.StaticArrayTypeSpec(pt.abi.ByteTypeSpec(), 10) + ), + ], + "fn_2arg_0ret", + "void", + "fn_2arg_0ret(uint64,byte[10])void", + ), + ABISubroutineTC( + fn_2arg_1ret, + [ + pt.abi.Uint64(), + pt.abi.StaticArray( + pt.abi.StaticArrayTypeSpec(pt.abi.ByteTypeSpec(), 10) + ), + ], + "fn_2arg_1ret", + pt.abi.ByteTypeSpec(), + "fn_2arg_1ret(uint64,byte[10])byte", + ), + ABISubroutineTC( + fn_2arg_1ret_with_expr, + [ + pt.Int(5), + pt.abi.StaticArray( + pt.abi.StaticArrayTypeSpec(pt.abi.ByteTypeSpec(), 10) + ), + ], + "fn_2arg_1ret_with_expr", + pt.abi.ByteTypeSpec(), + None, + ), + ABISubroutineTC( + fn_w_tuple1arg, + [ + pt.Int(5), + pt.abi.make(pt.abi.Tuple1[pt.abi.Byte]), + ], + "fn_w_tuple1arg", + pt.abi.ByteTypeSpec(), + None, + ), + ) + + for case in cases: + assert case.definition.subroutine.argument_count() == len(case.arg_instances) + assert case.definition.name() == case.name + + if len(case.arg_instances) > 0: + with pytest.raises(pt.TealInputError): + case.definition(*case.arg_instances[:-1]) + + with pytest.raises(pt.TealInputError): + case.definition(*(case.arg_instances + [pt.abi.Uint64()])) + + assert case.definition.type_of() == case.ret_type + invoked = case.definition(*case.arg_instances) + assert isinstance( + invoked, (pt.Expr if case.ret_type == "void" else pt.abi.ReturnedValue) + ) + assert case.definition.is_abi_routable() == all( + map(lambda x: isinstance(x, pt.abi.BaseType), case.arg_instances) + ) + + if case.definition.is_abi_routable(): + assert case.definition.method_signature() == cast(str, case.signature) + else: + with pytest.raises(pt.TealInputError): + case.definition.method_signature() + + +def test_subroutine_return_reference(): + @ABIReturnSubroutine + def invalid_ret_type(*, output: pt.abi.Account): + return output.decode(pt.Bytes(b"\x00")) + + with pytest.raises(pt.TealInputError): + invalid_ret_type.method_signature() + + @ABIReturnSubroutine + def invalid_ret_type_collection( + *, output: pt.abi.Tuple2[pt.abi.Account, pt.abi.Uint64] + ): + return output.set(pt.abi.Account(), pt.abi.Uint64()) + + with pytest.raises(pt.TealInputError): + invalid_ret_type_collection.method_signature() + + @ABIReturnSubroutine + def invalid_ret_type_collection_nested( + *, output: pt.abi.DynamicArray[pt.abi.Tuple2[pt.abi.Account, pt.abi.Uint64]] + ): + return output.set( + pt.abi.make( + pt.abi.DynamicArray[pt.abi.Tuple2[pt.abi.Account, pt.abi.Uint64]] + ) + ) + + with pytest.raises(pt.TealInputError): + invalid_ret_type_collection_nested.method_signature() + + def test_subroutine_definition_validate(): """ DFS through SubroutineDefinition.validate()'s logic """ - def mock_subroutine_definition(implementation): + def mock_subroutine_definition(implementation, has_abi_output=False): mock = pt.SubroutineDefinition(lambda: pt.Return(pt.Int(1)), pt.TealType.uint64) mock._validate() # haven't failed with dummy implementation mock.implementation = implementation + mock.has_abi_output = has_abi_output return mock not_callable = mock_subroutine_definition("I'm not callable") @@ -97,20 +288,17 @@ def mock_subroutine_definition(implementation): "Input to SubroutineDefinition is not callable" ) - three_params = mock_subroutine_definition(lambda x, y, z: pt.Return(pt.Int(1))) - two_inputs = [pt.TealType.uint64, pt.TealType.bytes] - with pytest.raises(pt.TealInputError) as tie: - three_params._validate(input_types=two_inputs) + # input_types: - assert tie.value == pt.TealInputError( - "Provided number of input_types (2) does not match detected number of parameters (3)" - ) + three_params = mock_subroutine_definition(lambda x, y, z: pt.Return(pt.Int(1))) - params, anns, arg_types, byrefs = three_params._validate() + params, anns, arg_types, byrefs, abi_args, output_kwarg = three_params._validate() assert len(params) == 3 assert anns == {} assert all(at is pt.Expr for at in arg_types) assert byrefs == set() + assert abi_args == {} + assert output_kwarg == {} def bad_return_impl() -> str: return pt.Return(pt.Int(1)) # type: ignore @@ -123,6 +311,32 @@ def bad_return_impl() -> str: "Function has return of disallowed type . Only Expr is allowed" ) + # now we iterate through the implementation params validating each as we go + + def var_abi_output_impl(*, output: pt.abi.Uint16): + pt.Return(pt.Int(1)) # this is wrong but ignored + + # raises without abi_output_arg_name: + var_abi_output_noname = mock_subroutine_definition(var_abi_output_impl) + with pytest.raises(pt.TealInputError) as tie: + var_abi_output_noname._validate() + + assert tie.value == pt.TealInputError( + "Function has a parameter type that is not allowed in a subroutine: parameter output with type KEYWORD_ONLY" + ) + + # copacetic abi output: + var_abi_output = mock_subroutine_definition( + var_abi_output_impl, has_abi_output=True + ) + params, anns, arg_types, byrefs, abi_args, output_kwarg = var_abi_output._validate() + assert len(params) == 1 + assert anns == {"output": pt.abi.Uint16} + assert all(at is pt.Expr for at in arg_types) + assert byrefs == set() + assert abi_args == {} + assert output_kwarg == {"output": pt.abi.Uint16TypeSpec()} + var_positional = mock_subroutine_definition(lambda *args: pt.Return(pt.Int(1))) with pytest.raises(pt.TealInputError) as tie: var_positional._validate() @@ -155,44 +369,42 @@ def bad_return_impl() -> str: "Function has a parameter with a default value, which is not allowed in a subroutine: x" ) - with pytest.raises(pt.TealInputError) as tie: - three_params._validate( - input_types=[pt.TealType.uint64, pt.Expr, pt.TealType.anytype] - ) - - assert tie.value == pt.TealInputError( - "Function has input type for parameter y which is not a TealType" - ) - - # Now we get to _validate_parameter_type(): + # Now we get to _validate_annotation(): one_vanilla = mock_subroutine_definition(lambda x: pt.Return(pt.Int(1))) - params, anns, arg_types, byrefs = one_vanilla._validate() + params, anns, arg_types, byrefs, abi_args, output_kwarg = one_vanilla._validate() assert len(params) == 1 assert anns == {} assert all(at is pt.Expr for at in arg_types) assert byrefs == set() + assert abi_args == {} + assert output_kwarg == {} def one_expr_impl(x: pt.Expr): return pt.Return(pt.Int(1)) one_expr = mock_subroutine_definition(one_expr_impl) - params, anns, arg_types, byrefs = one_expr._validate() + params, anns, arg_types, byrefs, abi_args, output_kwarg = one_expr._validate() assert len(params) == 1 assert anns == {"x": pt.Expr} assert all(at is pt.Expr for at in arg_types) assert byrefs == set() + assert abi_args == {} + assert output_kwarg == {} def one_scratchvar_impl(x: pt.ScratchVar): return pt.Return(pt.Int(1)) one_scratchvar = mock_subroutine_definition(one_scratchvar_impl) - params, anns, arg_types, byrefs = one_scratchvar._validate() + params, anns, arg_types, byrefs, abi_args, output_kwarg = one_scratchvar._validate() assert len(params) == 1 assert anns == {"x": pt.ScratchVar} assert all(at is pt.ScratchVar for at in arg_types) assert byrefs == {"x"} + assert abi_args == {} + assert output_kwarg == {} + # not is_class() def one_nontype_impl(x: "blahBlah"): # type: ignore # noqa: F821 return pt.Return(pt.Int(1)) @@ -212,16 +424,130 @@ def one_dynscratchvar_impl(x: pt.DynamicScratchVar): one_dynscratchvar._validate() assert tie.value == pt.TealInputError( - "Function has parameter x of disallowed type . Only the types (, ) are allowed" + "Function has parameter x of disallowed type . Only the types (, , 'ABI') are allowed" + ) + + # Now we're back to _validate() main body and looking at input_types + + three_params_with_output = mock_subroutine_definition( + lambda x, y, z, *, output: pt.Return(pt.Int(1)), has_abi_output=True + ) + four_inputs = [ + pt.TealType.uint64, + pt.TealType.uint64, + pt.TealType.bytes, + pt.TealType.uint64, + ] + + two_inputs = [pt.TealType.uint64, pt.TealType.bytes] + with pytest.raises(pt.TealInputError) as tie: + three_params._validate(input_types=two_inputs) + + assert tie.value == pt.TealInputError( + "Provided number of input_types (2) does not match detected number of input parameters (3)" + ) + + three_inputs_with_a_wrong_type = [pt.TealType.uint64, pt.Expr, pt.TealType.bytes] + + with pytest.raises(pt.TealInputError) as tie: + three_params._validate(input_types=three_inputs_with_a_wrong_type) + + assert tie.value == pt.TealInputError( + "Function has input type for parameter y which is not a TealType" + ) + + with pytest.raises(pt.TealInputError) as tie: + three_params._validate( + input_types=[pt.TealType.uint64, pt.Expr, pt.TealType.anytype] + ) + + assert tie.value == pt.TealInputError( + "Function has input type for parameter y which is not a TealType" + ) + + with pytest.raises(pt.TealInputError) as tie: + three_params._validate( + input_types=[pt.TealType.uint64, None, pt.TealType.anytype] + ) + assert tie.value == pt.TealInputError( + "input_type for y is unspecified i.e. None but this is only allowed for ABI arguments" ) - # Now we're back to validate() and everything should be copacetic + # this one gets caught inside of _validate_annotation() + with pytest.raises(pt.TealInputError) as tie: + three_params_with_output._validate(input_types=four_inputs) + + # everything should be copacetic for x, y, z in product(pt.TealType, pt.TealType, pt.TealType): - params, anns, arg_types, byrefs = three_params._validate(input_types=[x, y, z]) + ( + params, + anns, + arg_types, + byrefs, + abi_args, + output_kwarg, + ) = three_params._validate(input_types=[x, y, z]) assert len(params) == 3 assert anns == {} assert all(at is pt.Expr for at in arg_types) assert byrefs == set() + assert abi_args == {} + assert output_kwarg == {} + + # annotation / abi type handling: + abi_annotation_examples = { + pt.abi.Address: pt.abi.AddressTypeSpec(), + pt.abi.Bool: pt.abi.BoolTypeSpec(), + pt.abi.Byte: pt.abi.ByteTypeSpec(), + pt.abi.DynamicArray[pt.abi.Bool]: pt.abi.DynamicArrayTypeSpec( + pt.abi.BoolTypeSpec() + ), + pt.abi.StaticArray[pt.abi.Uint32, Literal[10]]: pt.abi.StaticArrayTypeSpec( + pt.abi.Uint32TypeSpec(), 10 + ), + pt.abi.String: pt.abi.StringTypeSpec(), + pt.abi.Tuple2[pt.abi.Bool, pt.abi.Uint32]: pt.abi.TupleTypeSpec( + pt.abi.BoolTypeSpec(), pt.abi.Uint32TypeSpec() + ), + pt.abi.Uint8: pt.abi.Uint8TypeSpec(), + pt.abi.Uint16: pt.abi.Uint16TypeSpec(), + pt.abi.Uint32: pt.abi.Uint32TypeSpec(), + pt.abi.Uint64: pt.abi.Uint64TypeSpec(), + } + + anns = (pt.Expr, pt.ScratchVar) + tuple(abi_annotation_examples.keys()) + for x_ann, z_ann in product(anns, anns): + + def mocker_impl(x: x_ann, y, z: z_ann): + return pt.Return(pt.Int(1)) + + mocker = mock_subroutine_definition(mocker_impl) + params, anns, arg_types, byrefs, abis, output_kwarg = mocker._validate() + print( + f"{x_ann=}, {z_ann=}, {params=}, {anns=}, {arg_types=}, {byrefs=}, {abis=}, {output_kwarg=}" + ) + + assert len(params) == 3 + + assert anns == {"x": x_ann, "z": z_ann} + + assert ( + (arg_types[0] is x_ann or arg_types[0] == abi_annotation_examples[x_ann]) + and arg_types[1] is pt.Expr + and ( + arg_types[2] is z_ann or arg_types[2] == abi_annotation_examples[z_ann] + ) + ), f"{arg_types[0]} -> {x_ann} and {arg_types[1]} -> {pt.Expr} and {arg_types[2]} -> {z_ann}" + + assert byrefs == set(["x"] if x_ann is pt.ScratchVar else []) | set( + ["z"] if z_ann is pt.ScratchVar else [] + ) + expected_abis = {} + if x_ann not in (pt.Expr, pt.ScratchVar): + expected_abis["x"] = abi_annotation_examples[x_ann] + if z_ann not in (pt.Expr, pt.ScratchVar): + expected_abis["z"] = abi_annotation_examples[z_ann] + assert abis == expected_abis def test_subroutine_invocation_param_types(): @@ -234,6 +560,13 @@ def fnWithExprAnnotations(a: pt.Expr, b: pt.Expr) -> pt.Expr: def fnWithSVAnnotations(a: pt.ScratchVar, b: pt.ScratchVar): return pt.Return() + def fnWithABIAnnotations( + a: pt.abi.Byte, + b: pt.abi.StaticArray[pt.abi.Uint32, Literal[10]], + c: pt.abi.DynamicArray[pt.abi.Bool], + ): + return pt.Return() + def fnWithMixedAnns1(a: pt.ScratchVar, b: pt.Expr) -> pt.Expr: return pt.Return() @@ -243,9 +576,22 @@ def fnWithMixedAnns2(a: pt.ScratchVar, b) -> pt.Expr: def fnWithMixedAnns3(a: pt.Expr, b: pt.ScratchVar): return pt.Return() + def fnWithMixedAnns4(a: pt.ScratchVar, b, c: pt.abi.Uint16) -> pt.Expr: + return pt.Return() + sv = pt.ScratchVar() x = pt.Int(42) s = pt.Bytes("hello") + av_u16 = pt.abi.Uint16() + av_bool_dym_arr = pt.abi.DynamicArray( + pt.abi.DynamicArrayTypeSpec(pt.abi.BoolTypeSpec()) + ) + av_u32_static_arr = pt.abi.StaticArray( + pt.abi.StaticArrayTypeSpec(pt.abi.Uint32TypeSpec(), 10) + ) + av_bool = pt.abi.Bool() + av_byte = pt.abi.Byte() + cases = [ ("vanilla 1", fnWithNoAnnotations, [x, s], None), ("vanilla 2", fnWithNoAnnotations, [x, x], None), @@ -256,6 +602,61 @@ def fnWithMixedAnns3(a: pt.Expr, b: pt.ScratchVar): ("all sv's 1", fnWithSVAnnotations, [sv, sv], None), ("all sv's but strings", fnWithSVAnnotations, [s, s], pt.TealInputError), ("all sv's but ints", fnWithSVAnnotations, [x, x], pt.TealInputError), + ( + "all abi's 1", + fnWithABIAnnotations, + [av_byte, av_u32_static_arr, av_bool_dym_arr], + None, + ), + ( + "all abi's but ints 1", + fnWithABIAnnotations, + [x, av_u32_static_arr, av_bool_dym_arr], + pt.TealInputError, + ), + ( + "all abi's but ints 2", + fnWithABIAnnotations, + [x, av_u32_static_arr, x], + pt.TealInputError, + ), + ("all abi's but ints 3", fnWithABIAnnotations, [x, x, x], pt.TealInputError), + ( + "all abi's but sv's 1", + fnWithABIAnnotations, + [sv, av_u32_static_arr, av_bool_dym_arr], + pt.TealInputError, + ), + ( + "all abi's but sv's 2", + fnWithABIAnnotations, + [av_byte, av_u32_static_arr, sv], + pt.TealInputError, + ), + ( + "all abi's but sv's 3", + fnWithABIAnnotations, + [av_byte, sv, av_u32_static_arr], + pt.TealInputError, + ), + ( + "all abi's but wrong typed 1", + fnWithABIAnnotations, + [av_u32_static_arr, av_u32_static_arr, av_bool_dym_arr], + pt.TealInputError, + ), + ( + "all abi's but wrong typed 2", + fnWithABIAnnotations, + [av_bool, av_bool_dym_arr, av_u16], + pt.TealInputError, + ), + ( + "all abi's but wrong typed 3", + fnWithABIAnnotations, + [av_u16, av_bool, av_byte], + pt.TealInputError, + ), ("mixed1 copacetic", fnWithMixedAnns1, [sv, x], None), ("mixed1 flipped", fnWithMixedAnns1, [x, sv], pt.TealInputError), ("mixed1 missing the sv", fnWithMixedAnns1, [x, s], pt.TealInputError), @@ -267,10 +668,29 @@ def fnWithMixedAnns3(a: pt.Expr, b: pt.ScratchVar): ("mixed3 copacetic", fnWithMixedAnns3, [s, sv], None), ("mixed3 flipped", fnWithMixedAnns3, [sv, x], pt.TealInputError), ("mixed3 missing the sv", fnWithMixedAnns3, [x, s], pt.TealInputError), + ("mixed anno", fnWithMixedAnns4, [sv, x, av_u16], None), + ( + "mixed anno but wrong typed 1", + fnWithMixedAnns4, + [av_byte, x, av_u16], + pt.TealInputError, + ), + ( + "mixed anno but wrong typed 2", + fnWithMixedAnns4, + [sv, av_byte, sv], + pt.TealInputError, + ), + ( + "mixed anno but wrong typed 3", + fnWithMixedAnns4, + [sv, x, av_byte], + pt.TealInputError, + ), ] for case_name, fn, args, err in cases: definition = pt.SubroutineDefinition(fn, pt.TealType.none) - assert definition.argumentCount() == len(args), case_name + assert definition.argument_count() == len(args), case_name assert definition.name() == fn.__name__, case_name if err is None: @@ -294,11 +714,220 @@ def fnWithMixedAnns3(a: pt.Expr, b: pt.ScratchVar): ), f"EXPECTED ERROR of type {err}. encountered unexpected error during invocation case <{case_name}>: {e}" +def test_abi_subroutine_calling_param_types(): + @pt.ABIReturnSubroutine + def fn_log_add(a: pt.abi.Uint64, b: pt.abi.Uint32) -> pt.Expr: + return pt.Seq(pt.Log(pt.Itob(a.get() + b.get())), pt.Return()) + + @pt.ABIReturnSubroutine + def fn_ret_add( + a: pt.abi.Uint64, b: pt.abi.Uint32, *, output: pt.abi.Uint64 + ) -> pt.Expr: + return output.set(a.get() + b.get() + pt.Int(0xA190)) + + @pt.ABIReturnSubroutine + def fn_abi_annotations_0( + a: pt.abi.Byte, + b: pt.abi.StaticArray[pt.abi.Uint32, Literal[10]], + c: pt.abi.DynamicArray[pt.abi.Bool], + ) -> pt.Expr: + return pt.Return() + + @pt.ABIReturnSubroutine + def fn_abi_annotations_0_with_ret( + a: pt.abi.Byte, + b: pt.abi.StaticArray[pt.abi.Uint32, Literal[10]], + c: pt.abi.DynamicArray[pt.abi.Bool], + *, + output: pt.abi.Byte, + ): + return output.set(a) + + @pt.ABIReturnSubroutine + def fn_mixed_annotations_0(a: pt.ScratchVar, b: pt.Expr, c: pt.abi.Byte) -> pt.Expr: + return pt.Seq( + a.store(c.get() * pt.Int(0x0FF1CE) * b), + pt.Return(), + ) + + @pt.ABIReturnSubroutine + def fn_mixed_annotations_0_with_ret( + a: pt.ScratchVar, b: pt.Expr, c: pt.abi.Byte, *, output: pt.abi.Uint64 + ) -> pt.Expr: + return pt.Seq( + a.store(c.get() * pt.Int(0x0FF1CE) * b), + output.set(a.load()), + ) + + @pt.ABIReturnSubroutine + def fn_mixed_annotation_1( + a: pt.ScratchVar, b: pt.abi.StaticArray[pt.abi.Uint32, Literal[10]] + ) -> pt.Expr: + return pt.Seq( + (intermediate := pt.abi.Uint32()).set(b[a.load() % pt.Int(10)]), + a.store(intermediate.get()), + pt.Return(), + ) + + @pt.ABIReturnSubroutine + def fn_mixed_annotation_1_with_ret( + a: pt.ScratchVar, b: pt.abi.Uint64, *, output: pt.abi.Bool + ) -> pt.Expr: + return output.set((a.load() + b.get()) % pt.Int(2)) + + abi_u64 = pt.abi.Uint64() + abi_u32 = pt.abi.Uint32() + abi_byte = pt.abi.Byte() + abi_static_u32_10 = pt.abi.StaticArray( + pt.abi.StaticArrayTypeSpec(pt.abi.Uint32TypeSpec(), 10) + ) + abi_dynamic_bool = pt.abi.DynamicArray( + pt.abi.DynamicArrayTypeSpec(pt.abi.BoolTypeSpec()) + ) + sv = pt.ScratchVar() + expr_int = pt.Int(1) + + cases = [ + ("vanilla 1", fn_log_add, [abi_u64, abi_u32], "void", None), + ( + "vanilla 1 with wrong ABI type", + fn_log_add, + [abi_u64, abi_u64], + None, + pt.TealInputError, + ), + ( + "vanilla 1 with ABI return", + fn_ret_add, + [abi_u64, abi_u32], + pt.abi.Uint64TypeSpec(), + None, + ), + ( + "vanilla 1 with ABI return wrong typed", + fn_ret_add, + [abi_u32, abi_u64], + None, + pt.TealInputError, + ), + ( + "full ABI annotations no return", + fn_abi_annotations_0, + [abi_byte, abi_static_u32_10, abi_dynamic_bool], + "void", + None, + ), + ( + "full ABI annotations wrong input 0", + fn_abi_annotations_0, + [abi_u64, abi_static_u32_10, abi_dynamic_bool], + None, + pt.TealInputError, + ), + ( + "full ABI annotations with ABI return", + fn_abi_annotations_0_with_ret, + [abi_byte, abi_static_u32_10, abi_dynamic_bool], + pt.abi.ByteTypeSpec(), + None, + ), + ( + "full ABI annotations with ABI return wrong inputs", + fn_abi_annotations_0_with_ret, + [abi_byte, abi_dynamic_bool, abi_static_u32_10], + None, + pt.TealInputError, + ), + ( + "mixed with ABI annotations 0", + fn_mixed_annotations_0, + [sv, expr_int, abi_byte], + "void", + None, + ), + ( + "mixed with ABI annotations 0 wrong inputs", + fn_mixed_annotations_0, + [abi_u64, expr_int, abi_byte], + None, + pt.TealInputError, + ), + ( + "mixed with ABI annotations 0 with ABI return", + fn_mixed_annotations_0_with_ret, + [sv, expr_int, abi_byte], + pt.abi.Uint64TypeSpec(), + None, + ), + ( + "mixed with ABI annotations 0 with ABI return wrong inputs", + fn_mixed_annotations_0_with_ret, + [sv, expr_int, sv], + None, + pt.TealInputError, + ), + ( + "mixed with ABI annotations 1", + fn_mixed_annotation_1, + [sv, abi_static_u32_10], + "void", + None, + ), + ( + "mixed with ABI annotations 1 with ABI return", + fn_mixed_annotation_1_with_ret, + [sv, abi_u64], + pt.abi.BoolTypeSpec(), + None, + ), + ( + "mixed with ABI annotations 1 with ABI return wrong inputs", + fn_mixed_annotation_1_with_ret, + [expr_int, abi_static_u32_10], + None, + pt.TealInputError, + ), + ] + + for case_name, definition, args, ret_type, err in cases: + assert definition.subroutine.argument_count() == len(args), case_name + assert ( + definition.name() == definition.subroutine.implementation.__name__ + ), case_name + + if err is None: + invocation = definition(*args) + if ret_type == "void": + assert isinstance(invocation, pt.SubroutineCall), case_name + assert not invocation.has_return(), case_name + assert invocation.args == args, case_name + else: + assert isinstance(invocation, pt.abi.ReturnedValue), case_name + assert invocation.type_spec == ret_type + assert isinstance(invocation.computation, pt.SubroutineCall), case_name + assert not invocation.computation.has_return(), case_name + assert invocation.computation.args == args, case_name + else: + try: + with pytest.raises(err): + definition(*args) + except Exception as e: + assert ( + not e + ), f"EXPECTED ERROR of type {err}. encountered unexpected error during invocation case <{case_name}>: {e}" + + def test_subroutine_definition_invalid(): def fnWithDefaults(a, b=None): return pt.Return() - def fnWithKeywordArgs(a, *, b): + def fnWithKeywordArgs(a, *, output): + return pt.Return() + + def fnWithKeywordArgsWrongKWName(a, *, b: pt.abi.Uint64): + return pt.Return() + + def fnWithMultipleABIKeywordArgs(a, *, b: pt.abi.Byte, c: pt.abi.Bool): return pt.Return() def fnWithVariableArgs(a, *b): @@ -317,51 +946,107 @@ def fnReturningExprSubclass(a: pt.ScratchVar, b: pt.Expr) -> pt.Return: return pt.Return() def fnWithMixedAnns4AndBytesReturn(a: pt.Expr, b: pt.ScratchVar) -> pt.Bytes: - return pt.Bytes("helo") + return pt.Bytes("hello uwu") + + def fnWithMixedAnnsABIRet1( + a: pt.Expr, b: pt.ScratchVar, c: pt.abi.Uint16 + ) -> pt.abi.StaticArray[pt.abi.Uint32, Literal[10]]: + return pt.abi.StaticArray( + pt.abi.StaticArrayTypeSpec(pt.abi.Uint32TypeSpec(), 10) + ) + + def fnWithMixedAnnsABIRet2( + a: pt.Expr, b: pt.abi.Byte, c: pt.ScratchVar + ) -> pt.abi.Uint64: + return pt.abi.Uint64() cases = ( - (1, "TealInputError('Input to SubroutineDefinition is not callable'"), - (None, "TealInputError('Input to SubroutineDefinition is not callable'"), + ( + 1, + "TealInputError('Input to SubroutineDefinition is not callable'", + "TealInputError('Input to ABIReturnSubroutine is not callable'", + ), + ( + None, + "TealInputError('Input to SubroutineDefinition is not callable'", + "TealInputError('Input to ABIReturnSubroutine is not callable'", + ), ( fnWithDefaults, "TealInputError('Function has a parameter with a default value, which is not allowed in a subroutine: b'", + "TealInputError('Function has a parameter with a default value, which is not allowed in a subroutine: b'", ), ( fnWithKeywordArgs, + "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter output with type", + "TealInputError('ABI return subroutine output-kwarg output must specify ABI type')", + ), + ( + fnWithKeywordArgsWrongKWName, "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter b with type", + "TealInputError('ABI return subroutine output-kwarg name must be `output` at this moment", + ), + ( + fnWithMultipleABIKeywordArgs, + "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter b with type", + "multiple output arguments (2) with type annotations", ), ( fnWithVariableArgs, "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter b with type", + "Function has a parameter type that is not allowed in a subroutine: parameter b with type VAR_POSITIONAL", ), ( fnWithNonExprReturnAnnotation, "Function has return of disallowed type TealType.uint64. Only Expr is allowed", + "Function has return of disallowed type TealType.uint64. Only Expr is allowed", ), ( fnWithNonExprParamAnnotation, "Function has parameter b of declared type TealType.uint64 which is not a class", + "Function has parameter b of declared type TealType.uint64 which is not a class", ), ( fnWithScratchVarSubclass, "Function has parameter b of disallowed type ", + "Function has parameter b of disallowed type ", ), ( fnReturningExprSubclass, "Function has return of disallowed type ", + "Function has return of disallowed type . Only Expr is allowed", ), ( fnWithMixedAnns4AndBytesReturn, "Function has return of disallowed type ", + "Function has return of disallowed type . Only Expr is allowed", + ), + ( + fnWithMixedAnnsABIRet1, + "Function has return of disallowed type pyteal.abi.StaticArray[pyteal.abi.Uint32, typing.Literal[10]]. " + "Only Expr is allowed", + "Function has return of disallowed type pyteal.abi.StaticArray[pyteal.abi.Uint32, typing.Literal[10]]. " + "Only Expr is allowed", + ), + ( + fnWithMixedAnnsABIRet2, + "Function has return of disallowed type . Only Expr is allowed", + "Function has return of disallowed type . Only Expr is allowed", ), ) - for fn, msg in cases: + for fn, sub_def_msg, abi_sub_def_msg in cases: with pytest.raises(pt.TealInputError) as e: - print(f"case=[{msg}]") + print(f"case=[{sub_def_msg}]") pt.SubroutineDefinition(fn, pt.TealType.none) - assert msg in str(e), "failed for case [{}]".format(fn.__name__) + assert sub_def_msg in str(e), f"failed for case [{fn.__name__}]" + + with pytest.raises(pt.TealInputError) as e: + print(f"case=[{abi_sub_def_msg}]") + pt.ABIReturnSubroutine(fn) + + assert abi_sub_def_msg in str(e), f"failed for case[{fn.__name__}]" def test_subroutine_declaration(): @@ -468,8 +1153,8 @@ def mySubroutine(): return returnValue definition = pt.SubroutineDefinition(mySubroutine, returnType) + declaration = evaluate_subroutine(definition) - declaration = evaluateSubroutine(definition) assert isinstance(declaration, pt.SubroutineDeclaration) assert declaration.subroutine is definition @@ -502,8 +1187,8 @@ def mySubroutine(a1): return returnValue definition = pt.SubroutineDefinition(mySubroutine, returnType) + declaration = evaluate_subroutine(definition) - declaration = evaluateSubroutine(definition) assert isinstance(declaration, pt.SubroutineDeclaration) assert declaration.subroutine is definition @@ -546,7 +1231,8 @@ def mySubroutine(a1, a2): definition = pt.SubroutineDefinition(mySubroutine, returnType) - declaration = evaluateSubroutine(definition) + declaration = evaluate_subroutine(definition) + assert isinstance(declaration, pt.SubroutineDeclaration) assert declaration.subroutine is definition @@ -591,8 +1277,8 @@ def mySubroutine(a1, a2, a3, a4, a5, a6, a7, a8, a9, a10): return returnValue definition = pt.SubroutineDefinition(mySubroutine, returnType) + declaration = evaluate_subroutine(definition) - declaration = evaluateSubroutine(definition) assert isinstance(declaration, pt.SubroutineDeclaration) assert declaration.subroutine is definition diff --git a/pyteal/ast/unaryexpr.py b/pyteal/ast/unaryexpr.py index 619bfab46..da2dbb165 100644 --- a/pyteal/ast/unaryexpr.py +++ b/pyteal/ast/unaryexpr.py @@ -31,7 +31,7 @@ def __teal__(self, options: "CompileOptions"): return TealBlock.FromOp(options, TealOp(self, self.op), self.arg) def __str__(self): - return "({} {})".format(self.op, self.arg) + return "({} {})".format(str(self.op).title().replace("_", ""), self.arg) def type_of(self): return self.outputType diff --git a/pyteal/compiler/compiler.py b/pyteal/compiler/compiler.py index 5a6709efe..5db55b00b 100644 --- a/pyteal/compiler/compiler.py +++ b/pyteal/compiler/compiler.py @@ -10,7 +10,7 @@ SubroutineDefinition, SubroutineDeclaration, ) -from pyteal.ir import Mode, TealComponent, TealOp, TealBlock, TealSimpleBlock +from pyteal.ir import Mode, Op, TealComponent, TealOp, TealBlock, TealSimpleBlock from pyteal.errors import TealInputError, TealInternalError from pyteal.compiler.sort import sortBlocks @@ -70,7 +70,7 @@ def addLoopContinueBlock(self, block: TealSimpleBlock) -> None: def exitLoop(self) -> Tuple[List[TealSimpleBlock], List[TealSimpleBlock]]: if len(self.breakBlocksStack) == 0 or len(self.continueBlocksStack) == 0: raise TealInternalError("Cannot exit loop when no loop is active") - return (self.breakBlocksStack.pop(), self.continueBlocksStack.pop()) + return self.breakBlocksStack.pop(), self.continueBlocksStack.pop() def verifyOpsForVersion(teal: List[TealComponent], version: int): @@ -139,26 +139,60 @@ def compileSubroutine( ast = ret_expr options.setSubroutine(currentSubroutine) + start, end = ast.__teal__(options) start.addIncoming() start.validateTree() - start = TealBlock.NormalizeBlocks(start) - start.validateTree() + if ( + currentSubroutine is not None + and currentSubroutine.get_declaration().deferred_expr is not None + ): + # this represents code that should be inserted before each retsub op + deferred_expr = cast(Expr, currentSubroutine.get_declaration().deferred_expr) + + for block in TealBlock.Iterate(start): + if not any(op.getOp() == Op.retsub for op in block.ops): + continue + + if len(block.ops) != 1: + # we expect all retsub ops to be in their own block at this point since + # TealBlock.NormalizeBlocks has not yet been used + raise TealInternalError( + f"Expected retsub to be the only op in the block, but there are {len(block.ops)} ops" + ) - order = sortBlocks(start, end) - teal = flattenBlocks(order) + # we invoke __teal__ here and not outside of this loop because the same block cannot be + # added in multiple places to the control flow graph + deferred_start, deferred_end = deferred_expr.__teal__(options) + deferred_start.addIncoming() + deferred_start.validateTree() - verifyOpsForVersion(teal, options.version) - verifyOpsForMode(teal, options.mode) + # insert deferred blocks between the previous block(s) and this one + deferred_start.incoming = block.incoming + block.incoming = [deferred_end] + deferred_end.nextBlock = block + + for prev in deferred_start.incoming: + prev.replaceOutgoing(block, deferred_start) + + if block is start: + # this is the start block, replace start + start = deferred_start + + start.validateTree() + + start = TealBlock.NormalizeBlocks(start) + start.validateTree() subroutine_start_blocks[currentSubroutine] = start subroutine_end_blocks[currentSubroutine] = end referencedSubroutines: Set[SubroutineDefinition] = set() - for stmt in teal: - for subroutine in stmt.getSubroutines(): - referencedSubroutines.add(subroutine) + for block in TealBlock.Iterate(start): + for stmt in block.ops: + for subroutine in stmt.getSubroutines(): + referencedSubroutines.add(subroutine) if currentSubroutine is not None: subroutineGraph[currentSubroutine] = referencedSubroutines @@ -166,7 +200,7 @@ def compileSubroutine( newSubroutines = referencedSubroutines - subroutine_start_blocks.keys() for subroutine in sorted(newSubroutines, key=lambda subroutine: subroutine.id): compileSubroutine( - subroutine.getDeclaration(), + subroutine.get_declaration(), options, subroutineGraph, subroutine_start_blocks, @@ -262,6 +296,9 @@ def compileTeal( subroutineLabels = resolveSubroutines(subroutineMapping) teal = flattenSubroutines(subroutineMapping, subroutineLabels) + verifyOpsForVersion(teal, options.version) + verifyOpsForMode(teal, options.mode) + if assembleConstants: if version < 3: raise TealInternalError( diff --git a/pyteal/compiler/compiler_test.py b/pyteal/compiler/compiler_test.py index 0d6fdbe09..40dc987f6 100644 --- a/pyteal/compiler/compiler_test.py +++ b/pyteal/compiler/compiler_test.py @@ -1659,6 +1659,194 @@ def storeValue(key: pt.Expr, t1: pt.Expr, t2: pt.Expr, t3: pt.Expr) -> pt.Expr: assert actual == expected +def test_compile_subroutine_deferred_expr(): + @pt.Subroutine(pt.TealType.none) + def deferredExample(value: pt.Expr) -> pt.Expr: + return pt.Seq( + pt.If(value == pt.Int(0)).Then(pt.Return()), + pt.If(value == pt.Int(1)).Then(pt.Approve()), + pt.If(value == pt.Int(2)).Then(pt.Reject()), + pt.If(value == pt.Int(3)).Then(pt.Err()), + ) + + program = pt.Seq(deferredExample(pt.Int(10)), pt.Approve()) + + expected_no_deferred = """#pragma version 6 +int 10 +callsub deferredExample_0 +int 1 +return + +// deferredExample +deferredExample_0: +store 0 +load 0 +int 0 +== +bnz deferredExample_0_l7 +load 0 +int 1 +== +bnz deferredExample_0_l6 +load 0 +int 2 +== +bnz deferredExample_0_l5 +load 0 +int 3 +== +bz deferredExample_0_l8 +err +deferredExample_0_l5: +int 0 +return +deferredExample_0_l6: +int 1 +return +deferredExample_0_l7: +retsub +deferredExample_0_l8: +retsub + """.strip() + actual_no_deferred = pt.compileTeal( + program, pt.Mode.Application, version=6, assembleConstants=False + ) + assert actual_no_deferred == expected_no_deferred + + # manually add deferred expression to SubroutineDefinition + declaration = deferredExample.subroutine.get_declaration() + declaration.deferred_expr = pt.Pop(pt.Bytes("deferred")) + + expected_deferred = """#pragma version 6 +int 10 +callsub deferredExample_0 +int 1 +return + +// deferredExample +deferredExample_0: +store 0 +load 0 +int 0 +== +bnz deferredExample_0_l7 +load 0 +int 1 +== +bnz deferredExample_0_l6 +load 0 +int 2 +== +bnz deferredExample_0_l5 +load 0 +int 3 +== +bz deferredExample_0_l8 +err +deferredExample_0_l5: +int 0 +return +deferredExample_0_l6: +int 1 +return +deferredExample_0_l7: +byte "deferred" +pop +retsub +deferredExample_0_l8: +byte "deferred" +pop +retsub + """.strip() + actual_deferred = pt.compileTeal( + program, pt.Mode.Application, version=6, assembleConstants=False + ) + assert actual_deferred == expected_deferred + + +def test_compile_subroutine_deferred_expr_empty(): + @pt.Subroutine(pt.TealType.none) + def empty() -> pt.Expr: + return pt.Return() + + program = pt.Seq(empty(), pt.Approve()) + + expected_no_deferred = """#pragma version 6 +callsub empty_0 +int 1 +return + +// empty +empty_0: +retsub + """.strip() + actual_no_deferred = pt.compileTeal( + program, pt.Mode.Application, version=6, assembleConstants=False + ) + assert actual_no_deferred == expected_no_deferred + + # manually add deferred expression to SubroutineDefinition + declaration = empty.subroutine.get_declaration() + declaration.deferred_expr = pt.Pop(pt.Bytes("deferred")) + + expected_deferred = """#pragma version 6 +callsub empty_0 +int 1 +return + +// empty +empty_0: +byte "deferred" +pop +retsub + """.strip() + actual_deferred = pt.compileTeal( + program, pt.Mode.Application, version=6, assembleConstants=False + ) + assert actual_deferred == expected_deferred + + +def test_compileSubroutine_deferred_block_malformed(): + class BadRetsub(pt.Expr): + def type_of(self) -> pt.TealType: + return pt.TealType.none + + def has_return(self) -> bool: + return True + + def __str__(self) -> str: + return "(BadRetsub)" + + def __teal__( + self, options: pt.CompileOptions + ) -> tuple[pt.TealBlock, pt.TealSimpleBlock]: + block = pt.TealSimpleBlock( + [ + pt.TealOp(self, pt.Op.int, 1), + pt.TealOp(self, pt.Op.pop), + pt.TealOp(self, pt.Op.retsub), + ] + ) + + return block, block + + @pt.Subroutine(pt.TealType.none) + def bad() -> pt.Expr: + return BadRetsub() + + program = pt.Seq(bad(), pt.Approve()) + + # manually add deferred expression to SubroutineDefinition + declaration = bad.subroutine.get_declaration() + declaration.deferred_expr = pt.Pop(pt.Bytes("deferred")) + + with pytest.raises( + pt.TealInternalError, + match=r"^Expected retsub to be the only op in the block, but there are 3 ops$", + ): + pt.compileTeal(program, pt.Mode.Application, version=6, assembleConstants=False) + + def test_compile_wide_ratio(): cases = ( ( @@ -1816,3 +2004,1415 @@ def test_compile_wide_ratio(): program, pt.Mode.Application, version=5, assembleConstants=False ) assert actual == expected.strip() + + +def test_compile_abi_subroutine_return(): + @pt.ABIReturnSubroutine + def abi_sum( + toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64 + ) -> pt.Expr: + i = pt.ScratchVar(pt.TealType.uint64) + valueAtIndex = pt.abi.Uint64() + return pt.Seq( + output.set(0), + pt.For( + i.store(pt.Int(0)), + i.load() < toSum.length(), + i.store(i.load() + pt.Int(1)), + ).Do( + pt.Seq( + toSum[i.load()].store_into(valueAtIndex), + output.set(output.get() + valueAtIndex.get()), + ) + ), + ) + + program = pt.Seq( + (to_sum_arr := pt.abi.make(pt.abi.DynamicArray[pt.abi.Uint64])).decode( + pt.Txn.application_args[1] + ), + (res := pt.abi.Uint64()).set(abi_sum(to_sum_arr)), + pt.abi.MethodReturn(res), + pt.Approve(), + ) + + expected_sum = """#pragma version 6 +txna ApplicationArgs 1 +store 0 +load 0 +callsub abisum_0 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return + +// abi_sum +abisum_0: +store 2 +int 0 +store 3 +int 0 +store 4 +abisum_0_l1: +load 4 +load 2 +int 0 +extract_uint16 +store 6 +load 6 +< +bz abisum_0_l3 +load 2 +int 8 +load 4 +* +int 2 ++ +extract_uint64 +store 5 +load 3 +load 5 ++ +store 3 +load 4 +int 1 ++ +store 4 +b abisum_0_l1 +abisum_0_l3: +load 3 +retsub + """.strip() + + actual_sum = pt.compileTeal(program, pt.Mode.Application, version=6) + assert expected_sum == actual_sum + + @pt.ABIReturnSubroutine + def conditional_factorial( + _factor: pt.abi.Uint64, *, output: pt.abi.Uint64 + ) -> pt.Expr: + i = pt.ScratchVar(pt.TealType.uint64) + + return pt.Seq( + output.set(1), + pt.If(_factor.get() <= pt.Int(1)) + .Then(pt.Return()) + .Else( + pt.For( + i.store(_factor.get()), + i.load() > pt.Int(1), + i.store(i.load() - pt.Int(1)), + ).Do(output.set(output.get() * i.load())), + ), + ) + + program_cond_factorial = pt.Seq( + (factor := pt.abi.Uint64()).decode(pt.Txn.application_args[1]), + (res := pt.abi.Uint64()).set(conditional_factorial(factor)), + pt.abi.MethodReturn(res), + pt.Approve(), + ) + + expected_conditional_factorial = """#pragma version 6 +txna ApplicationArgs 1 +btoi +store 0 +load 0 +callsub conditionalfactorial_0 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return + +// conditional_factorial +conditionalfactorial_0: +store 2 +int 1 +store 3 +load 2 +int 1 +<= +bnz conditionalfactorial_0_l4 +load 2 +store 4 +conditionalfactorial_0_l2: +load 4 +int 1 +> +bz conditionalfactorial_0_l5 +load 3 +load 4 +* +store 3 +load 4 +int 1 +- +store 4 +b conditionalfactorial_0_l2 +conditionalfactorial_0_l4: +load 3 +retsub +conditionalfactorial_0_l5: +load 3 +retsub + """.strip() + + actual_conditional_factorial = pt.compileTeal( + program_cond_factorial, pt.Mode.Application, version=6 + ) + assert actual_conditional_factorial == expected_conditional_factorial + + @pt.ABIReturnSubroutine + def load_b4_set(*, output: pt.abi.Bool): + return pt.Return() + + program_load_b4_set_broken = pt.Seq( + (_ := pt.abi.Bool()).set(load_b4_set()), pt.Approve() + ) + + with pytest.raises(pt.TealInternalError): + pt.compileTeal(program_load_b4_set_broken, pt.Mode.Application, version=6) + + @pt.ABIReturnSubroutine + def access_b4_store(magic_num: pt.abi.Uint64, *, output: pt.abi.Uint64): + return pt.Seq(output.set(output.get() ^ magic_num.get())) + + program_access_b4_store_broken = pt.Seq( + (other_party_magic := pt.abi.Uint64()).decode(pt.Txn.application_args[1]), + (_ := pt.abi.Uint64()).set(access_b4_store(other_party_magic)), + pt.Approve(), + ) + + with pytest.raises(pt.TealInternalError): + pt.compileTeal(program_access_b4_store_broken, pt.Mode.Application, version=6) + + +def test_router_app(): + def add_methods_to_router(router: pt.Router): + @pt.ABIReturnSubroutine + def add( + a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64 + ) -> pt.Expr: + return output.set(a.get() + b.get()) + + meth = router.add_method_handler(add) + assert meth.method_signature() == "add(uint64,uint64)uint64" + + @pt.ABIReturnSubroutine + def sub( + a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64 + ) -> pt.Expr: + return output.set(a.get() - b.get()) + + meth = router.add_method_handler(sub) + assert meth.method_signature() == "sub(uint64,uint64)uint64" + + @pt.ABIReturnSubroutine + def mul( + a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64 + ) -> pt.Expr: + return output.set(a.get() * b.get()) + + meth = router.add_method_handler(mul) + assert meth.method_signature() == "mul(uint64,uint64)uint64" + + @pt.ABIReturnSubroutine + def div( + a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64 + ) -> pt.Expr: + return output.set(a.get() / b.get()) + + meth = router.add_method_handler(div) + assert meth.method_signature() == "div(uint64,uint64)uint64" + + @pt.ABIReturnSubroutine + def mod( + a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64 + ) -> pt.Expr: + return output.set(a.get() % b.get()) + + meth = router.add_method_handler(mod) + assert meth.method_signature() == "mod(uint64,uint64)uint64" + + @pt.ABIReturnSubroutine + def all_laid_to_args( + _a: pt.abi.Uint64, + _b: pt.abi.Uint64, + _c: pt.abi.Uint64, + _d: pt.abi.Uint64, + _e: pt.abi.Uint64, + _f: pt.abi.Uint64, + _g: pt.abi.Uint64, + _h: pt.abi.Uint64, + _i: pt.abi.Uint64, + _j: pt.abi.Uint64, + _k: pt.abi.Uint64, + _l: pt.abi.Uint64, + _m: pt.abi.Uint64, + _n: pt.abi.Uint64, + _o: pt.abi.Uint64, + _p: pt.abi.Uint64, + *, + output: pt.abi.Uint64, + ): + return output.set( + _a.get() + + _b.get() + + _c.get() + + _d.get() + + _e.get() + + _f.get() + + _g.get() + + _h.get() + + _i.get() + + _j.get() + + _k.get() + + _l.get() + + _m.get() + + _n.get() + + _o.get() + + _p.get() + ) + + meth = router.add_method_handler(all_laid_to_args) + assert ( + meth.method_signature() + == "all_laid_to_args(uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64)uint64" + ) + + @pt.ABIReturnSubroutine + def empty_return_subroutine() -> pt.Expr: + return pt.Log(pt.Bytes("appear in both approval and clear state")) + + meth = router.add_method_handler( + empty_return_subroutine, + method_config=pt.MethodConfig( + no_op=pt.CallConfig.CALL, + opt_in=pt.CallConfig.ALL, + clear_state=pt.CallConfig.CALL, + ), + ) + assert meth.method_signature() == "empty_return_subroutine()void" + + @pt.ABIReturnSubroutine + def log_1(*, output: pt.abi.Uint64) -> pt.Expr: + return output.set(1) + + meth = router.add_method_handler( + log_1, + method_config=pt.MethodConfig( + no_op=pt.CallConfig.CALL, + opt_in=pt.CallConfig.CALL, + clear_state=pt.CallConfig.CALL, + ), + ) + + assert meth.method_signature() == "log_1()uint64" + + @pt.ABIReturnSubroutine + def log_creation(*, output: pt.abi.String) -> pt.Expr: + return output.set("logging creation") + + meth = router.add_method_handler( + log_creation, method_config=pt.MethodConfig(no_op=pt.CallConfig.CREATE) + ) + assert meth.method_signature() == "log_creation()string" + + @pt.ABIReturnSubroutine + def approve_if_odd(condition_encoding: pt.abi.Uint32) -> pt.Expr: + return ( + pt.If(condition_encoding.get() % pt.Int(2)) + .Then(pt.Approve()) + .Else(pt.Reject()) + ) + + meth = router.add_method_handler( + approve_if_odd, + method_config=pt.MethodConfig( + no_op=pt.CallConfig.NEVER, clear_state=pt.CallConfig.CALL + ), + ) + assert meth.method_signature() == "approve_if_odd(uint32)void" + + on_completion_actions = pt.BareCallActions( + opt_in=pt.OnCompleteAction.call_only(pt.Log(pt.Bytes("optin call"))), + clear_state=pt.OnCompleteAction.call_only(pt.Approve()), + ) + + _router_with_oc = pt.Router( + "ASimpleQuestionablyRobustContract", on_completion_actions + ) + add_methods_to_router(_router_with_oc) + ( + actual_ap_with_oc_compiled, + actual_csp_with_oc_compiled, + _, + ) = _router_with_oc.compile_program(version=6) + + expected_ap_with_oc = """#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l20 +txna ApplicationArgs 0 +method "add(uint64,uint64)uint64" +== +bnz main_l19 +txna ApplicationArgs 0 +method "sub(uint64,uint64)uint64" +== +bnz main_l18 +txna ApplicationArgs 0 +method "mul(uint64,uint64)uint64" +== +bnz main_l17 +txna ApplicationArgs 0 +method "div(uint64,uint64)uint64" +== +bnz main_l16 +txna ApplicationArgs 0 +method "mod(uint64,uint64)uint64" +== +bnz main_l15 +txna ApplicationArgs 0 +method "all_laid_to_args(uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64)uint64" +== +bnz main_l14 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l13 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l12 +txna ApplicationArgs 0 +method "log_creation()string" +== +bnz main_l11 +err +main_l11: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +== +&& +assert +callsub logcreation_8 +store 67 +byte 0x151f7c75 +load 67 +concat +log +int 1 +return +main_l12: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 +!= +&& +|| +assert +callsub log1_7 +store 65 +byte 0x151f7c75 +load 65 +itob +concat +log +int 1 +return +main_l13: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +|| +assert +callsub emptyreturnsubroutine_6 +int 1 +return +main_l14: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 30 +txna ApplicationArgs 2 +btoi +store 31 +txna ApplicationArgs 3 +btoi +store 32 +txna ApplicationArgs 4 +btoi +store 33 +txna ApplicationArgs 5 +btoi +store 34 +txna ApplicationArgs 6 +btoi +store 35 +txna ApplicationArgs 7 +btoi +store 36 +txna ApplicationArgs 8 +btoi +store 37 +txna ApplicationArgs 9 +btoi +store 38 +txna ApplicationArgs 10 +btoi +store 39 +txna ApplicationArgs 11 +btoi +store 40 +txna ApplicationArgs 12 +btoi +store 41 +txna ApplicationArgs 13 +btoi +store 42 +txna ApplicationArgs 14 +btoi +store 43 +txna ApplicationArgs 15 +store 46 +load 46 +int 0 +extract_uint64 +store 44 +load 46 +int 8 +extract_uint64 +store 45 +load 30 +load 31 +load 32 +load 33 +load 34 +load 35 +load 36 +load 37 +load 38 +load 39 +load 40 +load 41 +load 42 +load 43 +load 44 +load 45 +callsub alllaidtoargs_5 +store 47 +byte 0x151f7c75 +load 47 +itob +concat +log +int 1 +return +main_l15: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 24 +txna ApplicationArgs 2 +btoi +store 25 +load 24 +load 25 +callsub mod_4 +store 26 +byte 0x151f7c75 +load 26 +itob +concat +log +int 1 +return +main_l16: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 18 +txna ApplicationArgs 2 +btoi +store 19 +load 18 +load 19 +callsub div_3 +store 20 +byte 0x151f7c75 +load 20 +itob +concat +log +int 1 +return +main_l17: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 12 +txna ApplicationArgs 2 +btoi +store 13 +load 12 +load 13 +callsub mul_2 +store 14 +byte 0x151f7c75 +load 14 +itob +concat +log +int 1 +return +main_l18: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 6 +txna ApplicationArgs 2 +btoi +store 7 +load 6 +load 7 +callsub sub_1 +store 8 +byte 0x151f7c75 +load 8 +itob +concat +log +int 1 +return +main_l19: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 0 +txna ApplicationArgs 2 +btoi +store 1 +load 0 +load 1 +callsub add_0 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return +main_l20: +txn OnCompletion +int OptIn +== +bnz main_l22 +err +main_l22: +txn ApplicationID +int 0 +!= +assert +byte "optin call" +log +int 1 +return + +// add +add_0: +store 4 +store 3 +load 3 +load 4 ++ +store 5 +load 5 +retsub + +// sub +sub_1: +store 10 +store 9 +load 9 +load 10 +- +store 11 +load 11 +retsub + +// mul +mul_2: +store 16 +store 15 +load 15 +load 16 +* +store 17 +load 17 +retsub + +// div +div_3: +store 22 +store 21 +load 21 +load 22 +/ +store 23 +load 23 +retsub + +// mod +mod_4: +store 28 +store 27 +load 27 +load 28 +% +store 29 +load 29 +retsub + +// all_laid_to_args +alllaidtoargs_5: +store 63 +store 62 +store 61 +store 60 +store 59 +store 58 +store 57 +store 56 +store 55 +store 54 +store 53 +store 52 +store 51 +store 50 +store 49 +store 48 +load 48 +load 49 ++ +load 50 ++ +load 51 ++ +load 52 ++ +load 53 ++ +load 54 ++ +load 55 ++ +load 56 ++ +load 57 ++ +load 58 ++ +load 59 ++ +load 60 ++ +load 61 ++ +load 62 ++ +load 63 ++ +store 64 +load 64 +retsub + +// empty_return_subroutine +emptyreturnsubroutine_6: +byte "appear in both approval and clear state" +log +retsub + +// log_1 +log1_7: +int 1 +store 66 +load 66 +retsub + +// log_creation +logcreation_8: +byte "logging creation" +len +itob +extract 6 0 +byte "logging creation" +concat +store 68 +load 68 +retsub""".strip() + + assert expected_ap_with_oc == actual_ap_with_oc_compiled + + expected_csp_with_oc = """#pragma version 6 +txn NumAppArgs +int 0 +== +bnz main_l8 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l7 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l6 +txna ApplicationArgs 0 +method "approve_if_odd(uint32)void" +== +bnz main_l5 +err +main_l5: +txna ApplicationArgs 1 +int 0 +extract_uint32 +store 2 +load 2 +callsub approveifodd_2 +int 1 +return +main_l6: +callsub log1_1 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return +main_l7: +callsub emptyreturnsubroutine_0 +int 1 +return +main_l8: +int 1 +return + +// empty_return_subroutine +emptyreturnsubroutine_0: +byte "appear in both approval and clear state" +log +retsub + +// log_1 +log1_1: +int 1 +store 0 +load 0 +retsub + +// approve_if_odd +approveifodd_2: +store 3 +load 3 +int 2 +% +bnz approveifodd_2_l2 +int 0 +return +approveifodd_2_l2: +int 1 +return""".strip() + assert expected_csp_with_oc == actual_csp_with_oc_compiled + + _router_without_oc = pt.Router("yetAnotherContractConstructedFromRouter") + add_methods_to_router(_router_without_oc) + ( + actual_ap_without_oc_compiled, + actual_csp_without_oc_compiled, + _, + ) = _router_without_oc.compile_program(version=6) + expected_ap_without_oc = """#pragma version 6 +txna ApplicationArgs 0 +method "add(uint64,uint64)uint64" +== +bnz main_l18 +txna ApplicationArgs 0 +method "sub(uint64,uint64)uint64" +== +bnz main_l17 +txna ApplicationArgs 0 +method "mul(uint64,uint64)uint64" +== +bnz main_l16 +txna ApplicationArgs 0 +method "div(uint64,uint64)uint64" +== +bnz main_l15 +txna ApplicationArgs 0 +method "mod(uint64,uint64)uint64" +== +bnz main_l14 +txna ApplicationArgs 0 +method "all_laid_to_args(uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64,uint64)uint64" +== +bnz main_l13 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l12 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l11 +txna ApplicationArgs 0 +method "log_creation()string" +== +bnz main_l10 +err +main_l10: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +== +&& +assert +callsub logcreation_8 +store 67 +byte 0x151f7c75 +load 67 +concat +log +int 1 +return +main_l11: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +txn ApplicationID +int 0 +!= +&& +|| +assert +callsub log1_7 +store 65 +byte 0x151f7c75 +load 65 +itob +concat +log +int 1 +return +main_l12: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +txn OnCompletion +int OptIn +== +|| +assert +callsub emptyreturnsubroutine_6 +int 1 +return +main_l13: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 30 +txna ApplicationArgs 2 +btoi +store 31 +txna ApplicationArgs 3 +btoi +store 32 +txna ApplicationArgs 4 +btoi +store 33 +txna ApplicationArgs 5 +btoi +store 34 +txna ApplicationArgs 6 +btoi +store 35 +txna ApplicationArgs 7 +btoi +store 36 +txna ApplicationArgs 8 +btoi +store 37 +txna ApplicationArgs 9 +btoi +store 38 +txna ApplicationArgs 10 +btoi +store 39 +txna ApplicationArgs 11 +btoi +store 40 +txna ApplicationArgs 12 +btoi +store 41 +txna ApplicationArgs 13 +btoi +store 42 +txna ApplicationArgs 14 +btoi +store 43 +txna ApplicationArgs 15 +store 46 +load 46 +int 0 +extract_uint64 +store 44 +load 46 +int 8 +extract_uint64 +store 45 +load 30 +load 31 +load 32 +load 33 +load 34 +load 35 +load 36 +load 37 +load 38 +load 39 +load 40 +load 41 +load 42 +load 43 +load 44 +load 45 +callsub alllaidtoargs_5 +store 47 +byte 0x151f7c75 +load 47 +itob +concat +log +int 1 +return +main_l14: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 24 +txna ApplicationArgs 2 +btoi +store 25 +load 24 +load 25 +callsub mod_4 +store 26 +byte 0x151f7c75 +load 26 +itob +concat +log +int 1 +return +main_l15: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 18 +txna ApplicationArgs 2 +btoi +store 19 +load 18 +load 19 +callsub div_3 +store 20 +byte 0x151f7c75 +load 20 +itob +concat +log +int 1 +return +main_l16: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 12 +txna ApplicationArgs 2 +btoi +store 13 +load 12 +load 13 +callsub mul_2 +store 14 +byte 0x151f7c75 +load 14 +itob +concat +log +int 1 +return +main_l17: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 6 +txna ApplicationArgs 2 +btoi +store 7 +load 6 +load 7 +callsub sub_1 +store 8 +byte 0x151f7c75 +load 8 +itob +concat +log +int 1 +return +main_l18: +txn OnCompletion +int NoOp +== +txn ApplicationID +int 0 +!= +&& +assert +txna ApplicationArgs 1 +btoi +store 0 +txna ApplicationArgs 2 +btoi +store 1 +load 0 +load 1 +callsub add_0 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return + +// add +add_0: +store 4 +store 3 +load 3 +load 4 ++ +store 5 +load 5 +retsub + +// sub +sub_1: +store 10 +store 9 +load 9 +load 10 +- +store 11 +load 11 +retsub + +// mul +mul_2: +store 16 +store 15 +load 15 +load 16 +* +store 17 +load 17 +retsub + +// div +div_3: +store 22 +store 21 +load 21 +load 22 +/ +store 23 +load 23 +retsub + +// mod +mod_4: +store 28 +store 27 +load 27 +load 28 +% +store 29 +load 29 +retsub + +// all_laid_to_args +alllaidtoargs_5: +store 63 +store 62 +store 61 +store 60 +store 59 +store 58 +store 57 +store 56 +store 55 +store 54 +store 53 +store 52 +store 51 +store 50 +store 49 +store 48 +load 48 +load 49 ++ +load 50 ++ +load 51 ++ +load 52 ++ +load 53 ++ +load 54 ++ +load 55 ++ +load 56 ++ +load 57 ++ +load 58 ++ +load 59 ++ +load 60 ++ +load 61 ++ +load 62 ++ +load 63 ++ +store 64 +load 64 +retsub + +// empty_return_subroutine +emptyreturnsubroutine_6: +byte "appear in both approval and clear state" +log +retsub + +// log_1 +log1_7: +int 1 +store 66 +load 66 +retsub + +// log_creation +logcreation_8: +byte "logging creation" +len +itob +extract 6 0 +byte "logging creation" +concat +store 68 +load 68 +retsub""".strip() + assert actual_ap_without_oc_compiled == expected_ap_without_oc + + expected_csp_without_oc = """#pragma version 6 +txna ApplicationArgs 0 +method "empty_return_subroutine()void" +== +bnz main_l6 +txna ApplicationArgs 0 +method "log_1()uint64" +== +bnz main_l5 +txna ApplicationArgs 0 +method "approve_if_odd(uint32)void" +== +bnz main_l4 +err +main_l4: +txna ApplicationArgs 1 +int 0 +extract_uint32 +store 2 +load 2 +callsub approveifodd_2 +int 1 +return +main_l5: +callsub log1_1 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return +main_l6: +callsub emptyreturnsubroutine_0 +int 1 +return + +// empty_return_subroutine +emptyreturnsubroutine_0: +byte "appear in both approval and clear state" +log +retsub + +// log_1 +log1_1: +int 1 +store 0 +load 0 +retsub + +// approve_if_odd +approveifodd_2: +store 3 +load 3 +int 2 +% +bnz approveifodd_2_l2 +int 0 +return +approveifodd_2_l2: +int 1 +return""".strip() + assert actual_csp_without_oc_compiled == expected_csp_without_oc diff --git a/pyteal/compiler/constants.py b/pyteal/compiler/constants.py index b47215588..415b7d19e 100644 --- a/pyteal/compiler/constants.py +++ b/pyteal/compiler/constants.py @@ -104,7 +104,7 @@ def extractMethodSigValue(op: TealOp) -> bytes: methodSignature = methodSignature[1:-1] else: raise TealInternalError( - "Method signature opcode error: signatue {} not wrapped with double-quotes".format( + "Method signature opcode error: signature {} not wrapped with double-quotes".format( methodSignature ) ) diff --git a/pyteal/compiler/subroutines.py b/pyteal/compiler/subroutines.py index 6335b82b3..8ea3f4ddb 100644 --- a/pyteal/compiler/subroutines.py +++ b/pyteal/compiler/subroutines.py @@ -167,7 +167,7 @@ def spillLocalSlotsDuringRecursion( # reentrySubroutineCalls should have a length of 1, since calledSubroutines has a # maximum length of 1 reentrySubroutineCall = reentrySubroutineCalls[0] - numArgs = reentrySubroutineCall.argumentCount() + numArgs = reentrySubroutineCall.argument_count() digArgs = True coverSpilledSlots = False @@ -214,7 +214,7 @@ def spillLocalSlotsDuringRecursion( hideReturnValueInFirstSlot = False - if subroutine.returnType != TealType.none: + if subroutine.return_type != TealType.none: # if the subroutine returns a value on the stack, we need to preserve this after # restoring all local slots. @@ -245,7 +245,7 @@ def spillLocalSlotsDuringRecursion( # clear out the duplicate arguments that were dug up previously, since dig # does not pop the dug values -- once we use cover/uncover to properly set up # the spilled slots, this will no longer be necessary - if subroutine.returnType != TealType.none: + if subroutine.return_type != TealType.none: # if there is a return value on top of the stack, we need to preserve # it, so swap it with the subroutine argument that's below it on the # stack diff --git a/pyteal/config.py b/pyteal/config.py index 54b9aee80..e32fda458 100644 --- a/pyteal/config.py +++ b/pyteal/config.py @@ -1,5 +1,14 @@ +from algosdk.atomic_transaction_composer import ABI_RETURN_HASH + + # Maximum size of an atomic transaction group. MAX_GROUP_SIZE = 16 # Number of scratch space slots available. NUM_SLOTS = 256 + +# Method return selector in base16 +RETURN_HASH_PREFIX = ABI_RETURN_HASH + +# Method argument number limit +METHOD_ARG_NUM_CUTOFF = 15 diff --git a/pyteal/errors.py b/pyteal/errors.py index 03e597e2f..c1379b565 100644 --- a/pyteal/errors.py +++ b/pyteal/errors.py @@ -64,6 +64,17 @@ def __eq__(self, other) -> bool: TealCompileError.__module__ = "pyteal" +class TealPragmaError(Exception): + def __init__(self, message: str) -> None: + self.message = message + + def __str__(self): + return self.message + + +TealPragmaError.__module__ = "pyteal" + + def verifyTealVersion(minVersion: int, version: int, msg: str): if minVersion > version: msg = "{}. Minimum version needed is {}, but current version being compiled is {}".format( diff --git a/pyteal/ir/tealblock.py b/pyteal/ir/tealblock.py index fb6cf7a35..7ce6259bc 100644 --- a/pyteal/ir/tealblock.py +++ b/pyteal/ir/tealblock.py @@ -1,5 +1,6 @@ from abc import ABC, abstractmethod -from typing import List, Tuple, Set, Iterator, cast, TYPE_CHECKING + +from typing import Dict, List, Tuple, Set, Iterator, cast, TYPE_CHECKING from pyteal.ir.tealop import TealOp, Op from pyteal.errors import TealCompileError @@ -163,7 +164,7 @@ def FromOp( @classmethod def Iterate(cls, start: "TealBlock") -> Iterator["TealBlock"]: - """Perform a depth-first search of the graph of blocks starting with start.""" + """Perform a breadth-first search of the graph of blocks starting with start.""" queue = [start] visited = list(queue) @@ -228,5 +229,63 @@ def NormalizeBlocks(cls, start: "TealBlock") -> "TealBlock": return start + @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): + for op in block.ops: + slots += op.getSlots() + + return slots + + @classmethod + 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 + + commonSlots = set(actual) & set(expected) + mapFromActualToExpected: Dict[ScratchSlot, ScratchSlot] = { + slot: slot for slot in commonSlots + } + + for actualSlot, expectedSlot in zip(actual, expected): + if actualSlot not in mapFromActualToExpected: + if expectedSlot in mapFromActualToExpected.values(): + # this value was already seen + return False + mapFromActualToExpected[actualSlot] = expectedSlot + continue + + if mapFromActualToExpected[actualSlot] != expectedSlot: + return False + + return True + TealBlock.__module__ = "pyteal" diff --git a/pyteal/ir/tealblock_test.py b/pyteal/ir/tealblock_test.py index b301a269b..cd6bb1e29 100644 --- a/pyteal/ir/tealblock_test.py +++ b/pyteal/ir/tealblock_test.py @@ -1,3 +1,5 @@ +from typing import NamedTuple, List + import pyteal as pt options = pt.CompileOptions() @@ -245,3 +247,108 @@ def test_normalize_branch_converge(): actual.validateTree() assert actual == expected + + +def test_GetReferencedScratchSlots(): + a = pt.ScratchSlot() + b = pt.ScratchSlot() + c = pt.ScratchSlot() + d = pt.ScratchSlot() + + end = pt.TealSimpleBlock([pt.TealOp(None, pt.Op.load, d)]) + trueBranch = pt.TealSimpleBlock([pt.TealOp(None, pt.Op.load, b)]) + trueBranch.setNextBlock(end) + falseBranch = pt.TealSimpleBlock([pt.TealOp(None, pt.Op.load, c)]) + falseBranch.setNextBlock(end) + splitBranch = pt.TealConditionalBlock([pt.TealOp(None, pt.Op.load, a)]) + splitBranch.setTrueBlock(trueBranch) + splitBranch.setFalseBlock(falseBranch) + + slotReferences = pt.TealBlock.GetReferencedScratchSlots(splitBranch) + assert slotReferences == [a, b, c, d] + + +def test_MatchScratchSlotReferences(): + class MatchSlotReferenceTest(NamedTuple): + actual: List[pt.ScratchSlot] + expected: List[pt.ScratchSlot] + match: bool + + a = pt.ScratchSlot() + b = pt.ScratchSlot() + c = pt.ScratchSlot() + d = pt.ScratchSlot() + + tests: List[MatchSlotReferenceTest] = [ + MatchSlotReferenceTest( + actual=[], + expected=[], + match=True, + ), + MatchSlotReferenceTest( + actual=[a], + expected=[], + match=False, + ), + MatchSlotReferenceTest( + actual=[a], + expected=[a], + match=True, + ), + MatchSlotReferenceTest( + actual=[a], + expected=[b], + match=True, + ), + MatchSlotReferenceTest( + actual=[a, a], + expected=[a, a], + match=True, + ), + MatchSlotReferenceTest( + actual=[a, a], + expected=[b, b], + match=True, + ), + MatchSlotReferenceTest( + actual=[a, b], + expected=[a, b], + match=True, + ), + MatchSlotReferenceTest( + actual=[a, b], + expected=[b, c], + match=False, + ), + MatchSlotReferenceTest( + actual=[a, b], + expected=[c, d], + match=True, + ), + MatchSlotReferenceTest( + actual=[a, b, b, a, b], + expected=[c, d, d, c, d], + match=True, + ), + MatchSlotReferenceTest( + actual=[a, b, b, a, b], + expected=[a, d, d, a, d], + match=True, + ), + MatchSlotReferenceTest( + actual=[a, b, b, a, b], + expected=[c, a, a, c, a], + match=False, + ), + MatchSlotReferenceTest( + actual=[a, b, b, a, b], + expected=[c, d, d, c, c], + match=False, + ), + ] + + for i, test in enumerate(tests): + assert ( + pt.TealBlock.MatchScratchSlotReferences(test.actual, test.expected) + == test.match + ), "Test at index {} failed".format(i) diff --git a/pyteal/ir/tealcomponent.py b/pyteal/ir/tealcomponent.py index d1b010f98..0dc69cd9e 100644 --- a/pyteal/ir/tealcomponent.py +++ b/pyteal/ir/tealcomponent.py @@ -40,20 +40,50 @@ def __eq__(self, other: object) -> bool: class Context: - checkExpr = True + checkExprEquality = True - class EqualityContext(AbstractContextManager): + class ExprEqualityContext(AbstractContextManager): def __enter__(self): - TealComponent.Context.checkExpr = False + TealComponent.Context.checkExprEquality = False return self def __exit__(self, *args): - TealComponent.Context.checkExpr = True + TealComponent.Context.checkExprEquality = True return None @classmethod def ignoreExprEquality(cls): - return cls.EqualityContext() + return cls.ExprEqualityContext() + + checkScratchSlotEquality = True + + class ScratchSlotEqualityContext(AbstractContextManager): + def __enter__(self): + TealComponent.Context.checkScratchSlotEquality = False + + def __exit__(self, *args): + TealComponent.Context.checkScratchSlotEquality = True + return None + + @classmethod + def ignoreScratchSlotEquality(cls): + """When comparing TealOps, do not verify the equality of any ScratchSlot arguments. + + This is commonly used in testing to verify the that two control flow graphs contains the + same operations, but may use different ScratchSlots in them. In this case, you will most + likely want to also use use the following code after comparing with this option enabled: + + .. code-block:: python + + TealBlock.MatchScratchSlotReferences( + TealBlock.GetReferencedScratchSlots(actual), + TealBlock.GetReferencedScratchSlots(expected), + ) + + This ensures that the ScratchSlot usages between the two control flow graphs is + equivalent. See :any:`TealBlock.MatchScratchSlotReferences` for more info. + """ + return cls.ScratchSlotEqualityContext() TealComponent.__module__ = "pyteal" diff --git a/pyteal/ir/teallabel.py b/pyteal/ir/teallabel.py index 1b7a1b793..6c6cea98e 100644 --- a/pyteal/ir/teallabel.py +++ b/pyteal/ir/teallabel.py @@ -33,7 +33,7 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: if not isinstance(other, TealLabel): return False - if TealComponent.Context.checkExpr and self.expr is not other.expr: + if TealComponent.Context.checkExprEquality and self.expr is not other.expr: return False return self.label == other.label and self.comment == other.comment diff --git a/pyteal/ir/tealop.py b/pyteal/ir/tealop.py index 9f509677b..3f4e8a2a8 100644 --- a/pyteal/ir/tealop.py +++ b/pyteal/ir/tealop.py @@ -68,7 +68,7 @@ def __repr__(self) -> str: for a in self.args: args.append(repr(a)) - return "TealOp({}, {})".format(self.expr, ", ".join(args)) + return "TealOp({})".format(", ".join(args)) def __hash__(self) -> int: return (self.op, *self.args).__hash__() @@ -76,9 +76,24 @@ def __hash__(self) -> int: def __eq__(self, other: object) -> bool: if not isinstance(other, TealOp): return False - if TealComponent.Context.checkExpr and self.expr is not other.expr: + + if TealComponent.Context.checkExprEquality and self.expr is not other.expr: + return False + + if not TealComponent.Context.checkScratchSlotEquality: + from pyteal import ScratchSlot + + if len(self.args) != len(other.args): + return False + for myArg, otherArg in zip(self.args, other.args): + if type(myArg) is ScratchSlot and type(otherArg) is ScratchSlot: + continue + if myArg != otherArg: + return False + elif self.args != other.args: return False - return self.op == other.op and self.args == other.args + + return self.op == other.op TealOp.__module__ = "pyteal" diff --git a/pyteal/pragma/__init__.py b/pyteal/pragma/__init__.py new file mode 100644 index 000000000..12b4ce039 --- /dev/null +++ b/pyteal/pragma/__init__.py @@ -0,0 +1,6 @@ +from pyteal.pragma.pragma import is_valid_compiler_version, pragma + +__all__ = [ + "is_valid_compiler_version", + "pragma", +] diff --git a/pyteal/pragma/pragma.py b/pyteal/pragma/pragma.py new file mode 100644 index 000000000..745b28e28 --- /dev/null +++ b/pyteal/pragma/pragma.py @@ -0,0 +1,128 @@ +import re +import pkg_resources +from typing import Any +from semantic_version import Version, NpmSpec + +from pyteal.errors import TealPragmaError + + +def __convert_pep440_compiler_version(compiler_version: str): + """Convert PEP 440 version identifiers to valid NPM versions. + + For example: + "1.0.0" -> "1.0.0" + "1.0.0a1" -> "1.0.0-a1" + "<0.5.0+local || >=1.0.0a9.post1.dev2" -> "<0.5.0 || >=1.0.0-alpha9.1.2" + """ + NUMBER = r"(?:x|X|\*|0|[1-9][0-9]*)" + LOCAL = r"[a-zA-Z0-9.]*" + TRIM_PREFIX_RE = re.compile( + r""" + (?:v)? # Strip optional initial v + (?P<|<=|>=|>|=|\^|~|) # Operator, can be empty + (?P{nb})(?:\.(?P{nb})(?:\.(?P{nb}))?)? + (?:(?Pa|b|rc)(?P{nb}))? # Optional pre-release + (?:\.post(?P{nb}))? # Optional post-release + (?:\.dev(?P{nb}))? # Optional dev release + (?:\+(?P{lcl}))? # Optional local version + """.format( + nb=NUMBER, + lcl=LOCAL, + ), + re.VERBOSE, + ) + + def match_replacer(match: re.Match): + ( + op, + major, + minor, + patch, + prerel_type, + prerel, + postrel, + dev, + local, + ) = match.groups() + + # Base version (major/minor/patch) + base_version = "{}.{}.{}".format(major or "0", minor or "0", patch or "0") + + # Combine prerel, postrel, and dev + combined_additions = [] + short_prerel_type_to_long = { + "a": "alpha", + "b": "beta", + "rc": "rc", + } + if prerel_type is not None: + combined_additions.append(short_prerel_type_to_long[prerel_type] + prerel) + if len(combined_additions) > 0 or postrel is not None or dev is not None: + combined_additions.append(postrel or "0") + if len(combined_additions) > 0 or dev is not None: + combined_additions.append(dev or "0") + combined_additions_str = ".".join(combined_additions) + + # Build full_version + full_version = base_version + if len(combined_additions) > 0: + full_version += "-" + combined_additions_str + if local is not None: + full_version += "+" + local.lower() + + if op is not None: + return op + full_version + return full_version + + return re.sub(TRIM_PREFIX_RE, match_replacer, compiler_version) + + +def is_valid_compiler_version(compiler_version: str): + """Check if the compiler version is valid. + + Args: + compiler_version: The compiler version to check. + + Returns: + True if the compiler version is a valid NPM specification range + using either the PEP 440 or semantic version format, otherwise False. + """ + try: + pep440_converted = __convert_pep440_compiler_version(compiler_version) + NpmSpec(pep440_converted) + return True + except ValueError: + return False + + +def pragma( + *, + compiler_version: str, + **kwargs: Any, +) -> None: + """ + Specify pragmas for the compiler. + + Args: + compiler_version: Acceptable versions of the compiler. Will fail if the current PyTeal version + is not contained in the range. Follows the npm `semver range scheme `_ + for specifying compatible versions. + + For example: + + .. code-block:: python + + # this will immediately fail if the current PyTeal version does not satisfy the + # version constraint + pragma(compiler_version="^0.14.0") + """ + pkg_version = pkg_resources.require("pyteal")[0].version + pyteal_version = Version(__convert_pep440_compiler_version(pkg_version)) + if pyteal_version not in NpmSpec( + __convert_pep440_compiler_version(compiler_version) + ): + raise TealPragmaError( + "PyTeal version {} is not compatible with compiler version {}".format( + pkg_version, compiler_version + ) + ) diff --git a/pyteal/pragma/pragma_test.py b/pyteal/pragma/pragma_test.py new file mode 100644 index 000000000..65b4a399b --- /dev/null +++ b/pyteal/pragma/pragma_test.py @@ -0,0 +1,84 @@ +import pytest +import pkg_resources +from tests.mock_version import ( # noqa: F401 + mock_version, +) + +import pyteal as pt +from pyteal.pragma.pragma import __convert_pep440_compiler_version + + +@pytest.mark.parametrize( + "compiler_version,expected", + [ + ("2", "2.0.0"), + (">=0.12.0a9.post2.dev9", ">=0.12.0-alpha9.2.9"), + ("<0.5.0 || >=1.0.0a9.post1.dev2", "<0.5.0 || >=1.0.0-alpha9.1.2"), + ("v0.12.9.post1 - v1.13.0.dev1", "0.12.9-1.0 - 1.13.0-0.1"), + ( + "1.2.3a4.post5.dev6+AVM7.1", + "1.2.3-alpha4.5.6+avm7.1", + ), # local versions are lowercased to be consistent with pkg_resources + ], +) +def test_convert_pep440_compiler_version(compiler_version, expected): + assert __convert_pep440_compiler_version(compiler_version) == expected + + +@pytest.mark.usefixtures("mock_version") +@pytest.mark.parametrize( + "version, compiler_version, should_error", + [ + # valid + ("0.12.0", "0.12.0", False), + ("0.12.0", "<=0.12.0", False), + ("0.12.0", ">=0.12.0", False), + ("0.13.0", "<0.8.0 || >=0.12.0", False), + ("0.12.0", "0.12.0-rc1", False), + ("0.1.0", "<0.2.0", False), + ("1.2.3", "^1.2.3", False), + ("1.5.0", "^1.2.3", False), + ("1.2.3b9", "^1.2.3b4", False), + ("0.1.0a1", "<0.1.0a2", False), + ("0.1.0-rc1", "<0.1.0-rc2", False), + ("0.1.0.dev1", "<0.1.0.dev2", False), + ("0.1.0a9.dev2", ">0.1.0a8.dev1", False), + ("v1.1", "<0.5.0 || >=1.0.0a9.post1.dev2", False), + ("v1.0a9.post2.dev2", "<0.5.0 || >=1.0.0a9.post1.dev2", False), + ("v1.0a9.post1", "<0.5.0 || >=1.0.0a9.dev10", False), + ("0.4.0", "<0.5.0 || >=1.0.0a9.dev10", False), + ("1.2.3a4.post5.dev6+AVM7.1", "=1.2.3a4.post5.dev6+AVM7.1", False), + ( + "1.0.0+AVM7.1", + "=1.0.0", + False, + ), # Ignores local version (consistent with PEP 440) + ( + pkg_resources.require("pyteal")[0].version, + pkg_resources.require("pyteal")[0].version, + False, + ), + # invalid + ("0.13.0", "0.13.1", True), + ("1.2.3a2", "<0.8.0 || >=0.12.0", True), + ("0.1.0a1", "<0.2.0", True), + ("2.0.0", "^1.2.3", True), + ("0.4.0b10", "<0.5.0 || >=1.0.0a9.dev10", True), + ("0.4.9a10.dev2.post3", "<0.5.0 || >=1.0.0a9.post1.dev2", True), + ], +) +def test_pragma_compiler_version(version, compiler_version, should_error): + if should_error: + with pytest.raises(pt.TealPragmaError): + pt.pragma(compiler_version=compiler_version) + else: + pt.pragma(compiler_version=compiler_version) + + +@pytest.mark.parametrize( + "compiler_version", + ["not a version", ">=0.1.1,<0.3.0", "1.2.3aq"], # incorrect spec # invalid PEP 440 +) +def test_pragma_compiler_version_invalid(compiler_version): + with pytest.raises(ValueError): + pt.pragma(compiler_version=compiler_version) diff --git a/requirements.txt b/requirements.txt index 8735bd72e..2f9b271d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,9 +1,11 @@ black==22.3.0 flake8==4.0.1 flake8-tidy-imports==4.6.0 -graviton@git+https://github.com/algorand/graviton@v0.1.0 +graviton@git+https://github.com/algorand/graviton@v0.3.0 mypy==0.950 pytest==7.1.1 pytest-cov==3.0.0 pytest-timeout==2.1.0 pytest-xdist==2.5.0 +semantic-version==2.10.0 +types-setuptools==57.4.18 diff --git a/setup.py b/setup.py index fa767d1e3..708fbb320 100644 --- a/setup.py +++ b/setup.py @@ -15,7 +15,10 @@ long_description_content_type="text/markdown", url="https://github.com/algorand/pyteal", packages=setuptools.find_packages(), - install_requires=["py-algorand-sdk"], + install_requires=[ + "py-algorand-sdk>=1.9.0,<2.0.0", + "semantic-version>=2.9.0,<3.0.0", + ], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", diff --git a/tests/abi_roundtrip.py b/tests/abi_roundtrip.py new file mode 100644 index 000000000..efde3ecd3 --- /dev/null +++ b/tests/abi_roundtrip.py @@ -0,0 +1,141 @@ +from typing import Generic, TypeVar + +import pyteal as pt +from pyteal import abi + +from tests.blackbox import Blackbox, BlackboxWrapper, PyTealDryRunExecutor + +T = TypeVar("T", bound=abi.BaseType) + +DEFAULT_DYNAMIC_ARRAY_LENGTH = 3 + + +class ABIRoundtrip(Generic[T]): + def __init__( + self, + annotation_instance: abi.BaseType, + length: int | None = None, + ): + self.instance: abi.BaseType = annotation_instance + self.type_spec: abi.TypeSpec = annotation_instance.type_spec() + self.annotation: type[abi.BaseType] = self.type_spec.annotation_type() + + self.length: int | None = length + + def pytealer(self) -> PyTealDryRunExecutor: + roundtrip = self.roundtrip_factory() + return PyTealDryRunExecutor(roundtrip, pt.Mode.Application) + + def roundtrip_factory(self) -> BlackboxWrapper: + comp = self.mutator_factory() + + ann_out = abi.Tuple3[self.annotation, self.annotation, self.annotation] # type: ignore[misc,name-defined] + + @Blackbox(input_types=[None]) + @pt.ABIReturnSubroutine + def round_tripper(x: self.annotation, *, output: ann_out): # type: ignore[name-defined] + y = abi.make(self.annotation) + z = abi.make(self.annotation) + return pt.Seq(y.set(comp(x)), z.set(comp(y)), output.set(x, y, z)) # type: ignore[attr-defined] + + return round_tripper + + def mutator_factory(self) -> pt.ABIReturnSubroutine: + if isinstance(self.type_spec, abi.BoolTypeSpec): + return self.bool_comp_factory() + if isinstance(self.type_spec, abi.UintTypeSpec): + return self.numerical_comp_factory() + if isinstance(self.type_spec, abi.StringTypeSpec): + return self.string_reverse_factory() + if isinstance(self.type_spec, abi.TupleTypeSpec): + return self.tuple_comp_factory() + if isinstance(self.type_spec, abi.ArrayTypeSpec): + return self.array_comp_factory() + if isinstance(self.type_spec, abi.TransactionTypeSpec): + return self.transaction_comp_factory() + + raise ValueError(f"uh-oh!!! didn't handle type {self.instance}") + + def bool_comp_factory(self) -> pt.ABIReturnSubroutine: + @pt.ABIReturnSubroutine + def bool_comp(x: abi.Bool, *, output: abi.Bool): + return output.set(pt.Not(x.get())) + + return bool_comp + + @classmethod + def max_int(cls, bit_size): + return (1 << bit_size) - 1 + + def transaction_comp_factory(self) -> pt.ABIReturnSubroutine: + @pt.ABIReturnSubroutine + def transaction_comp(x: self.annotation, *, output: abi.Uint64): # type: ignore[name-defined] + return output.set(x.get().amount()) + + return transaction_comp + + def numerical_comp_factory(self) -> pt.ABIReturnSubroutine: + @pt.ABIReturnSubroutine + def numerical_comp(x: self.annotation, *, output: self.annotation): # type: ignore[name-defined] + max_uint = pt.Int(self.max_int(self.type_spec.bit_size())) # type: ignore[attr-defined] + return output.set(max_uint - x.get()) + + return numerical_comp + + def string_reverse_factory(self) -> pt.ABIReturnSubroutine: + """ + Assume strings are python utf-8 compliant and therefore each byte value is at most 127 + """ + if self.length is None: + self.length = DEFAULT_DYNAMIC_ARRAY_LENGTH + + char_type_spec = abi.ByteTypeSpec() + + @pt.ABIReturnSubroutine + def string_reverse(x: self.annotation, *, output: self.annotation): # type: ignore[name-defined] + insts = [char_type_spec.new_instance() for _ in range(self.length)] # type: ignore[arg-type] + setters = [inst.set(x[i]) for i, inst in enumerate(reversed(insts))] + return pt.Seq(*(setters + [output.set(insts)])) + + return string_reverse + + def tuple_comp_factory(self) -> pt.ABIReturnSubroutine: # type: ignore[name-defined] + value_type_specs: list[abi.TypeSpec] = self.type_spec.value_type_specs() # type: ignore[attr-defined] + insts = [vts.new_instance() for vts in value_type_specs] + roundtrips: list[ABIRoundtrip[T]] = [ + ABIRoundtrip(inst, length=None) for inst in insts # type: ignore[arg-type] + ] + + @pt.ABIReturnSubroutine + def tuple_complement(x: self.annotation, *, output: self.annotation): # type: ignore[name-defined] + setters = [inst.set(x[i]) for i, inst in enumerate(insts)] # type: ignore[attr-defined] + comp_funcs = [rtrip.mutator_factory() for rtrip in roundtrips] + compers = [inst.set(comp_funcs[i](inst)) for i, inst in enumerate(insts)] # type: ignore[attr-defined] + return pt.Seq(*(setters + compers + [output.set(*insts)])) + + return tuple_complement + + def array_comp_factory(self) -> pt.ABIReturnSubroutine: + """ + When the length has not been provided for a dynamic array, + default to DEFAULT_DYNAMIC_ARRAY_LENGTH + """ + if self.length is not None: + assert self.type_spec.is_length_dynamic() # type: ignore[attr-defined] + elif not self.type_spec.is_length_dynamic(): # type: ignore[attr-defined] + self.length = self.type_spec.length_static() # type: ignore[attr-defined] + else: + self.length = DEFAULT_DYNAMIC_ARRAY_LENGTH + + internal_type_spec = self.type_spec.value_type_spec() # type: ignore[attr-defined] + internal_ann_inst = internal_type_spec.new_instance() + comp_func = ABIRoundtrip(internal_ann_inst, length=None).mutator_factory() + + @pt.ABIReturnSubroutine + def array_complement(x: self.annotation, *, output: self.annotation): # type: ignore[name-defined] + insts = [internal_type_spec.new_instance() for _ in range(self.length)] # type: ignore[arg-type] + setters = [inst.set(x[i]) for i, inst in enumerate(insts)] + compers = [inst.set(comp_func(inst)) for inst in insts] + return pt.Seq(*(setters + compers + [output.set(insts)])) + + return array_complement diff --git a/tests/blackbox.py b/tests/blackbox.py index 3d09bca21..26ce77595 100644 --- a/tests/blackbox.py +++ b/tests/blackbox.py @@ -1,19 +1,27 @@ -from typing import Callable +from typing import Callable, Generic, Sequence, TypeVar, cast +from dataclasses import dataclass +import algosdk.abi from algosdk.v2client import algod from graviton import blackbox +from graviton.blackbox import DryRunInspector, DryRunExecutor + +from pyteal.ast.subroutine import OutputKwArgInfo from pyteal import ( + abi, Arg, Btoi, Bytes, + compileTeal, Expr, Int, Itob, Len, Log, Mode, + Pop, ScratchVar, Seq, SubroutineFnWrapper, @@ -21,6 +29,8 @@ Txn, ) +from pyteal.ast.subroutine import ABIReturnSubroutine + # ---- Clients ---- # @@ -41,20 +51,84 @@ def _algod_client( class BlackboxWrapper: - def __init__(self, subr: SubroutineFnWrapper, input_types: list[TealType]): + def __init__( + self, + subr: SubroutineFnWrapper | ABIReturnSubroutine, + input_types: list[TealType | None], + ): subr.subroutine._validate(input_types=input_types) - self.subroutine = subr - self.input_types = input_types + self.subroutine: SubroutineFnWrapper | ABIReturnSubroutine = subr + self.input_types: list[TealType | abi.TypeSpec | None] = self._fill(input_types) - def __call__(self, *args: Expr | ScratchVar, **kwargs) -> Expr: + def __call__(self, *args: Expr | ScratchVar, **kwargs) -> Expr | abi.ReturnedValue: return self.subroutine(*args, **kwargs) def name(self) -> str: return self.subroutine.name() + def _fill( + self, input_types: list[TealType | None] + ) -> list[TealType | abi.TypeSpec | None]: + match self.subroutine: + case SubroutineFnWrapper() | ABIReturnSubroutine(): + args = self.subroutine.subroutine.arguments() + abis = self.subroutine.subroutine.abi_args + return [(x if x else abis[args[i]]) for i, x in enumerate(input_types)] + case _: + raise AssertionError( + f"Cannot handle subroutine of type {type(self.subroutine)}" + ) + + +def Blackbox(input_types: list[TealType | None]): + """ + Decorator for transforming @Subroutine and @ABIReturnSubroutine wrapped functions + into PyTeal expressions that compile into executable Teal programs. + + input_types: list[TealType] (required) + List shadowing the input arguments of the decorated subroutine. In particular: + * the list needs to be the same length as the number of subroutine arguments + * if the subroutine argument is an ABI type, the shadowing input_type must be None + as it will be determined at compile time from the subroutine's annotation + * if the subroutine argument is an Expr or a ScratchVar, the shadowing input_type + must be a TealType of the same kind as expected in the argument + + Some _Correct_ Examples: + + @Blackbox(input_types=[TealType.bytes, TealType.uint64]) + @Subroutine(TealType.bytes) + def string_mult(x, y): + ... + + @Blackbox(input_types=[TealType.bytes, TealType.uint64]) + @Subroutine(TealType.bytes) + def string_mult(x: Expr, y: Expr): + ... + + @Blackbox(input_types=[TealType.bytes, TealType.uint64]) + @Subroutine(TealType.bytes) + def string_mult(x: ScratchVar, y: Expr): + ... + + + @Blackbox(input_types=[None, None]) + @ABIReturnSubroutine + def string_mult(x: abi.String, y: abi.Uint16): + ... + + @Blackbox(input_types=[None, TealType.uint64]) + @ABIReturnSubroutine + def string_mult(x: abi.String, y): + ... + + @Blackbox([None]) + @Subroutine(TealType.uint64) + def cubed(n: abi.Uint64): + ... + + """ -def Blackbox(input_types: list[TealType]): - def decorator_blackbox(func: SubroutineFnWrapper): + def decorator_blackbox(func: SubroutineFnWrapper | ABIReturnSubroutine): return BlackboxWrapper(func, input_types) return decorator_blackbox @@ -63,139 +137,311 @@ def decorator_blackbox(func: SubroutineFnWrapper): # ---- API ---- # -def mode_to_execution_mode(mode: Mode) -> blackbox.ExecutionMode: - if mode == Mode.Application: - return blackbox.ExecutionMode.Application - if mode == Mode.Signature: - return blackbox.ExecutionMode.Signature - - raise Exception(f"Unknown mode {mode} of type {type(mode)}") - - -def blackbox_pyteal(subr: BlackboxWrapper, mode: Mode) -> Callable[..., Expr]: - """Functor producing ready-to-compile PyTeal programs from annotated subroutines - - Args: - subr: annotated subroutine to wrap inside program. - Note: the `input_types` parameters should be supplied to @Subroutine() annotation - mode: type of program to produce: logic sig (Mode.Signature) or app (Mode.Application) - - Returns: - a function that called with no parameters -e.g. result()- - returns a PyTeal expression compiling to a ready-to-test TEAL program. - - The return type is callable in order to adhere to the API of blackbox tests. - - Generated TEAL code depends on the mode, subroutine input types, and subroutine output types. - * logic sigs: - * input received via `arg i` - * args are converted (cf. "input conversion" below) and passed to the subroutine - * subroutine output is not logged (log is not available) - * subroutine output is converted (cf "output conversion" below) - * apps: - * input received via `txna ApplicationArgs i` - * args are converted (cf. "input conversion" below) and passed to the subroutine - * subroutine output is logged after possible conversion (cf. "logging conversion") - * subroutine output is converted (cf "output conversion" below) - * input conversion: - * Empty input array: - do not read any args and call subroutine immediately - * arg of TealType.bytes and TealType.anytype: - read arg and pass to subroutine as is - * arg of TealType.uint64: - convert arg to int using Btoi() when received - * pass-by-ref ScratchVar arguments: - in addition to the above - - o store the arg (or converted arg) in a ScratchVar - o invoke the subroutine using this ScratchVar instead of the arg (or converted arg) - * output conversion: - * TealType.uint64: - provide subroutine's result to the top of the stack when exiting program - * TealType.bytes: - convert subroutine's result to the top of the stack to its length and then exit - * TealType.none or TealType.anytype: - push Int(1337) to the stack as it is either impossible (TealType.none), - or unknown at compile time (TealType.anytype) to convert to an Int - * logging conversion: - * TealType.uint64: - convert subroutine's output using Itob() and log the result - * TealType.bytes: - log the subroutine's result - * TealType.none or TealType.anytype: - log Itob(Int(1337)) as it is either impossible (TealType.none), - or unknown at compile time (TealType.anytype) how to convert to Bytes - - For illustrative examples of how to use this function please refer to the integration test file `graviton_test.py` and especially: - - * `blackbox_pyteal_example1()`: Using blackbox_pyteal() for a simple test of both an app and logic sig - * `blackbox_pyteal_example2()`: Using blackbox_pyteal() to make 400 assertions and generate a CSV report with 400 dryrun rows - * `blackbox_pyteal_example3()`: declarative Test Driven Development approach through Invariant's - """ - input_types = subr.input_types - assert ( - input_types is not None - ), "please provide input_types in your @Subroutine annotation (crucial for generating proper end-to-end testable PyTeal)" +Output = TypeVar("Output") +Lazy = Callable[[], Output] + + +@dataclass(frozen=True) +class _MatchMode(Generic[Output]): + app_case: Lazy + signature_case: Lazy - subdef = subr.subroutine.subroutine - arg_names = subdef.arguments() + def __call__(self, mode: Mode, *args, **kwargs) -> Output: + match mode: + case Mode.Application: + return self.app_case() + case Mode.Signature: + return self.signature_case() + case _: + raise Exception(f"Unknown mode {mode} of type {type(mode)}") - def arg_prep_n_call(i, p): + +def mode_to_execution_mode(mode: Mode) -> blackbox.ExecutionMode: + return _MatchMode( + app_case=lambda: blackbox.ExecutionMode.Application, + signature_case=lambda: blackbox.ExecutionMode.Signature, + )(mode) + + +class PyTealDryRunExecutor: + def __init__(self, subr: BlackboxWrapper, mode: Mode): + """ + Args: + subr: a Subroutine or ABIReturnSubroutine which has been decorated with @Blackbox. + Note: the `input_types` parameters should be supplied to the @Blackbox() decorator + cf. the Blackbox class for futher details about acceptable `input_types` + + mode: type of program to produce: logic sig (Mode.Signature) or app (Mode.Application) + """ + input_types = subr.input_types + assert ( + input_types is not None + ), "please provide input_types in your @Subroutine or @ABIReturnSubroutine annotation (this is crucial for generating proper end-to-end testable PyTeal)" + + self.subr, self.mode, self.input_types = subr, mode, input_types + match subr.subroutine: + case SubroutineFnWrapper(): + approval = self._handle_SubroutineFnWrapper() + case ABIReturnSubroutine(): + approval = self._handle_ABIReturnSubroutine() + case _: + raise AssertionError( + f"Cannot produce Blackbox pyteal for provided subroutine of type {type(subr.subroutine)}" + ) + + self._pyteal_lambda: Callable[..., Expr] = approval + + def is_abi(self) -> bool: + return isinstance(self.subr.subroutine, ABIReturnSubroutine) + + def abi_argument_types(self) -> None | list[algosdk.abi.ABIType]: + if not self.is_abi(): + return None + + def handle_arg(arg): + if isinstance(arg, abi.TypeSpec): + return abi.algosdk_from_type_spec(arg) + return None + + return [handle_arg(arg) for arg in self.input_types] + + def abi_return_type(self) -> None | algosdk.abi.ABIType: + if not self.is_abi(): + return None + + out_info = getattr(self.subr.subroutine, "output_kwarg_info") + if not out_info: + return None + + return abi.algosdk_from_type_spec(cast(OutputKwArgInfo, out_info).abi_type) + + def program(self) -> Expr: + """Get ready-to-compile PyTeal program from Subroutines and ABIReturnSubroutines + + Returns: + a PyTeal expression representing a ready-to-run TEAL program + + Generated TEAL code depends on the self.subr's type, the mode, the input types, and output type + * logic sigs: + * input received via `arg i` + * args are converted (cf. "input conversion" below) and passed to the subroutine + * subroutine output is not logged (log is not available) + * in the case of ABIReturnSubroutine: the output is encoded on to the stack an then popped off + * subroutine output is converted (cf "output conversion" below) + * apps: + * input received via `txna ApplicationArgs i` + * args are converted (cf. "input conversion" below) and passed to the subroutine + * the output is logged in the following ways: + * Subroutine: logged after possible conversion (cf. "logging conversion") + * ABIReturnSubroutine: the encoded output is concatenated to the return method selector and then logged + * subroutine output is converted (cf "output conversion" below) (Subroutine case only) + * input conversion: + * Empty input array: + do not read any args and call subroutine immediately + * Expr arg of TealType.bytes and TealType.anytype: + read arg and pass to subroutine as is + * Expr arg of TealType.uint64: + convert arg to int using Btoi() when received + * pass-by-ref ScratchVar arguments: + in addition to the above - + o store the arg (or converted arg) in a ScratchVar + o invoke the subroutine using this ScratchVar instead of the arg (or converted arg) + * ABI arguments: + in addition to the above - + o store the decoded arg into the ScratchVar of an ABI Type instance + o invoke the subroutine using this ABI Type instead of the arg + * output conversion: + * Subroutine case: + * TealType.uint64: + provide subroutine's result to the top of the stack when exiting program + * TealType.bytes: + convert subroutine's result to the top of the stack to its length and then exit + * TealType.none or TealType.anytype: + push Int(1337) to the stack as it is either impossible (TealType.none), + or unknown at compile time (TealType.anytype) to convert to an Int + * ABIReturnSubroutine case: + * when present, the output is encoded as TealType.bytes which can be decoded by the receiver using + appropriate ABI-libraries + * logging conversion: + * Subroutine case: + * TealType.uint64: + convert subroutine's output using Itob() and log the result + * TealType.bytes: + log the subroutine's result + * TealType.none or TealType.anytype: + log Itob(Int(1337)) as it is either impossible (TealType.none), + or unknown at compile time (TealType.anytype) how to convert to Bytes + * ABIReturnSubroutine case: + * when present, the output is encoded as TealType.bytes and concatenated to the rewturn + method selector. This can be decoded by the receiver using appropriate ABI-libraries + + For illustrative examples of how to use this method please refer to the integration test file `graviton_test.py` and especially: + + * `blackbox_pyteal_example1()`: Using blackbox_pyteal() for a simple test of both an app and logic sig + * `blackbox_pyteal_example2()`: Using blackbox_pyteal() to make 400 assertions and generate a CSV report with 400 dryrun rows + * `blackbox_pyteal_example3()`: declarative Test Driven Development approach through Invariant's + * `blackbox_pyteal_example4()`: Using PyTealDryRunExecutor to debug an ABIReturnSubroutine with an app, logic sig and csv reports + """ + + return self._pyteal_lambda() + + def _arg_prep_n_call(self, i, p): + subdef = self.subr.subroutine.subroutine + arg_names = subdef.arguments() name = arg_names[i] - by_ref = name in subdef.by_ref_args - arg_expr = Txn.application_args[i] if mode == Mode.Application else Arg(i) + arg_expr = Txn.application_args[i] if self.mode == Mode.Application else Arg(i) if p == TealType.uint64: arg_expr = Btoi(arg_expr) - prep = None - arg_var = arg_expr - if by_ref: + + if name in subdef.by_ref_args: arg_var = ScratchVar(p) prep = arg_var.store(arg_expr) + elif name in subdef.abi_args: + arg_var = p.new_instance() + prep = arg_var.decode(arg_expr) + else: + arg_var = arg_expr + prep = None return prep, arg_var - def subr_caller(): - preps_n_calls = [*(arg_prep_n_call(i, p) for i, p in enumerate(input_types))] + def _prepare_n_calls(self): + preps_n_calls = [ + *(self._arg_prep_n_call(i, p) for i, p in enumerate(self.input_types)) + ] preps, calls = zip(*preps_n_calls) if preps_n_calls else ([], []) preps = [p for p in preps if p] - invocation = subr(*calls) - if preps: - return Seq(*(preps + [invocation])) - return invocation - - def make_return(e): - if e.type_of() == TealType.uint64: - return e - if e.type_of() == TealType.bytes: - return Len(e) - if e.type_of() == TealType.anytype: - x = ScratchVar(TealType.anytype) - return Seq(x.store(e), Int(1337)) - # TealType.none: - return Seq(e, Int(1337)) - - def make_log(e): - if e.type_of() == TealType.uint64: - return Log(Itob(e)) - if e.type_of() == TealType.bytes: - return Log(e) - return Log(Bytes("nada")) - - if mode == Mode.Signature: - - def approval(): - return make_return(subr_caller()) - - else: + return preps, calls + + def _handle_SubroutineFnWrapper(self): + subdef = self.subr.subroutine.subroutine + + def subr_caller(): + preps, calls = self._prepare_n_calls() + invocation = self.subr(*calls) + if preps: + return Seq(*(preps + [invocation])) + return invocation + + def make_return(e): + if e.type_of() == TealType.uint64: + return e + if e.type_of() == TealType.bytes: + return Len(e) + if e.type_of() == TealType.anytype: + x = ScratchVar(TealType.anytype) + return Seq(x.store(e), Int(1337)) + # TealType.none: + return Seq(e, Int(1337)) + + def make_log(e): + if e.type_of() == TealType.uint64: + return Log(Itob(e)) + if e.type_of() == TealType.bytes: + return Log(e) + return Log(Bytes("nada")) + + if self.mode == Mode.Signature: + + def approval(): + return make_return(subr_caller()) + + else: + + def approval(): + if subdef.return_type == TealType.none: + result = ScratchVar(TealType.uint64) + part1 = [subr_caller(), result.store(Int(1337))] + else: + result = ScratchVar(subdef.return_type) + part1 = [result.store(subr_caller())] + + part2 = [make_log(result.load()), make_return(result.load())] + return Seq(*(part1 + part2)) + + return approval + + def _handle_ABIReturnSubroutine(self): + output = None + if self.subr.subroutine.output_kwarg_info: + output = self.subr.subroutine.output_kwarg_info.abi_type.new_instance() def approval(): - if subdef.returnType == TealType.none: - result = ScratchVar(TealType.uint64) - part1 = [subr_caller(), result.store(Int(1337))] + preps, calls = self._prepare_n_calls() + + # when @ABIReturnSubroutine is void: + # invocation is an Expr of TealType.none + # otherwise: + # it is a ComputedValue + invocation = self.subr(*calls) + if output: + invocation = output.set(invocation) + if self.mode == Mode.Signature: + results = [invocation, Pop(output.encode()), Int(1)] + else: + results = [invocation, abi.MethodReturn(output), Int(1)] else: - result = ScratchVar(subdef.returnType) - part1 = [result.store(subr_caller())] - - part2 = [make_log(result.load()), make_return(result.load())] - return Seq(*(part1 + part2)) - - setattr(approval, "__name__", f"sem_{mode}_{subr.name()}") - return approval + results = [invocation, Int(1)] + + return Seq(*(preps + results)) + + return approval + + def compile(self, version: int, assemble_constants: bool = False) -> str: + return _MatchMode( + app_case=lambda: compileTeal( + self.program(), + self.mode, + version=version, + assembleConstants=assemble_constants, + ), + signature_case=lambda: compileTeal( + self.program(), + self.mode, + version=version, + assembleConstants=assemble_constants, + ), + )(self.mode) + + def dryrun_on_sequence( + self, + inputs: list[Sequence[str | int]], + compiler_version=6, + ) -> list[DryRunInspector]: + return _MatchMode( + app_case=lambda: DryRunExecutor.dryrun_app_on_sequence( + algod_with_assertion(), + self.compile(compiler_version), + inputs, + self.abi_argument_types(), + self.abi_return_type(), + ), + signature_case=lambda: DryRunExecutor.dryrun_logicsig_on_sequence( + algod_with_assertion(), + self.compile(compiler_version), + inputs, + self.abi_argument_types(), + self.abi_return_type(), + ), + )(self.mode) + + def dryrun( + self, + args: Sequence[bytes | str | int], + compiler_version=6, + ) -> DryRunInspector: + return _MatchMode( + app_case=lambda: DryRunExecutor.dryrun_app( + algod_with_assertion(), + self.compile(compiler_version), + args, + self.abi_argument_types(), + self.abi_return_type(), + ), + signature_case=lambda: DryRunExecutor.dryrun_logicsig( + algod_with_assertion(), + self.compile(compiler_version), + args, + self.abi_argument_types(), + self.abi_return_type(), + ), + )(self.mode) diff --git a/tests/integration/abi_roundtrip_test.py b/tests/integration/abi_roundtrip_test.py new file mode 100644 index 000000000..a9659c1e9 --- /dev/null +++ b/tests/integration/abi_roundtrip_test.py @@ -0,0 +1,269 @@ +from pathlib import Path +import pytest +from typing import Literal + +import algosdk.abi + +from graviton.abi_strategy import ABIStrategy + +from pyteal import abi + +from tests.abi_roundtrip import ABIRoundtrip +from tests.compile_asserts import assert_teal_as_expected + +GAI_ISSUE_2050 = "https://github.com/algorand/go-algorand-internal/issues/2050" +BAD_TEALS = { + "()": GAI_ISSUE_2050, +} + + +GAI_ISSUE_2068 = "https://github.com/algorand/go-algorand-internal/issues/2068" +BAD_TYPES = { + # Reference Types + "account": GAI_ISSUE_2068, + "asset": GAI_ISSUE_2068, + "application": GAI_ISSUE_2068, + # Transaction Types + "txn": GAI_ISSUE_2068, + "pay": GAI_ISSUE_2068, + "keyreg": GAI_ISSUE_2068, + "acfg": GAI_ISSUE_2068, + "afrz": GAI_ISSUE_2068, + "axfer": GAI_ISSUE_2068, + "appl": GAI_ISSUE_2068, +} + +PATH = Path.cwd() / "tests" / "integration" +FIXTURES = PATH / "teal" +GENERATED = PATH / "generated" +ABI_TYPES = [ + abi.Address, + abi.Bool, + abi.Byte, + (abi.String, 0), + (abi.String, 1), + (abi.String, 13), + abi.Uint8, + abi.Uint16, + abi.Uint32, + abi.Uint64, + abi.Account, + abi.Asset, + abi.Application, + abi.Transaction, + abi.PaymentTransaction, + abi.KeyRegisterTransaction, + abi.AssetConfigTransaction, + abi.AssetFreezeTransaction, + abi.AssetTransferTransaction, + abi.ApplicationCallTransaction, + abi.Tuple0, + abi.Tuple1[abi.Bool], + abi.Tuple1[abi.Byte], + abi.Tuple1[abi.Uint8], + abi.Tuple1[abi.Uint16], + abi.Tuple1[abi.Uint32], + abi.Tuple1[abi.Uint64], + abi.Tuple2[abi.Bool, abi.Byte], + abi.Tuple3[abi.Bool, abi.Uint64, abi.Uint32], + abi.Tuple3[abi.Byte, abi.Bool, abi.Uint64], + abi.Tuple3[abi.Uint8, abi.Byte, abi.Bool], + abi.Tuple3[abi.Uint16, abi.Uint8, abi.Byte], + abi.Tuple3[abi.Uint32, abi.Uint16, abi.Uint8], + abi.Tuple3[abi.Uint64, abi.Uint32, abi.Uint16], + abi.Tuple4[abi.Bool, abi.Byte, abi.Address, abi.String], + abi.Tuple5[abi.Bool, abi.Byte, abi.Address, abi.String, abi.Uint64], + abi.StaticArray[abi.Bool, Literal[1]], + abi.StaticArray[abi.Bool, Literal[42]], + abi.StaticArray[abi.Uint64, Literal[1]], + abi.StaticArray[abi.Uint64, Literal[42]], + (abi.DynamicArray[abi.Bool], 0), + (abi.DynamicArray[abi.Bool], 1), + (abi.DynamicArray[abi.Bool], 42), + (abi.DynamicArray[abi.Uint64], 0), + (abi.DynamicArray[abi.Uint64], 1), + (abi.DynamicArray[abi.Uint64], 42), + (abi.DynamicArray[abi.Address], 10), + (abi.DynamicArray[abi.StaticArray[abi.Bool, Literal[3]]], 11), + abi.StaticArray[abi.Tuple1[abi.Bool], Literal[10]], + ( + abi.DynamicArray[ + abi.Tuple4[ + abi.StaticArray[abi.Byte, Literal[4]], + abi.Tuple2[abi.Bool, abi.Bool], + abi.Uint64, + abi.Address, + ] + ], + 7, + ), + ( + abi.DynamicArray[ + abi.Tuple5[ + abi.Bool, + abi.Byte, + abi.Address, + abi.String, + abi.Tuple4[ + abi.Address, + abi.StaticArray[ + abi.Tuple5[ + abi.Uint32, + abi.DynamicArray[abi.String], + abi.StaticArray[abi.Bool, Literal[2]], + abi.Tuple1[abi.Byte], + abi.Uint8, + ], + Literal[2], + ], + abi.String, + abi.DynamicArray[abi.Bool], + ], + ] + ], + 2, + ), +] + + +def roundtrip_setup(abi_type): + dynamic_length = None + if isinstance(abi_type, tuple): + abi_type, dynamic_length = abi_type + + abi_type_str = str(abi.type_spec_from_annotation(abi_type)) + roundtrip_or_none = None + if abi_type_str not in BAD_TYPES: + roundtrip_or_none = ABIRoundtrip( + abi.make(abi_type), length=dynamic_length + ).pytealer() + + return (abi_type, abi_type_str, dynamic_length, roundtrip_or_none) + + +def test_abi_types_comprehensive(): + top_level_names = { + tli.split("[")[0] if tli.startswith("pyteal") else tli.split("'")[1] + for tli in ( + str(x) for x in (at[0] if isinstance(at, tuple) else at for at in ABI_TYPES) + ) + } + + def get_subclasses(cls): + for subclass in cls.__subclasses__(): + yield from get_subclasses(subclass) + yield subclass + + all_abi_names = { + str(at).split("'")[1] + for at in ( + cls + for cls in abi.__dict__.values() + if isinstance(cls, type) + and issubclass(cls, abi.BaseType) + and not cls.__abstractmethods__ + and cls is not abi.Tuple + ) + } + + missing_cases = all_abi_names - top_level_names + assert not missing_cases, f"missing round trip tests for {missing_cases}" + + +@pytest.mark.parametrize("abi_type", ABI_TYPES) +def test_pure_compilation(abi_type): + print(f"Pure Compilation Test for {abi_type=}") + abi_type, type_str, dynamic_length, roundtripper = roundtrip_setup(abi_type) + + if type_str in BAD_TYPES: + print( + f"Skipping encoding roundtrip test of '{abi_type}' because of {BAD_TYPES[type_str]}" + ) + return + + sdk_abi_type = abi.algosdk_from_annotation(abi_type) + + abi_arg_types = roundtripper.abi_argument_types() + abi_ret_type = roundtripper.abi_return_type() + assert [sdk_abi_type] == abi_arg_types + assert algosdk.abi.TupleType([sdk_abi_type] * 3) == abi_ret_type + + teal = roundtripper.compile(version=6) + + filename = ( + f"app_roundtrip_{sdk_abi_type}" + + ("" if dynamic_length is None else f"_<{dynamic_length}>") + + ".teal" + ) + tealdir = GENERATED / "roundtrip" + tealdir.mkdir(parents=True, exist_ok=True) + + save_to = tealdir / filename + with open(save_to, "w") as f: + f.write(teal) + + assert_teal_as_expected(save_to, FIXTURES / "roundtrip" / filename) + + +@pytest.mark.parametrize("abi_type", ABI_TYPES) +def test_roundtrip(abi_type): + print(f"Round Trip Test for {abi_type=}") + + _, type_str, dynamic_length, roundtripper = roundtrip_setup(abi_type) + + if type_str in BAD_TYPES: + print( + f"Skipping encoding roundtrip test of '{abi_type}' because of {BAD_TYPES[type_str]}" + ) + return + + sdk_abi_types = roundtripper.abi_argument_types() + sdk_ret_type = roundtripper.abi_return_type() + + sdk_abi_str = str(sdk_abi_types[0]) + if sdk_abi_str in BAD_TEALS: + print( + f"Skipping encoding roundtrip test of '{sdk_abi_str}' because of {BAD_TEALS[sdk_abi_str]}" + ) + return + + abi_strat = ABIStrategy(sdk_abi_types[0], dynamic_length=dynamic_length) + rand_abi_instance = abi_strat.get_random() + args = (rand_abi_instance,) + inspector = roundtripper.dryrun(args) + + cost = inspector.cost() + passed = inspector.passed() + original, mut, mut_mut = inspector.last_log() + + print( + f""" +{abi_type=} +{sdk_abi_str=} +{dynamic_length=} +{sdk_abi_types=} +{sdk_ret_type=} +{rand_abi_instance=} +{cost=} +{original=} +{mut=} +{mut_mut=} +""" + ) + + last_steps = 2 + + assert passed == (cost <= 700), inspector.report( + args, f"passed={passed} contradicted cost={cost}", last_steps=last_steps + ) + assert rand_abi_instance == original, inspector.report( + args, "rand_abi_instance v. original", last_steps=last_steps + ) + assert original == mut_mut, inspector.report( + args, "orginal v. mut_mut", last_steps=last_steps + ) + + expected_mut = abi_strat.mutate_for_roundtrip(rand_abi_instance) + assert expected_mut == mut, inspector.report( + args, "expected_mut v. mut", last_steps=last_steps + ) diff --git a/tests/integration/ecdsa_test.py b/tests/integration/ecdsa_test.py index 9bdeaeae6..4efceccb7 100644 --- a/tests/integration/ecdsa_test.py +++ b/tests/integration/ecdsa_test.py @@ -1,5 +1,3 @@ -from graviton.blackbox import DryRunExecutor - from pyteal import ( Bytes, EcdsaCurve, @@ -10,15 +8,13 @@ And, Subroutine, Sha512_256, - compileTeal, Mode, TealType, ) from tests.blackbox import ( Blackbox, - algod_with_assertion, - blackbox_pyteal, + PyTealDryRunExecutor, ) @@ -49,11 +45,10 @@ def verify(): ), ) - approval_app = blackbox_pyteal(verify, Mode.Application) - app_teal = compileTeal(approval_app(), Mode.Application, version=5) args = [] - algod = algod_with_assertion() - app_result = DryRunExecutor.dryrun_app(algod, app_teal, args) + app_result = PyTealDryRunExecutor(verify, Mode.Application).dryrun( + args, compiler_version=5 + ) assert app_result.stack_top() == 1, app_result.report( args, "stack_top() is not equal to 1, indicating ecdsa verification failed." @@ -85,11 +80,10 @@ def verify_fail(): ), ) - approval_app = blackbox_pyteal(verify_fail, Mode.Application) - app_teal = compileTeal(approval_app(), Mode.Application, version=5) args = [] - algod = algod_with_assertion() - app_result = DryRunExecutor.dryrun_app(algod, app_teal, args) + app_result = PyTealDryRunExecutor(verify_fail, Mode.Application).dryrun( + args, compiler_version=5 + ) assert app_result.stack_top() == 0, app_result.report( args, @@ -122,11 +116,10 @@ def decompress(): ) ) - approval_app = blackbox_pyteal(decompress, Mode.Application) - app_teal = compileTeal(approval_app(), Mode.Application, version=5) args = [] - algod = algod_with_assertion() - app_result = DryRunExecutor.dryrun_app(algod, app_teal, args) + app_result = PyTealDryRunExecutor(decompress, Mode.Application).dryrun( + args, compiler_version=5 + ) assert app_result.stack_top() == 1, app_result.report( args, "stack_top() is not equal to 1, indicating ecdsa verification failed." @@ -164,11 +157,10 @@ def recover(): ) ) - approval_app = blackbox_pyteal(recover, Mode.Application) - app_teal = compileTeal(approval_app(), Mode.Application, version=5) args = [] - algod = algod_with_assertion() - app_result = DryRunExecutor.dryrun_app(algod, app_teal, args) + app_result = PyTealDryRunExecutor(recover, Mode.Application).dryrun( + args, compiler_version=5 + ) assert app_result.stack_top() == 1, app_result.report( args, "stack_top() is not equal to 1, indicating ecdsa verification failed." diff --git a/tests/integration/graviton_abi_test.py b/tests/integration/graviton_abi_test.py new file mode 100644 index 000000000..229b7a671 --- /dev/null +++ b/tests/integration/graviton_abi_test.py @@ -0,0 +1,536 @@ +import random + +from graviton.blackbox import DryRunInspector + +import pyteal as pt +from pyteal.ast.subroutine import ABIReturnSubroutine + +from tests.blackbox import ( + Blackbox, + PyTealDryRunExecutor, +) + + +# ---- Integers and Complex Integral Numbers (aka Gaussian Integers) ---- # + + +""" +WARNING: The following ABI types Int65 and Complex130 are ONLY for the purpose of testing/demo'ing +ABISubroutine and graviton ABI capabilities and are NOT the recommended approach for +implementing integers and complex integers. +A better approach would likely leverage `Uint64` (if any ABI type at all) and make use of 2's complement arithmetic. + +Cf. https://github.com/algorand/pyteal/issues/184 +""" + +Int65 = pt.abi.Tuple2[pt.abi.Bool, pt.abi.Uint64] +Complex130 = pt.abi.Tuple2[Int65, Int65] + + +@Blackbox(input_types=[None, None]) +@pt.ABIReturnSubroutine +def int65_minus_cond(x: Int65, y: Int65, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + x0 = pt.abi.Bool() + x1 = pt.abi.Uint64() + y0 = pt.abi.Bool() + y1 = pt.abi.Uint64() + z0 = pt.abi.Bool() + z1 = pt.abi.Uint64() + return pt.Seq( + x0.set(x[0]), + x1.set(x[1]), + y0.set(y[0]), + y1.set(y[1]), + pt.Cond( + # Case I. x, y positive + [ + pt.And(x0.get(), y0.get()), + pt.Seq( + z0.set(x1.get() >= y1.get()), + z1.set( + pt.If(x1.get() <= y1.get()) + .Then(y1.get() - x1.get()) + .Else(x1.get() - y1.get()) + ), + ), + ], + # Case II. x positive, y negative + [ + pt.And(x0.get(), pt.Not(y0.get())), + pt.Seq(z0.set(True), z1.set(x1.get() + y1.get())), + ], + # Case III. x negative, y positive + [ + pt.And(pt.Not(x0.get()), y0.get()), + pt.Seq(z0.set(False), z1.set(x1.get() + y1.get())), + ], + # Case IV. x, y negative + [ + pt.Int(1), + pt.Seq( + z0.set(x1.get() <= y1.get()), + z1.set( + pt.If(x1.get() <= y1.get()) + .Then(y1.get() - x1.get()) + .Else(x1.get() - y1.get()) + ), + ), + ], + ), + output.set(z0, z1), + ) + + +@Blackbox(input_types=[None, None]) +@pt.ABIReturnSubroutine +def int65_sub(x: Int65, y: Int65, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + x0 = pt.abi.Bool() + x1 = pt.abi.Uint64() + y0 = pt.abi.Bool() + y1 = pt.abi.Uint64() + z0 = pt.abi.Bool() + z1 = pt.abi.Uint64() + return pt.Seq( + x0.set(x[0]), + x1.set(x[1]), + y0.set(y[0]), + y1.set(y[1]), + pt.If(x0.get() == y0.get()) + .Then( # Case I. x, y same signature + pt.Seq( + z0.set(pt.Not(x0.get()) ^ (x1.get() >= y1.get())), + z1.set( + pt.If(x1.get() <= y1.get()) + .Then(y1.get() - x1.get()) + .Else(x1.get() - y1.get()) + ), + ) + ) + .Else( # Case II. x, y opposite signatures + pt.Seq( + z0.set(x0.get()), + z1.set(x1.get() + y1.get()), + ), + ), + output.set(z0, z1), + ) + + +@Blackbox(input_types=[None, None]) +@pt.ABIReturnSubroutine +def int65_mult(x: Int65, y: Int65, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + # TODO: can we get something like the following one-liner working? + # return output.set(pt.Not(x[0].get() ^ y[0].get()), x[1].get() * y[1].get()) + def get(x): + return x.use(lambda ni: ni.get()) + + return pt.Seq( + (z0 := pt.abi.Bool()).set(pt.Not(get(x[0]) ^ get(y[0]))), + (z1 := pt.abi.Uint64()).set(get(x[1]) * get(y[1])), + output.set(z0, z1), + ) + + +@Blackbox(input_types=[None]) +@ABIReturnSubroutine +def int65_negate(x: Int65, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + # TODO: can I haz a one-liner pls???? + x0 = pt.abi.Bool() + x1 = pt.abi.Uint64() + z0 = pt.abi.Bool() + z1 = pt.abi.Uint64() + return pt.Seq( + x0.set(x[0]), + x1.set(x[1]), + z0.set(pt.Not(x0.get())), + z1.set(x1.get()), + output.set(z0, z1), + ) + + +@Blackbox(input_types=[None, None]) +@ABIReturnSubroutine +def int65_add(x: Int65, y: Int65, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + return pt.Seq(y.set(int65_negate(y)), output.set(int65_sub(x, y))) + + +@Blackbox(input_types=[None, None]) +@ABIReturnSubroutine +def complex130_add(x: Complex130, y: Complex130, *, output: Complex130): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + x0 = pt.abi.make(Int65) + x1 = pt.abi.make(Int65) + y0 = pt.abi.make(Int65) + y1 = pt.abi.make(Int65) + z0 = pt.abi.make(Int65) + z1 = pt.abi.make(Int65) + return pt.Seq( + x0.set(x[0]), + x1.set(x[1]), + y0.set(y[0]), + y1.set(y[1]), + z0.set(int65_add(x0, y0)), + z1.set(int65_add(x1, y1)), + output.set(z0, z1), + ) + + +@Blackbox(input_types=[None, None]) +@ABIReturnSubroutine +def complex130_mult(x: Complex130, y: Complex130, *, output: Complex130): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + x0 = pt.abi.make(Int65) + x1 = pt.abi.make(Int65) + y0 = pt.abi.make(Int65) + y1 = pt.abi.make(Int65) + t1 = pt.abi.make(Int65) + t2 = pt.abi.make(Int65) + t3 = pt.abi.make(Int65) + t4 = pt.abi.make(Int65) + z0 = pt.abi.make(Int65) + z1 = pt.abi.make(Int65) + return pt.Seq( + x0.set(x[0]), + x1.set(x[1]), + y0.set(y[0]), + y1.set(y[1]), + # TODO: why can't I chain ABI calls? + # z0.set(int65_sub(int65_mult(x0, y0), int65_mult(x1, y1))), + # z1.set(int65_add(int65_mult(x0, y1), int65_mult(x1, y0))), + t1.set(int65_mult(x0, y0)), + t2.set(int65_mult(x1, y1)), + t3.set(int65_mult(x0, y1)), + t4.set(int65_mult(x1, y0)), + z0.set(int65_sub(t1, t2)), + z1.set(int65_add(t3, t4)), + output.set(z0, z1), + ) + + +@Blackbox(input_types=[None]) +@ABIReturnSubroutine +def complex130_real(x: Complex130, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + return output.set(x[0]) + + +@Blackbox(input_types=[None]) +@ABIReturnSubroutine +def complex130_imag(x: Complex130, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + return output.set(x[1]) + + +@Blackbox(input_types=[None]) +@ABIReturnSubroutine +def complex130_conjugate(x: Complex130, *, output: Complex130): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + z0 = pt.abi.make(Int65) + z1 = pt.abi.make(Int65) + return pt.Seq( + z0.set(complex130_real(x)), + z1.set(complex130_imag(x)), + z1.set(int65_negate(z1)), + output.set(z0, z1), + ) + + +@Blackbox(input_types=[None]) +@ABIReturnSubroutine +def complex130_norm_squared(x: Complex130, *, output: Int65): + """ + WARNING: not an ideal implementation. See explanation in first WARNING above. + """ + t = pt.abi.make(Complex130) + return pt.Seq( + t.set(complex130_conjugate(x)), + t.set(complex130_mult(t, x)), + output.set(complex130_real(t)), + ) + + +# ---- additional stand-alone ABIReturnSubroutine's ---- # + + +@Blackbox(input_types=[None]) +@ABIReturnSubroutine +def conditional_factorial(_factor: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + i = pt.ScratchVar(pt.TealType.uint64) + + return pt.Seq( + output.set(1), + pt.If(_factor.get() <= pt.Int(1)) + .Then(pt.Return()) + .Else( + pt.For( + i.store(_factor.get()), + i.load() > pt.Int(1), + i.store(i.load() - pt.Int(1)), + ).Do(output.set(output.get() * i.load())), + ), + ) + + +# ---- integration test functions ---- # + + +def test_integer65(): + bbpt_subtract_slick = PyTealDryRunExecutor(int65_sub, pt.Mode.Application) + + bbpt_subtract_cond = PyTealDryRunExecutor(int65_minus_cond, pt.Mode.Application) + + bbpt_mult = PyTealDryRunExecutor(int65_mult, pt.Mode.Application) + + bbpt_negate = PyTealDryRunExecutor(int65_negate, pt.Mode.Application) + + bbpt_add = PyTealDryRunExecutor(int65_add, pt.Mode.Application) + + def pynum_to_tuple(n): + return (n >= 0, abs(n)) + + def pytuple_to_num(t): + s, x = t + return x if s else -x + + N = 100 + random.seed(42) + + choices = range(-9_999, 10_000) + unary_inputs = [(pynum_to_tuple(x),) for x in random.sample(choices, N)] + + binary_inputs = [ + (pynum_to_tuple(x), pynum_to_tuple(y)) + for x, y in zip(random.sample(choices, N), random.sample(choices, N)) + ] + + def binary_dryrun(p: PyTealDryRunExecutor) -> list[DryRunInspector]: + return p.dryrun_on_sequence(binary_inputs) + + # Binary: + inspectors_subtract_slick = binary_dryrun(bbpt_subtract_slick) + + inspectors_subtract_cond = binary_dryrun(bbpt_subtract_cond) + + inspectors_mult = binary_dryrun(bbpt_mult) + + inspectors_add = binary_dryrun(bbpt_add) + + # Unary: + inspectors_negate = bbpt_negate.dryrun_on_sequence(unary_inputs) + + for i in range(N): + binary_args = binary_inputs[i] + x, y = tuple(map(pytuple_to_num, binary_args)) + + unary_args = unary_inputs[i] + u = pytuple_to_num(unary_args[0]) + + inspector_subtract_slick = inspectors_subtract_slick[i] + inspector_subtract_cond = inspectors_subtract_cond[i] + inspector_mult = inspectors_mult[i] + inspector_add = inspectors_add[i] + + inspector_negate = inspectors_negate[i] + + assert x - y == pytuple_to_num( + inspector_subtract_slick.last_log() + ), inspector_subtract_slick.report( + binary_args, f"failed for {binary_args}", row=i + ) + + assert x - y == pytuple_to_num( + inspector_subtract_cond.last_log() + ), inspector_subtract_cond.report( + binary_args, f"failed for {binary_args}", row=i + ) + + assert x * y == pytuple_to_num( + inspector_mult.last_log() + ), inspector_mult.report(binary_args, f"failed for {binary_args}", row=i) + + assert x + y == pytuple_to_num(inspector_add.last_log()), inspector_add.report( + binary_args, f"failed for {binary_args}", row=i + ) + + assert -u == pytuple_to_num( + inspector_negate.last_log() + ), inspector_negate.report(unary_args, f"failed for {unary_args}", row=i) + + +def test_complex130(): + # Binary: + + bbpt_cplx_add = PyTealDryRunExecutor(complex130_add, pt.Mode.Application) + + bbpt_cplx_mult = PyTealDryRunExecutor(complex130_mult, pt.Mode.Application) + + # Unary: + + bbpt_complex_real = PyTealDryRunExecutor(complex130_real, pt.Mode.Application) + + bbpt_complex_imag = PyTealDryRunExecutor(complex130_imag, pt.Mode.Application) + + bbpt_complex_conjugate = PyTealDryRunExecutor( + complex130_conjugate, pt.Mode.Application + ) + + bbpt_complex_norm_squared = PyTealDryRunExecutor( + complex130_norm_squared, pt.Mode.Application + ) + + def pyint_to_tuple(n): + return (n >= 0, abs(n)) + + def pycomplex_to_tuple(z): + return (pyint_to_tuple(int(z.real)), pyint_to_tuple(int(z.imag))) + + def pytuple_to_int(t): + s, x = t + return x if s else -x + + def pytuple_to_complex(tt): + tx, ty = tt + return complex(pytuple_to_int(tx), pytuple_to_int(ty)) + + N = 100 + # just for fun - no random seed - but this shouldn't be flakey + + choices = range(-999_999, 1_000_000) + + unary_inputs = [ + (pycomplex_to_tuple(complex(x, y)),) + for x, y in zip(random.sample(choices, N), random.sample(choices, N)) + ] + + binary_inputs = [ + (pycomplex_to_tuple(complex(x, y)), pycomplex_to_tuple(complex(z, w))) + for x, y, z, w in zip( + random.sample(choices, N), + random.sample(choices, N), + random.sample(choices, N), + random.sample(choices, N), + ) + ] + + # Binary: + def binary_dryrun(p: PyTealDryRunExecutor) -> list[DryRunInspector]: + return p.dryrun_on_sequence(binary_inputs) + + inspectors_cplx_add = binary_dryrun(bbpt_cplx_add) + + inspectors_cplx_mult = binary_dryrun(bbpt_cplx_mult) + + # Unary: + def unary_dryrun(p: PyTealDryRunExecutor) -> list[DryRunInspector]: + return p.dryrun_on_sequence(unary_inputs) + + inspectors_cplx_real = unary_dryrun(bbpt_complex_real) + + inspectors_cplx_imag = unary_dryrun(bbpt_complex_imag) + + inspectors_cplx_conjugate = unary_dryrun(bbpt_complex_conjugate) + + inspectors_cplx_norm_squared = unary_dryrun(bbpt_complex_norm_squared) + + for i in range(N): + binary_args = binary_inputs[i] + x, y = tuple(map(pytuple_to_complex, binary_args)) + + unary_args = unary_inputs[i] + u = pytuple_to_complex(unary_args[0]) + + # Binary: + + inspector_cplx_add = inspectors_cplx_add[i] + + inspector_cplx_mult = inspectors_cplx_mult[i] + + # Unary: + + inspector_cplx_real = inspectors_cplx_real[i] + + inspector_cplx_imag = inspectors_cplx_imag[i] + + inspector_cplx_conjugate = inspectors_cplx_conjugate[i] + + inspector_cplx_norm_squared = inspectors_cplx_norm_squared[i] + + assert x + y == pytuple_to_complex( + inspector_cplx_add.last_log() + ), inspector_cplx_add.report(binary_args, f"failed for {binary_args}", row=i) + + assert x * y == pytuple_to_complex( + inspector_cplx_mult.last_log() + ), inspector_cplx_mult.report(binary_args, f"failed for {binary_args}", row=i) + + assert u.real == pytuple_to_int( + inspector_cplx_real.last_log() + ), inspector_cplx_real.report(unary_args, f"failed for {unary_args}", row=i) + + assert u.imag == pytuple_to_int( + inspector_cplx_imag.last_log() + ), inspector_cplx_imag.report(unary_args, f"failed for {unary_args}", row=i) + + assert u.conjugate() == pytuple_to_complex( + inspector_cplx_conjugate.last_log() + ), inspector_cplx_conjugate.report( + unary_args, f"failed for {unary_args}", row=i + ) + + assert u * u.conjugate() == pytuple_to_int( + inspector_cplx_norm_squared.last_log() + ), inspector_cplx_norm_squared.report( + unary_args, f"failed for {unary_args}", row=i + ) + + +def py_factorial(n): + return 1 if n <= 1 else n * py_factorial(n - 1) + + +def test_conditional_factorial(): + ptdre = PyTealDryRunExecutor(conditional_factorial, pt.Mode.Application) + inputs = [(n,) for n in range(20)] + inspectors = ptdre.dryrun_on_sequence(inputs) + for i, args in enumerate(inputs): + inspector = inspectors[i] + n = args[0] + assert inspector.passed(), inspector.report(args, row=i + 1) + + expected = py_factorial(n) + assert expected == inspector.last_log(), inspector.report(args, row=i + 1) + + n = 21 + args = (n,) + inspector = ptdre.dryrun(args) + assert inspector.rejected(), inspector.report( + args, f"FAILED: should have rejected for {n=}", row=n + 1 + ) + assert inspector.error(), inspector.report( + args, f"FAILED: should error for {n=}", row=n + 1 + ) diff --git a/tests/integration/graviton_test.py b/tests/integration/graviton_test.py index da1a9ab5a..7ea8390ef 100644 --- a/tests/integration/graviton_test.py +++ b/tests/integration/graviton_test.py @@ -4,31 +4,15 @@ import pytest -from pyteal import ( - Bytes, - Concat, - For, - If, - Int, - Mode, - ScratchVar, - Seq, - While, - Continue, - Return, - Subroutine, - SubroutineFnWrapper, - TealType, - compileTeal, -) +import pyteal as pt from tests.compile_asserts import assert_teal_as_expected from tests.blackbox import ( Blackbox, BlackboxWrapper, algod_with_assertion, - blackbox_pyteal, mode_to_execution_mode, + PyTealDryRunExecutor, ) from graviton.blackbox import ( @@ -55,15 +39,9 @@ def wrap_compile_and_save( subr, mode, version, assemble_constants, test_name, case_name ): - is_app = mode == Mode.Application - - # 1. PyTeal program Expr generation - approval = blackbox_pyteal(subr, mode) + is_app = mode == pt.Mode.Application - # 2. TEAL generation - teal = compileTeal( - approval(), mode, version=version, assembleConstants=assemble_constants - ) + teal = PyTealDryRunExecutor(subr, mode).compile(version, assemble_constants) tealfile = f'{"app" if is_app else "lsig"}_{case_name}.teal' tealdir = GENERATED / test_name @@ -73,7 +51,7 @@ def wrap_compile_and_save( f.write(teal) print( - f"""subroutine {case_name}@{mode} generated TEAL. + f"""Subroutine {case_name}@{mode} generated TEAL. saved to {tealpath}: ------- {teal} @@ -87,60 +65,60 @@ def wrap_compile_and_save( @Blackbox(input_types=[]) -@Subroutine(TealType.uint64) +@pt.Subroutine(pt.TealType.uint64) def exp(): - return Int(2) ** Int(10) + return pt.Int(2) ** pt.Int(10) -@Blackbox(input_types=[TealType.uint64]) -@Subroutine(TealType.none) -def square_byref(x: ScratchVar): +@Blackbox(input_types=[pt.TealType.uint64]) +@pt.Subroutine(pt.TealType.none) +def square_byref(x: pt.ScratchVar): return x.store(x.load() * x.load()) -@Blackbox(input_types=[TealType.uint64]) -@Subroutine(TealType.uint64) +@Blackbox(input_types=[pt.TealType.uint64]) +@pt.Subroutine(pt.TealType.uint64) def square(x): - return x ** Int(2) + return x ** pt.Int(2) -@Blackbox(input_types=[TealType.anytype, TealType.anytype]) -@Subroutine(TealType.none) -def swap(x: ScratchVar, y: ScratchVar): - z = ScratchVar(TealType.anytype) - return Seq( +@Blackbox(input_types=[pt.TealType.anytype, pt.TealType.anytype]) +@pt.Subroutine(pt.TealType.none) +def swap(x: pt.ScratchVar, y: pt.ScratchVar): + z = pt.ScratchVar(pt.TealType.anytype) + return pt.Seq( z.store(x.load()), x.store(y.load()), y.store(z.load()), ) -@Blackbox(input_types=[TealType.bytes, TealType.uint64]) -@Subroutine(TealType.bytes) -def string_mult(s: ScratchVar, n): - i = ScratchVar(TealType.uint64) - tmp = ScratchVar(TealType.bytes) - start = Seq(i.store(Int(1)), tmp.store(s.load()), s.store(Bytes(""))) - step = i.store(i.load() + Int(1)) - return Seq( - For(start, i.load() <= n, step).Do(s.store(Concat(s.load(), tmp.load()))), +@Blackbox(input_types=[pt.TealType.bytes, pt.TealType.uint64]) +@pt.Subroutine(pt.TealType.bytes) +def string_mult(s: pt.ScratchVar, n): + i = pt.ScratchVar(pt.TealType.uint64) + tmp = pt.ScratchVar(pt.TealType.bytes) + start = pt.Seq(i.store(pt.Int(1)), tmp.store(s.load()), s.store(pt.Bytes(""))) + step = i.store(i.load() + pt.Int(1)) + return pt.Seq( + pt.For(start, i.load() <= n, step).Do(s.store(pt.Concat(s.load(), tmp.load()))), s.load(), ) -@Blackbox(input_types=[TealType.uint64]) -@Subroutine(TealType.uint64) +@Blackbox(input_types=[pt.TealType.uint64]) +@pt.Subroutine(pt.TealType.uint64) def oldfac(n): - return If(n < Int(2)).Then(Int(1)).Else(n * oldfac(n - Int(1))) + return pt.If(n < pt.Int(2)).Then(pt.Int(1)).Else(n * oldfac(n - pt.Int(1))) -@Blackbox(input_types=[TealType.uint64]) -@Subroutine(TealType.uint64) +@Blackbox(input_types=[pt.TealType.uint64]) +@pt.Subroutine(pt.TealType.uint64) def slow_fibonacci(n): return ( - If(n <= Int(1)) + pt.If(n <= pt.Int(1)) .Then(n) - .Else(slow_fibonacci(n - Int(2)) + slow_fibonacci(n - Int(1))) + .Else(slow_fibonacci(n - pt.Int(2)) + slow_fibonacci(n - pt.Int(1))) ) @@ -174,7 +152,7 @@ def fib_cost(args): "subr, mode", product( [exp, square_byref, square, swap, string_mult, oldfac, slow_fibonacci], - [Mode.Application, Mode.Signature], + [pt.Mode.Application, pt.Mode.Signature], ), ) def test_stable_teal_generation(subr, mode): @@ -509,8 +487,8 @@ def test_stable_teal_generation(subr, mode): def blackbox_test_runner( - subr: SubroutineFnWrapper, - mode: Mode, + subr: pt.SubroutineFnWrapper, + mode: pt.Mode, scenario: Dict[str, Any], version: int, assemble_constants: bool = True, @@ -521,7 +499,7 @@ def blackbox_test_runner( # 0. Validations assert isinstance(subr, BlackboxWrapper), f"unexpected subr type {type(subr)}" - assert isinstance(mode, Mode) + assert isinstance(mode, pt.Mode) # 1. Compile to TEAL teal, _, tealfile = wrap_compile_and_save( @@ -567,48 +545,39 @@ def blackbox_test_runner( @pytest.mark.parametrize("subr, scenario", APP_SCENARIOS.items()) def test_blackbox_subroutines_as_apps( - subr: SubroutineFnWrapper, + subr: pt.SubroutineFnWrapper, scenario: Dict[str, Any], ): - blackbox_test_runner(subr, Mode.Application, scenario, 6) + blackbox_test_runner(subr, pt.Mode.Application, scenario, 6) @pytest.mark.parametrize("subr, scenario", LOGICSIG_SCENARIOS.items()) def test_blackbox_subroutines_as_logic_sigs( - subr: SubroutineFnWrapper, + subr: pt.SubroutineFnWrapper, scenario: Dict[str, Any], ): - blackbox_test_runner(subr, Mode.Signature, scenario, 6) + blackbox_test_runner(subr, pt.Mode.Signature, scenario, 6) def blackbox_pyteal_example1(): # Example 1: Using blackbox_pyteal for a simple test of both an app and logic sig: - from graviton.blackbox import DryRunEncoder, DryRunExecutor + from graviton.blackbox import DryRunEncoder - from pyteal import compileTeal, Int, Mode, Subroutine, TealType - from tests.blackbox import Blackbox, algod_with_assertion, blackbox_pyteal + from pyteal import Int, Mode, Subroutine, TealType + from tests.blackbox import Blackbox @Blackbox(input_types=[TealType.uint64]) @Subroutine(TealType.uint64) def square(x): return x ** Int(2) - # create pyteal app and logic sig approvals: - approval_app = blackbox_pyteal(square, Mode.Application) - approval_lsig = blackbox_pyteal(square, Mode.Signature) - - # compile the evaluated approvals to generate TEAL: - app_teal = compileTeal(approval_app(), Mode.Application, version=6) - lsig_teal = compileTeal(approval_lsig(), Mode.Signature, version=6) - # provide args for evaluation (will compute x^2) x = 9 args = [x] # evaluate the programs - algod = algod_with_assertion() - app_result = DryRunExecutor.dryrun_app(algod, app_teal, args) - lsig_result = DryRunExecutor.dryrun_logicsig(algod, lsig_teal, args) + app_result = PyTealDryRunExecutor(square, Mode.Application).dryrun(args) + lsig_result = PyTealDryRunExecutor(square, Mode.Signature).dryrun(args) # check to see that x^2 is at the top of the stack as expected assert app_result.stack_top() == x**2, app_result.report( @@ -631,10 +600,9 @@ def blackbox_pyteal_example2(): from pathlib import Path import random - from graviton.blackbox import DryRunExecutor, DryRunInspector + from graviton.blackbox import DryRunInspector from pyteal import ( - compileTeal, For, If, Int, @@ -646,7 +614,7 @@ def blackbox_pyteal_example2(): TealType, ) - from tests.blackbox import Blackbox, algod_with_assertion, blackbox_pyteal + from tests.blackbox import Blackbox # GCD via the Euclidean Algorithm (iterative version): @Blackbox(input_types=[TealType.uint64, TealType.uint64]) @@ -662,10 +630,6 @@ def euclid(x, y): ) return Seq(For(start, cond, step).Do(Seq()), a.load()) - # create approval PyTeal and compile it to TEAL: - euclid_app = blackbox_pyteal(euclid, Mode.Application) - euclid_app_teal = compileTeal(euclid_app(), Mode.Application, version=6) - # generate a report with 400 = 20*20 dry run rows: N = 20 inputs = list( @@ -675,11 +639,10 @@ def euclid(x, y): ) ) - # execute the dry-run sequence: - algod = algod_with_assertion() - # assert that each result is that same as what Python's math.gcd() computes - inspectors = DryRunExecutor.dryrun_app_on_sequence(algod, euclid_app_teal, inputs) + inspectors = PyTealDryRunExecutor(euclid, Mode.Application).dryrun_on_sequence( + inputs + ) for i, result in enumerate(inspectors): args = inputs[i] assert result.stack_top() == math.gcd(*args), result.report( @@ -700,14 +663,13 @@ def blackbox_pyteal_example3(): from graviton.blackbox import ( DryRunEncoder, - DryRunExecutor, DryRunProperty as DRProp, ) from graviton.invariant import Invariant - from pyteal import compileTeal, If, Int, Mod, Mode, Subroutine, TealType + from pyteal import If, Int, Mod, Mode, Subroutine, TealType - from tests.blackbox import Blackbox, algod_with_assertion, blackbox_pyteal + from tests.blackbox import Blackbox # avoid flaky tests just in case I was wrong about the stack height invariant... random.seed(42) @@ -760,20 +722,162 @@ def euclid(x, y): .Else(If(y == Int(0)).Then(x).Else(euclid(y, Mod(x, y)))) ) - # Generate PyTeal and TEAL for the recursive Euclidean algorithm: - euclid_app = blackbox_pyteal(euclid, Mode.Application) - euclid_app_teal = compileTeal(euclid_app(), Mode.Application, version=6) - # Execute on the input sequence to get a dry-run inspectors: - algod = algod_with_assertion() - inspectors = DryRunExecutor.dryrun_app_on_sequence(algod, euclid_app_teal, inputs) + inspectors = PyTealDryRunExecutor(euclid, Mode.Application).dryrun_on_sequence( + inputs + ) # Assert that each invariant holds on the sequences of inputs and dry-runs: for property, predicate in predicates.items(): Invariant(predicate).validates(property, inputs, inspectors) +def blackbox_pyteal_example4(): + # Example 4: Using PyTealDryRunExecutor to debug an ABIReturnSubroutine with an app, logic sig and csv report + from pathlib import Path + import random + + from graviton.blackbox import DryRunInspector + + from pyteal import ( + abi, + ABIReturnSubroutine, + Expr, + For, + Int, + Mode, + ScratchVar, + Seq, + TealType, + ) + + from tests.blackbox import Blackbox, PyTealDryRunExecutor + + # Sum a dynamic uint64 array + @Blackbox(input_types=[None]) + @ABIReturnSubroutine + def abi_sum(toSum: abi.DynamicArray[abi.Uint64], *, output: abi.Uint64) -> Expr: + i = ScratchVar(TealType.uint64) + valueAtIndex = abi.Uint64() + return Seq( + output.set(0), + For( + i.store(Int(0)), + i.load() < toSum.length(), + i.store(i.load() + Int(1)), + ).Do( + Seq( + toSum[i.load()].store_into(valueAtIndex), + output.set(output.get() + valueAtIndex.get()), + ) + ), + ) + + # instantiate PyTealDryRunExecutor objects for the app and lsig: + app_pytealer = PyTealDryRunExecutor(abi_sum, Mode.Application) + lsig_pytealer = PyTealDryRunExecutor(abi_sum, Mode.Signature) + + # generate reports with the same random inputs (fix the randomness with a seed): + random.seed(42) + + N = 50 # the number of dry runs for each experiment + choices = range(10_000) + inputs = [] + for n in range(N): + inputs.append(tuple([random.sample(choices, n)])) + + app_inspectors = app_pytealer.dryrun_on_sequence(inputs) + + lsig_inspectors = lsig_pytealer.dryrun_on_sequence(inputs) + + for i in range(N): + args = inputs[i] + + app_inspector = app_inspectors[i] + lsig_inspector = lsig_inspectors[i] + + def message(insp): + return insp.report(args, f"failed for {args}", row=i) + + # the app should pass exactly when it's cost was within the 700 budget: + assert app_inspector.passed() == (app_inspector.cost() <= 700), message( + app_inspector + ) + # the lsig always passes (never goes over budget): + assert lsig_inspector.passed(), message(lsig_inspector) + + expected = sum(args[0]) + actual4app = app_inspector.last_log() + assert expected == actual4app, message(app_inspector) + + if i > 0: + assert expected in app_inspector.final_scratch().values(), message( + app_inspector + ) + assert expected in lsig_inspector.final_scratch().values(), message( + lsig_inspector + ) + + def report(kind): + assert kind in ("app", "lsig") + insps = app_inspectors if kind == "app" else lsig_inspectors + csv_report = DryRunInspector.csv_report(inputs, insps) + with open(Path.cwd() / f"abi_sum_{kind}.csv", "w") as f: + f.write(csv_report) + + report("app") + report("lsig") + + +def blackbox_pyteal_example5(): + from graviton.blackbox import DryRunEncoder + + from pyteal import abi, Subroutine, TealType, Int, Mode + from tests.blackbox import Blackbox + + @Blackbox([None]) + @Subroutine(TealType.uint64) + def cubed(n: abi.Uint64): + return n.get() ** Int(3) + + app_pytealer = PyTealDryRunExecutor(cubed, Mode.Application) + lsig_pytealer = PyTealDryRunExecutor(cubed, Mode.Signature) + + inputs = [[i] for i in range(1, 11)] + + app_inspect = app_pytealer.dryrun_on_sequence(inputs) + lsig_inspect = lsig_pytealer.dryrun_on_sequence(inputs) + + for index, inspect in enumerate(app_inspect): + input_var = inputs[index][0] + assert inspect.stack_top() == input_var**3, inspect.report( + args=inputs[index], msg="stack_top() gave unexpected results from app" + ) + assert inspect.last_log() == DryRunEncoder.hex(input_var**3), inspect.report( + args=inputs[index], msg="last_log() gave unexpected results from app" + ) + + for index, inspect in enumerate(lsig_inspect): + input_var = inputs[index][0] + assert inspect.stack_top() == input_var**3, inspect.report( + args=inputs[index], msg="stack_top() gave unexpected results from app" + ) + + def blackbox_pyteal_while_continue_test(): + from tests.blackbox import Blackbox + from pyteal import ( + Continue, + Int, + Mode, + Return, + ScratchVar, + Seq, + Subroutine, + TealType, + While, + ) + @Blackbox(input_types=[TealType.uint64]) @Subroutine(TealType.uint64) def while_continue_accumulation(n): @@ -789,13 +893,11 @@ def while_continue_accumulation(n): Return(i.load()), ) - approval_lsig = blackbox_pyteal(while_continue_accumulation, Mode.Signature) - lsig_teal = compileTeal(approval_lsig(), Mode.Signature, version=6) - algod = algod_with_assertion() - for x in range(30): args = [x] - lsig_result = DryRunExecutor.dryrun_logicsig(algod, lsig_teal, args) + lsig_result = PyTealDryRunExecutor( + while_continue_accumulation, Mode.Signature + ).dryrun(args) if x == 0: assert not lsig_result.passed() else: @@ -812,6 +914,8 @@ def while_continue_accumulation(n): blackbox_pyteal_example1, blackbox_pyteal_example2, blackbox_pyteal_example3, + blackbox_pyteal_example4, + blackbox_pyteal_example5, blackbox_pyteal_while_continue_test, ], ) diff --git a/tests/integration/pure_logicsig_test.py b/tests/integration/pure_logicsig_test.py new file mode 100644 index 000000000..40e322447 --- /dev/null +++ b/tests/integration/pure_logicsig_test.py @@ -0,0 +1,126 @@ +from itertools import product +from os import environ +from pathlib import Path +import pytest + +from pyteal import compileTeal, Mode + +import examples.signature.factorizer_game as factorizer + +from tests.blackbox import algod_with_assertion +from graviton.blackbox import ( + DryRunExecutor as Executor, + DryRunInspector as Inspector, + DryRunProperty as DRProp, +) +from graviton.invariant import Invariant + +REPORTS_DIR = Path.cwd() / "tests" / "integration" / "reports" +ALGOD = algod_with_assertion() + +DEFAULT = { + "A": 3, + "P": 5, # 13 + "Q": 7, # 13 + "M": 5, # 10 + "N": 5, # 10 +} + + +def get_param_bounds(): + """ + Allow setting the bounds either from the environment via something like: + + % A=3 P=13 Q=13 M=10 N=10 pytest tests/integration/pure_logicsig_test.py::test_many_factorizer_games + + OR - when any of the above is missing, replace with the default version + """ + vars = [] + for var in ("A", "P", "Q", "M", "N"): + val = environ.get(var) + if val is None: + val = DEFAULT[var] + vars.append(int(val)) + return vars + + +def get_factorizer_param_sequence(): + A, P, Q, M, N = get_param_bounds() + return [(a, p, q, M, N) for a in range(A) for p in range(P) for q in range(Q)] + + +def inputs_for_coefficients(a, p, q, M, N): + # TODO: this should really be focused around the roots p and q + return product(range(M), range(N)) + + +def factorizer_game_check(a: int, p: int, q: int, M: int, N: int): + ae = None + if a <= 0 or p < 0 or q <= p: + with pytest.raises(AssertionError) as ae: + factorizer.logicsig(a, p, q), + + if ae: + return + + compiled = compileTeal( + factorizer.logicsig(a, p, q), + version=6, + mode=Mode.Signature, + assembleConstants=True, + ) + inputs = list(inputs_for_coefficients(a, p, q, M, N)) + N = len(inputs) + + def poly(x): + return abs(a * x**2 - a * (p + q) * x + a * p * q) + + def naive_prize(x, y): + return 1_000_000 * max(10 - (sum(map(poly, (x, y))) + 1) // 2, 0) + + def payment_amount(x, y): + return 0 if x == y else naive_prize(x, y) + + amts = list(map(lambda args: payment_amount(*args), inputs)) + + inspectors, txns = [], [] + for args, amt in zip(inputs, amts): + txn = {"amt": amt} + txns.append(txn) + inspectors.append(Executor.dryrun_logicsig(ALGOD, compiled, args, **txn)) + + print( + f"generating a report for (a,p,q) = {a,p,q} with {M, N} dry-run calls and spreadsheet rows" + ) + filebase = f"factorizer_game_{a}_{p}_{q}" + + reports_dir = REPORTS_DIR / "pure_logicsig" + reports_dir.mkdir(parents=True, exist_ok=True) + csvpath = reports_dir / f"{filebase}.csv" + with open(csvpath, "w") as f: + f.write(Inspector.csv_report(inputs, inspectors, txns=txns)) + + print(f"validating passing_invariant for (a,p,q) = {a,p,q} over {N} dry-run calls") + passing_invariant = Invariant( + lambda args: bool(payment_amount(*args)), + name=f"passing invariant for coeffs {a, p, q}", + ) + passing_invariant.validates(DRProp.passed, inputs, inspectors) + + print( + f"validate procedurally that payment amount as expected for (a,p,q) = {a,p,q} over {M, N} dry-rundry-run calls" + ) + + for args, inspector in zip(inputs, inspectors): + x, y = args + eprize = naive_prize(x, y) + final_scratches = inspector.final_scratch().values() + assert eprize == 0 or eprize in final_scratches, inspector.report( + args, + f"(a, p, q, x, y) = {a, p, q, x, y}. final scratch slots expected to contain {eprize} v. actual={final_scratches}", + ) + + +@pytest.mark.parametrize("a, p, q, M, N", get_factorizer_param_sequence()) +def test_many_factorizer_games(a: int, p: int, q: int, M: int, N: int): + factorizer_game_check(a, p, q, M, N) diff --git a/tests/integration/teal/roundtrip/app_roundtrip_().teal b/tests/integration/teal/roundtrip/app_roundtrip_().teal new file mode 100644 index 000000000..6ce9ab897 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_().teal @@ -0,0 +1,38 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 6 +byte "" +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub tuplecomplement_0 +store 4 +load 4 +callsub tuplecomplement_0 +store 5 +load 2 +load 4 +concat +load 5 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool).teal new file mode 100644 index 000000000..0f40d760a --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool).teal @@ -0,0 +1,59 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 2 +load 2 +callsub roundtripper_1 +store 1 +byte 0x151f7c75 +load 1 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 7 +load 7 +int 0 +getbit +store 0 +load 0 +callsub boolcomp_2 +store 0 +byte 0x00 +int 0 +load 0 +setbit +store 8 +load 8 +retsub + +// round_tripper +roundtripper_1: +store 3 +load 3 +callsub tuplecomplement_0 +store 5 +load 5 +callsub tuplecomplement_0 +store 6 +load 3 +load 5 +concat +load 6 +concat +store 4 +load 4 +retsub + +// bool_comp +boolcomp_2: +store 9 +load 9 +! +! +! +store 10 +load 10 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool)[10].teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool)[10].teal new file mode 100644 index 000000000..fcf6ceb0f --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool)[10].teal @@ -0,0 +1,185 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 2 +load 2 +callsub roundtripper_2 +store 1 +byte 0x151f7c75 +load 1 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 19 +load 19 +int 0 +getbit +store 0 +load 0 +callsub boolcomp_3 +store 0 +byte 0x00 +int 0 +load 0 +setbit +store 20 +load 20 +retsub + +// array_complement +arraycomplement_1: +store 7 +load 7 +int 1 +int 0 +* +int 1 +extract3 +store 9 +load 7 +int 1 +int 1 +* +int 1 +extract3 +store 10 +load 7 +int 1 +int 2 +* +int 1 +extract3 +store 11 +load 7 +int 1 +int 3 +* +int 1 +extract3 +store 12 +load 7 +int 1 +int 4 +* +int 1 +extract3 +store 13 +load 7 +int 1 +int 5 +* +int 1 +extract3 +store 14 +load 7 +int 1 +int 6 +* +int 1 +extract3 +store 15 +load 7 +int 1 +int 7 +* +int 1 +extract3 +store 16 +load 7 +int 1 +int 8 +* +int 1 +extract3 +store 17 +load 7 +int 1 +int 9 +* +int 1 +extract3 +store 18 +load 9 +callsub tuplecomplement_0 +store 9 +load 10 +callsub tuplecomplement_0 +store 10 +load 11 +callsub tuplecomplement_0 +store 11 +load 12 +callsub tuplecomplement_0 +store 12 +load 13 +callsub tuplecomplement_0 +store 13 +load 14 +callsub tuplecomplement_0 +store 14 +load 15 +callsub tuplecomplement_0 +store 15 +load 16 +callsub tuplecomplement_0 +store 16 +load 17 +callsub tuplecomplement_0 +store 17 +load 18 +callsub tuplecomplement_0 +store 18 +load 9 +load 10 +concat +load 11 +concat +load 12 +concat +load 13 +concat +load 14 +concat +load 15 +concat +load 16 +concat +load 17 +concat +load 18 +concat +store 8 +load 8 +retsub + +// round_tripper +roundtripper_2: +store 3 +load 3 +callsub arraycomplement_1 +store 5 +load 5 +callsub arraycomplement_1 +store 6 +load 3 +load 5 +concat +load 6 +concat +store 4 +load 4 +retsub + +// bool_comp +boolcomp_3: +store 21 +load 21 +! +! +! +store 22 +load 22 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte).teal new file mode 100644 index 000000000..f9ef93fdb --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte).teal @@ -0,0 +1,85 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 3 +load 3 +callsub roundtripper_1 +store 2 +byte 0x151f7c75 +load 2 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 8 +load 8 +int 0 +getbit +store 0 +load 8 +int 1 +getbyte +store 1 +load 0 +callsub boolcomp_2 +store 0 +load 1 +callsub numericalcomp_3 +store 1 +byte 0x00 +int 0 +load 0 +setbit +byte 0x00 +int 0 +load 1 +setbyte +concat +store 9 +load 9 +retsub + +// round_tripper +roundtripper_1: +store 4 +load 4 +callsub tuplecomplement_0 +store 6 +load 6 +callsub tuplecomplement_0 +store 7 +load 4 +load 6 +concat +load 7 +concat +store 5 +load 5 +retsub + +// bool_comp +boolcomp_2: +store 10 +load 10 +! +! +! +store 11 +load 11 +retsub + +// numerical_comp +numericalcomp_3: +store 12 +int 255 +load 12 +- +store 13 +load 13 +int 256 +< +assert +load 13 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string).teal new file mode 100644 index 000000000..1c901b056 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string).teal @@ -0,0 +1,683 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 5 +load 5 +callsub roundtripper_1 +store 4 +byte 0x151f7c75 +load 4 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 10 +load 10 +int 0 +getbit +store 0 +load 10 +int 1 +getbyte +store 1 +load 10 +extract 2 32 +store 2 +load 10 +load 10 +int 34 +extract_uint16 +dig 1 +len +substring3 +store 3 +load 0 +callsub boolcomp_2 +store 0 +load 1 +callsub numericalcomp_3 +store 1 +load 2 +callsub arraycomplement_5 +store 2 +load 3 +callsub stringreverse_6 +store 3 +byte 0x00 +int 0 +load 0 +setbit +byte 0x00 +int 0 +load 1 +setbyte +concat +load 2 +concat +load 3 +store 60 +load 60 +store 59 +int 36 +store 58 +load 58 +itob +extract 6 0 +concat +load 59 +concat +store 11 +load 11 +retsub + +// round_tripper +roundtripper_1: +store 6 +load 6 +callsub tuplecomplement_0 +store 8 +load 8 +callsub tuplecomplement_0 +store 9 +load 6 +store 64 +load 64 +store 63 +int 6 +store 61 +load 61 +load 64 +len ++ +store 62 +load 62 +int 65536 +< +assert +load 61 +itob +extract 6 0 +load 8 +store 64 +load 63 +load 64 +concat +store 63 +load 62 +store 61 +load 61 +load 64 +len ++ +store 62 +load 62 +int 65536 +< +assert +load 61 +itob +extract 6 0 +concat +load 9 +store 64 +load 63 +load 64 +concat +store 63 +load 62 +store 61 +load 61 +itob +extract 6 0 +concat +load 63 +concat +store 7 +load 7 +retsub + +// bool_comp +boolcomp_2: +store 12 +load 12 +! +! +! +store 13 +load 13 +retsub + +// numerical_comp +numericalcomp_3: +store 14 +int 255 +load 14 +- +store 15 +load 15 +int 256 +< +assert +load 15 +retsub + +// numerical_comp +numericalcomp_4: +store 50 +int 255 +load 50 +- +store 51 +load 51 +int 256 +< +assert +load 51 +retsub + +// array_complement +arraycomplement_5: +store 16 +load 16 +int 1 +int 0 +* +getbyte +store 18 +load 16 +int 1 +int 1 +* +getbyte +store 19 +load 16 +int 1 +int 2 +* +getbyte +store 20 +load 16 +int 1 +int 3 +* +getbyte +store 21 +load 16 +int 1 +int 4 +* +getbyte +store 22 +load 16 +int 1 +int 5 +* +getbyte +store 23 +load 16 +int 1 +int 6 +* +getbyte +store 24 +load 16 +int 1 +int 7 +* +getbyte +store 25 +load 16 +int 1 +int 8 +* +getbyte +store 26 +load 16 +int 1 +int 9 +* +getbyte +store 27 +load 16 +int 1 +int 10 +* +getbyte +store 28 +load 16 +int 1 +int 11 +* +getbyte +store 29 +load 16 +int 1 +int 12 +* +getbyte +store 30 +load 16 +int 1 +int 13 +* +getbyte +store 31 +load 16 +int 1 +int 14 +* +getbyte +store 32 +load 16 +int 1 +int 15 +* +getbyte +store 33 +load 16 +int 1 +int 16 +* +getbyte +store 34 +load 16 +int 1 +int 17 +* +getbyte +store 35 +load 16 +int 1 +int 18 +* +getbyte +store 36 +load 16 +int 1 +int 19 +* +getbyte +store 37 +load 16 +int 1 +int 20 +* +getbyte +store 38 +load 16 +int 1 +int 21 +* +getbyte +store 39 +load 16 +int 1 +int 22 +* +getbyte +store 40 +load 16 +int 1 +int 23 +* +getbyte +store 41 +load 16 +int 1 +int 24 +* +getbyte +store 42 +load 16 +int 1 +int 25 +* +getbyte +store 43 +load 16 +int 1 +int 26 +* +getbyte +store 44 +load 16 +int 1 +int 27 +* +getbyte +store 45 +load 16 +int 1 +int 28 +* +getbyte +store 46 +load 16 +int 1 +int 29 +* +getbyte +store 47 +load 16 +int 1 +int 30 +* +getbyte +store 48 +load 16 +int 1 +int 31 +* +getbyte +store 49 +load 18 +callsub numericalcomp_4 +store 18 +load 19 +callsub numericalcomp_4 +store 19 +load 20 +callsub numericalcomp_4 +store 20 +load 21 +callsub numericalcomp_4 +store 21 +load 22 +callsub numericalcomp_4 +store 22 +load 23 +callsub numericalcomp_4 +store 23 +load 24 +callsub numericalcomp_4 +store 24 +load 25 +callsub numericalcomp_4 +store 25 +load 26 +callsub numericalcomp_4 +store 26 +load 27 +callsub numericalcomp_4 +store 27 +load 28 +callsub numericalcomp_4 +store 28 +load 29 +callsub numericalcomp_4 +store 29 +load 30 +callsub numericalcomp_4 +store 30 +load 31 +callsub numericalcomp_4 +store 31 +load 32 +callsub numericalcomp_4 +store 32 +load 33 +callsub numericalcomp_4 +store 33 +load 34 +callsub numericalcomp_4 +store 34 +load 35 +callsub numericalcomp_4 +store 35 +load 36 +callsub numericalcomp_4 +store 36 +load 37 +callsub numericalcomp_4 +store 37 +load 38 +callsub numericalcomp_4 +store 38 +load 39 +callsub numericalcomp_4 +store 39 +load 40 +callsub numericalcomp_4 +store 40 +load 41 +callsub numericalcomp_4 +store 41 +load 42 +callsub numericalcomp_4 +store 42 +load 43 +callsub numericalcomp_4 +store 43 +load 44 +callsub numericalcomp_4 +store 44 +load 45 +callsub numericalcomp_4 +store 45 +load 46 +callsub numericalcomp_4 +store 46 +load 47 +callsub numericalcomp_4 +store 47 +load 48 +callsub numericalcomp_4 +store 48 +load 49 +callsub numericalcomp_4 +store 49 +byte 0x00 +int 0 +load 18 +setbyte +byte 0x00 +int 0 +load 19 +setbyte +concat +byte 0x00 +int 0 +load 20 +setbyte +concat +byte 0x00 +int 0 +load 21 +setbyte +concat +byte 0x00 +int 0 +load 22 +setbyte +concat +byte 0x00 +int 0 +load 23 +setbyte +concat +byte 0x00 +int 0 +load 24 +setbyte +concat +byte 0x00 +int 0 +load 25 +setbyte +concat +byte 0x00 +int 0 +load 26 +setbyte +concat +byte 0x00 +int 0 +load 27 +setbyte +concat +byte 0x00 +int 0 +load 28 +setbyte +concat +byte 0x00 +int 0 +load 29 +setbyte +concat +byte 0x00 +int 0 +load 30 +setbyte +concat +byte 0x00 +int 0 +load 31 +setbyte +concat +byte 0x00 +int 0 +load 32 +setbyte +concat +byte 0x00 +int 0 +load 33 +setbyte +concat +byte 0x00 +int 0 +load 34 +setbyte +concat +byte 0x00 +int 0 +load 35 +setbyte +concat +byte 0x00 +int 0 +load 36 +setbyte +concat +byte 0x00 +int 0 +load 37 +setbyte +concat +byte 0x00 +int 0 +load 38 +setbyte +concat +byte 0x00 +int 0 +load 39 +setbyte +concat +byte 0x00 +int 0 +load 40 +setbyte +concat +byte 0x00 +int 0 +load 41 +setbyte +concat +byte 0x00 +int 0 +load 42 +setbyte +concat +byte 0x00 +int 0 +load 43 +setbyte +concat +byte 0x00 +int 0 +load 44 +setbyte +concat +byte 0x00 +int 0 +load 45 +setbyte +concat +byte 0x00 +int 0 +load 46 +setbyte +concat +byte 0x00 +int 0 +load 47 +setbyte +concat +byte 0x00 +int 0 +load 48 +setbyte +concat +byte 0x00 +int 0 +load 49 +setbyte +concat +store 17 +load 17 +retsub + +// string_reverse +stringreverse_6: +store 52 +load 52 +int 1 +int 0 +* +int 2 ++ +getbyte +store 56 +load 52 +int 1 +int 1 +* +int 2 ++ +getbyte +store 55 +load 52 +int 1 +int 2 +* +int 2 ++ +getbyte +store 54 +int 3 +store 57 +load 57 +itob +extract 6 0 +byte 0x00 +int 0 +load 54 +setbyte +byte 0x00 +int 0 +load 55 +setbyte +concat +byte 0x00 +int 0 +load 56 +setbyte +concat +concat +store 53 +load 53 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,(address,(uint32,string[],bool[2],(byte),uint8)[2],string,bool[]))[]_<2>.teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,(address,(uint32,string[],bool[2],(byte),uint8)[2],string,bool[]))[]_<2>.teal new file mode 100644 index 000000000..b31be1f5b --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,(address,(uint32,string[],bool[2],(byte),uint8)[2],string,bool[]))[]_<2>.teal @@ -0,0 +1,2033 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 6 +load 6 +callsub roundtripper_2 +store 5 +byte 0x151f7c75 +load 5 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 17 +load 17 +int 0 +getbit +store 0 +load 17 +int 1 +getbyte +store 1 +load 17 +extract 2 32 +store 2 +load 17 +load 17 +int 34 +extract_uint16 +load 17 +int 36 +extract_uint16 +substring3 +store 3 +load 17 +load 17 +int 36 +extract_uint16 +dig 1 +len +substring3 +store 4 +load 0 +callsub boolcomp_3 +store 0 +load 1 +callsub numericalcomp_4 +store 1 +load 2 +callsub arraycomplement_6 +store 2 +load 3 +callsub stringreverse_7 +store 3 +load 4 +callsub tuplecomplement_8 +store 4 +byte 0x00 +int 0 +load 0 +setbit +byte 0x00 +int 0 +load 1 +setbyte +concat +load 2 +concat +load 3 +store 180 +load 180 +store 179 +int 38 +store 177 +load 177 +load 180 +len ++ +store 178 +load 178 +int 65536 +< +assert +load 177 +itob +extract 6 0 +concat +load 4 +store 180 +load 179 +load 180 +concat +store 179 +load 178 +store 177 +load 177 +itob +extract 6 0 +concat +load 179 +concat +store 18 +load 18 +retsub + +// array_complement +arraycomplement_1: +store 11 +load 11 +load 11 +int 2 +int 0 +* +int 2 ++ +extract_uint16 +int 2 ++ +int 0 +int 1 ++ +load 11 +int 0 +extract_uint16 +store 15 +load 15 +== +bnz arraycomplement_1_l5 +load 11 +int 2 +int 0 +* +int 2 ++ +int 2 ++ +extract_uint16 +int 2 ++ +arraycomplement_1_l2: +substring3 +store 13 +load 11 +load 11 +int 2 +int 1 +* +int 2 ++ +extract_uint16 +int 2 ++ +int 1 +int 1 ++ +load 11 +int 0 +extract_uint16 +store 16 +load 16 +== +bnz arraycomplement_1_l4 +load 11 +int 2 +int 1 +* +int 2 ++ +int 2 ++ +extract_uint16 +int 2 ++ +b arraycomplement_1_l6 +arraycomplement_1_l4: +load 11 +len +b arraycomplement_1_l6 +arraycomplement_1_l5: +load 11 +len +b arraycomplement_1_l2 +arraycomplement_1_l6: +substring3 +store 14 +load 13 +callsub tuplecomplement_0 +store 13 +load 14 +callsub tuplecomplement_0 +store 14 +int 2 +store 185 +load 185 +itob +extract 6 0 +load 13 +store 184 +load 184 +store 183 +int 4 +store 181 +load 181 +load 184 +len ++ +store 182 +load 182 +int 65536 +< +assert +load 181 +itob +extract 6 0 +load 14 +store 184 +load 183 +load 184 +concat +store 183 +load 182 +store 181 +load 181 +itob +extract 6 0 +concat +load 183 +concat +concat +store 12 +load 12 +retsub + +// round_tripper +roundtripper_2: +store 7 +load 7 +callsub arraycomplement_1 +store 9 +load 9 +callsub arraycomplement_1 +store 10 +load 7 +store 189 +load 189 +store 188 +int 6 +store 186 +load 186 +load 189 +len ++ +store 187 +load 187 +int 65536 +< +assert +load 186 +itob +extract 6 0 +load 9 +store 189 +load 188 +load 189 +concat +store 188 +load 187 +store 186 +load 186 +load 189 +len ++ +store 187 +load 187 +int 65536 +< +assert +load 186 +itob +extract 6 0 +concat +load 10 +store 189 +load 188 +load 189 +concat +store 188 +load 187 +store 186 +load 186 +itob +extract 6 0 +concat +load 188 +concat +store 8 +load 8 +retsub + +// bool_comp +boolcomp_3: +store 23 +load 23 +! +! +! +store 24 +load 24 +retsub + +// numerical_comp +numericalcomp_4: +store 25 +int 255 +load 25 +- +store 26 +load 26 +int 256 +< +assert +load 26 +retsub + +// numerical_comp +numericalcomp_5: +store 61 +int 255 +load 61 +- +store 62 +load 62 +int 256 +< +assert +load 62 +retsub + +// array_complement +arraycomplement_6: +store 27 +load 27 +int 1 +int 0 +* +getbyte +store 29 +load 27 +int 1 +int 1 +* +getbyte +store 30 +load 27 +int 1 +int 2 +* +getbyte +store 31 +load 27 +int 1 +int 3 +* +getbyte +store 32 +load 27 +int 1 +int 4 +* +getbyte +store 33 +load 27 +int 1 +int 5 +* +getbyte +store 34 +load 27 +int 1 +int 6 +* +getbyte +store 35 +load 27 +int 1 +int 7 +* +getbyte +store 36 +load 27 +int 1 +int 8 +* +getbyte +store 37 +load 27 +int 1 +int 9 +* +getbyte +store 38 +load 27 +int 1 +int 10 +* +getbyte +store 39 +load 27 +int 1 +int 11 +* +getbyte +store 40 +load 27 +int 1 +int 12 +* +getbyte +store 41 +load 27 +int 1 +int 13 +* +getbyte +store 42 +load 27 +int 1 +int 14 +* +getbyte +store 43 +load 27 +int 1 +int 15 +* +getbyte +store 44 +load 27 +int 1 +int 16 +* +getbyte +store 45 +load 27 +int 1 +int 17 +* +getbyte +store 46 +load 27 +int 1 +int 18 +* +getbyte +store 47 +load 27 +int 1 +int 19 +* +getbyte +store 48 +load 27 +int 1 +int 20 +* +getbyte +store 49 +load 27 +int 1 +int 21 +* +getbyte +store 50 +load 27 +int 1 +int 22 +* +getbyte +store 51 +load 27 +int 1 +int 23 +* +getbyte +store 52 +load 27 +int 1 +int 24 +* +getbyte +store 53 +load 27 +int 1 +int 25 +* +getbyte +store 54 +load 27 +int 1 +int 26 +* +getbyte +store 55 +load 27 +int 1 +int 27 +* +getbyte +store 56 +load 27 +int 1 +int 28 +* +getbyte +store 57 +load 27 +int 1 +int 29 +* +getbyte +store 58 +load 27 +int 1 +int 30 +* +getbyte +store 59 +load 27 +int 1 +int 31 +* +getbyte +store 60 +load 29 +callsub numericalcomp_5 +store 29 +load 30 +callsub numericalcomp_5 +store 30 +load 31 +callsub numericalcomp_5 +store 31 +load 32 +callsub numericalcomp_5 +store 32 +load 33 +callsub numericalcomp_5 +store 33 +load 34 +callsub numericalcomp_5 +store 34 +load 35 +callsub numericalcomp_5 +store 35 +load 36 +callsub numericalcomp_5 +store 36 +load 37 +callsub numericalcomp_5 +store 37 +load 38 +callsub numericalcomp_5 +store 38 +load 39 +callsub numericalcomp_5 +store 39 +load 40 +callsub numericalcomp_5 +store 40 +load 41 +callsub numericalcomp_5 +store 41 +load 42 +callsub numericalcomp_5 +store 42 +load 43 +callsub numericalcomp_5 +store 43 +load 44 +callsub numericalcomp_5 +store 44 +load 45 +callsub numericalcomp_5 +store 45 +load 46 +callsub numericalcomp_5 +store 46 +load 47 +callsub numericalcomp_5 +store 47 +load 48 +callsub numericalcomp_5 +store 48 +load 49 +callsub numericalcomp_5 +store 49 +load 50 +callsub numericalcomp_5 +store 50 +load 51 +callsub numericalcomp_5 +store 51 +load 52 +callsub numericalcomp_5 +store 52 +load 53 +callsub numericalcomp_5 +store 53 +load 54 +callsub numericalcomp_5 +store 54 +load 55 +callsub numericalcomp_5 +store 55 +load 56 +callsub numericalcomp_5 +store 56 +load 57 +callsub numericalcomp_5 +store 57 +load 58 +callsub numericalcomp_5 +store 58 +load 59 +callsub numericalcomp_5 +store 59 +load 60 +callsub numericalcomp_5 +store 60 +byte 0x00 +int 0 +load 29 +setbyte +byte 0x00 +int 0 +load 30 +setbyte +concat +byte 0x00 +int 0 +load 31 +setbyte +concat +byte 0x00 +int 0 +load 32 +setbyte +concat +byte 0x00 +int 0 +load 33 +setbyte +concat +byte 0x00 +int 0 +load 34 +setbyte +concat +byte 0x00 +int 0 +load 35 +setbyte +concat +byte 0x00 +int 0 +load 36 +setbyte +concat +byte 0x00 +int 0 +load 37 +setbyte +concat +byte 0x00 +int 0 +load 38 +setbyte +concat +byte 0x00 +int 0 +load 39 +setbyte +concat +byte 0x00 +int 0 +load 40 +setbyte +concat +byte 0x00 +int 0 +load 41 +setbyte +concat +byte 0x00 +int 0 +load 42 +setbyte +concat +byte 0x00 +int 0 +load 43 +setbyte +concat +byte 0x00 +int 0 +load 44 +setbyte +concat +byte 0x00 +int 0 +load 45 +setbyte +concat +byte 0x00 +int 0 +load 46 +setbyte +concat +byte 0x00 +int 0 +load 47 +setbyte +concat +byte 0x00 +int 0 +load 48 +setbyte +concat +byte 0x00 +int 0 +load 49 +setbyte +concat +byte 0x00 +int 0 +load 50 +setbyte +concat +byte 0x00 +int 0 +load 51 +setbyte +concat +byte 0x00 +int 0 +load 52 +setbyte +concat +byte 0x00 +int 0 +load 53 +setbyte +concat +byte 0x00 +int 0 +load 54 +setbyte +concat +byte 0x00 +int 0 +load 55 +setbyte +concat +byte 0x00 +int 0 +load 56 +setbyte +concat +byte 0x00 +int 0 +load 57 +setbyte +concat +byte 0x00 +int 0 +load 58 +setbyte +concat +byte 0x00 +int 0 +load 59 +setbyte +concat +byte 0x00 +int 0 +load 60 +setbyte +concat +store 28 +load 28 +retsub + +// string_reverse +stringreverse_7: +store 63 +load 63 +int 1 +int 0 +* +int 2 ++ +getbyte +store 67 +load 63 +int 1 +int 1 +* +int 2 ++ +getbyte +store 66 +load 63 +int 1 +int 2 +* +int 2 ++ +getbyte +store 65 +int 3 +store 68 +load 68 +itob +extract 6 0 +byte 0x00 +int 0 +load 65 +setbyte +byte 0x00 +int 0 +load 66 +setbyte +concat +byte 0x00 +int 0 +load 67 +setbyte +concat +concat +store 64 +load 64 +retsub + +// tuple_complement +tuplecomplement_8: +store 69 +load 69 +extract 0 32 +store 19 +load 69 +load 69 +int 32 +extract_uint16 +load 69 +int 34 +extract_uint16 +substring3 +store 20 +load 69 +load 69 +int 34 +extract_uint16 +load 69 +int 36 +extract_uint16 +substring3 +store 21 +load 69 +load 69 +int 36 +extract_uint16 +dig 1 +len +substring3 +store 22 +load 19 +callsub arraycomplement_10 +store 19 +load 20 +callsub arraycomplement_12 +store 20 +load 21 +callsub stringreverse_13 +store 21 +load 22 +callsub arraycomplement_15 +store 22 +load 19 +load 20 +store 176 +load 176 +store 175 +int 38 +store 173 +load 173 +load 176 +len ++ +store 174 +load 174 +int 65536 +< +assert +load 173 +itob +extract 6 0 +concat +load 21 +store 176 +load 175 +load 176 +concat +store 175 +load 174 +store 173 +load 173 +load 176 +len ++ +store 174 +load 174 +int 65536 +< +assert +load 173 +itob +extract 6 0 +concat +load 22 +store 176 +load 175 +load 176 +concat +store 175 +load 174 +store 173 +load 173 +itob +extract 6 0 +concat +load 175 +concat +store 70 +load 70 +retsub + +// numerical_comp +numericalcomp_9: +store 110 +int 255 +load 110 +- +store 111 +load 111 +int 256 +< +assert +load 111 +retsub + +// array_complement +arraycomplement_10: +store 76 +load 76 +int 1 +int 0 +* +getbyte +store 78 +load 76 +int 1 +int 1 +* +getbyte +store 79 +load 76 +int 1 +int 2 +* +getbyte +store 80 +load 76 +int 1 +int 3 +* +getbyte +store 81 +load 76 +int 1 +int 4 +* +getbyte +store 82 +load 76 +int 1 +int 5 +* +getbyte +store 83 +load 76 +int 1 +int 6 +* +getbyte +store 84 +load 76 +int 1 +int 7 +* +getbyte +store 85 +load 76 +int 1 +int 8 +* +getbyte +store 86 +load 76 +int 1 +int 9 +* +getbyte +store 87 +load 76 +int 1 +int 10 +* +getbyte +store 88 +load 76 +int 1 +int 11 +* +getbyte +store 89 +load 76 +int 1 +int 12 +* +getbyte +store 90 +load 76 +int 1 +int 13 +* +getbyte +store 91 +load 76 +int 1 +int 14 +* +getbyte +store 92 +load 76 +int 1 +int 15 +* +getbyte +store 93 +load 76 +int 1 +int 16 +* +getbyte +store 94 +load 76 +int 1 +int 17 +* +getbyte +store 95 +load 76 +int 1 +int 18 +* +getbyte +store 96 +load 76 +int 1 +int 19 +* +getbyte +store 97 +load 76 +int 1 +int 20 +* +getbyte +store 98 +load 76 +int 1 +int 21 +* +getbyte +store 99 +load 76 +int 1 +int 22 +* +getbyte +store 100 +load 76 +int 1 +int 23 +* +getbyte +store 101 +load 76 +int 1 +int 24 +* +getbyte +store 102 +load 76 +int 1 +int 25 +* +getbyte +store 103 +load 76 +int 1 +int 26 +* +getbyte +store 104 +load 76 +int 1 +int 27 +* +getbyte +store 105 +load 76 +int 1 +int 28 +* +getbyte +store 106 +load 76 +int 1 +int 29 +* +getbyte +store 107 +load 76 +int 1 +int 30 +* +getbyte +store 108 +load 76 +int 1 +int 31 +* +getbyte +store 109 +load 78 +callsub numericalcomp_9 +store 78 +load 79 +callsub numericalcomp_9 +store 79 +load 80 +callsub numericalcomp_9 +store 80 +load 81 +callsub numericalcomp_9 +store 81 +load 82 +callsub numericalcomp_9 +store 82 +load 83 +callsub numericalcomp_9 +store 83 +load 84 +callsub numericalcomp_9 +store 84 +load 85 +callsub numericalcomp_9 +store 85 +load 86 +callsub numericalcomp_9 +store 86 +load 87 +callsub numericalcomp_9 +store 87 +load 88 +callsub numericalcomp_9 +store 88 +load 89 +callsub numericalcomp_9 +store 89 +load 90 +callsub numericalcomp_9 +store 90 +load 91 +callsub numericalcomp_9 +store 91 +load 92 +callsub numericalcomp_9 +store 92 +load 93 +callsub numericalcomp_9 +store 93 +load 94 +callsub numericalcomp_9 +store 94 +load 95 +callsub numericalcomp_9 +store 95 +load 96 +callsub numericalcomp_9 +store 96 +load 97 +callsub numericalcomp_9 +store 97 +load 98 +callsub numericalcomp_9 +store 98 +load 99 +callsub numericalcomp_9 +store 99 +load 100 +callsub numericalcomp_9 +store 100 +load 101 +callsub numericalcomp_9 +store 101 +load 102 +callsub numericalcomp_9 +store 102 +load 103 +callsub numericalcomp_9 +store 103 +load 104 +callsub numericalcomp_9 +store 104 +load 105 +callsub numericalcomp_9 +store 105 +load 106 +callsub numericalcomp_9 +store 106 +load 107 +callsub numericalcomp_9 +store 107 +load 108 +callsub numericalcomp_9 +store 108 +load 109 +callsub numericalcomp_9 +store 109 +byte 0x00 +int 0 +load 78 +setbyte +byte 0x00 +int 0 +load 79 +setbyte +concat +byte 0x00 +int 0 +load 80 +setbyte +concat +byte 0x00 +int 0 +load 81 +setbyte +concat +byte 0x00 +int 0 +load 82 +setbyte +concat +byte 0x00 +int 0 +load 83 +setbyte +concat +byte 0x00 +int 0 +load 84 +setbyte +concat +byte 0x00 +int 0 +load 85 +setbyte +concat +byte 0x00 +int 0 +load 86 +setbyte +concat +byte 0x00 +int 0 +load 87 +setbyte +concat +byte 0x00 +int 0 +load 88 +setbyte +concat +byte 0x00 +int 0 +load 89 +setbyte +concat +byte 0x00 +int 0 +load 90 +setbyte +concat +byte 0x00 +int 0 +load 91 +setbyte +concat +byte 0x00 +int 0 +load 92 +setbyte +concat +byte 0x00 +int 0 +load 93 +setbyte +concat +byte 0x00 +int 0 +load 94 +setbyte +concat +byte 0x00 +int 0 +load 95 +setbyte +concat +byte 0x00 +int 0 +load 96 +setbyte +concat +byte 0x00 +int 0 +load 97 +setbyte +concat +byte 0x00 +int 0 +load 98 +setbyte +concat +byte 0x00 +int 0 +load 99 +setbyte +concat +byte 0x00 +int 0 +load 100 +setbyte +concat +byte 0x00 +int 0 +load 101 +setbyte +concat +byte 0x00 +int 0 +load 102 +setbyte +concat +byte 0x00 +int 0 +load 103 +setbyte +concat +byte 0x00 +int 0 +load 104 +setbyte +concat +byte 0x00 +int 0 +load 105 +setbyte +concat +byte 0x00 +int 0 +load 106 +setbyte +concat +byte 0x00 +int 0 +load 107 +setbyte +concat +byte 0x00 +int 0 +load 108 +setbyte +concat +byte 0x00 +int 0 +load 109 +setbyte +concat +store 77 +load 77 +retsub + +// tuple_complement +tuplecomplement_11: +store 116 +load 116 +int 0 +extract_uint32 +store 71 +load 116 +load 116 +int 4 +extract_uint16 +dig 1 +len +substring3 +store 72 +load 116 +extract 6 1 +store 73 +load 116 +extract 7 1 +store 74 +load 116 +int 8 +getbyte +store 75 +load 71 +callsub numericalcomp_16 +store 71 +load 72 +callsub arraycomplement_18 +store 72 +load 73 +callsub arraycomplement_20 +store 73 +load 74 +callsub tuplecomplement_21 +store 74 +load 75 +callsub numericalcomp_22 +store 75 +load 71 +itob +extract 4 0 +load 72 +store 154 +load 154 +store 153 +int 9 +store 152 +load 152 +itob +extract 6 0 +concat +load 73 +concat +load 74 +concat +byte 0x00 +int 0 +load 75 +setbyte +concat +load 153 +concat +store 117 +load 117 +retsub + +// array_complement +arraycomplement_12: +store 112 +load 112 +load 112 +int 2 +int 0 +* +extract_uint16 +int 0 +int 1 ++ +int 2 +== +bnz arraycomplement_12_l5 +load 112 +int 2 +int 0 +* +int 2 ++ +extract_uint16 +arraycomplement_12_l2: +substring3 +store 114 +load 112 +load 112 +int 2 +int 1 +* +extract_uint16 +int 1 +int 1 ++ +int 2 +== +bnz arraycomplement_12_l4 +load 112 +int 2 +int 1 +* +int 2 ++ +extract_uint16 +b arraycomplement_12_l6 +arraycomplement_12_l4: +load 112 +len +b arraycomplement_12_l6 +arraycomplement_12_l5: +load 112 +len +b arraycomplement_12_l2 +arraycomplement_12_l6: +substring3 +store 115 +load 114 +callsub tuplecomplement_11 +store 114 +load 115 +callsub tuplecomplement_11 +store 115 +load 114 +store 158 +load 158 +store 157 +int 4 +store 155 +load 155 +load 158 +len ++ +store 156 +load 156 +int 65536 +< +assert +load 155 +itob +extract 6 0 +load 115 +store 158 +load 157 +load 158 +concat +store 157 +load 156 +store 155 +load 155 +itob +extract 6 0 +concat +load 157 +concat +store 113 +load 113 +retsub + +// string_reverse +stringreverse_13: +store 159 +load 159 +int 1 +int 0 +* +int 2 ++ +getbyte +store 163 +load 159 +int 1 +int 1 +* +int 2 ++ +getbyte +store 162 +load 159 +int 1 +int 2 +* +int 2 ++ +getbyte +store 161 +int 3 +store 164 +load 164 +itob +extract 6 0 +byte 0x00 +int 0 +load 161 +setbyte +byte 0x00 +int 0 +load 162 +setbyte +concat +byte 0x00 +int 0 +load 163 +setbyte +concat +concat +store 160 +load 160 +retsub + +// bool_comp +boolcomp_14: +store 170 +load 170 +! +! +! +store 171 +load 171 +retsub + +// array_complement +arraycomplement_15: +store 165 +load 165 +int 0 +int 16 ++ +getbit +store 167 +load 165 +int 1 +int 16 ++ +getbit +store 168 +load 165 +int 2 +int 16 ++ +getbit +store 169 +load 167 +callsub boolcomp_14 +store 167 +load 168 +callsub boolcomp_14 +store 168 +load 169 +callsub boolcomp_14 +store 169 +int 3 +store 172 +load 172 +itob +extract 6 0 +byte 0x00 +int 0 +load 167 +setbit +int 1 +load 168 +setbit +int 2 +load 169 +setbit +concat +store 166 +load 166 +retsub + +// numerical_comp +numericalcomp_16: +store 119 +int 4294967295 +load 119 +- +store 120 +load 120 +int 4294967296 +< +assert +load 120 +retsub + +// string_reverse +stringreverse_17: +store 129 +load 129 +int 1 +int 0 +* +int 2 ++ +getbyte +store 133 +load 129 +int 1 +int 1 +* +int 2 ++ +getbyte +store 132 +load 129 +int 1 +int 2 +* +int 2 ++ +getbyte +store 131 +int 3 +store 134 +load 134 +itob +extract 6 0 +byte 0x00 +int 0 +load 131 +setbyte +byte 0x00 +int 0 +load 132 +setbyte +concat +byte 0x00 +int 0 +load 133 +setbyte +concat +concat +store 130 +load 130 +retsub + +// array_complement +arraycomplement_18: +store 121 +load 121 +load 121 +int 2 +int 0 +* +int 2 ++ +extract_uint16 +int 2 ++ +int 0 +int 1 ++ +load 121 +int 0 +extract_uint16 +store 126 +load 126 +== +bnz arraycomplement_18_l8 +load 121 +int 2 +int 0 +* +int 2 ++ +int 2 ++ +extract_uint16 +int 2 ++ +arraycomplement_18_l2: +substring3 +store 123 +load 121 +load 121 +int 2 +int 1 +* +int 2 ++ +extract_uint16 +int 2 ++ +int 1 +int 1 ++ +load 121 +int 0 +extract_uint16 +store 127 +load 127 +== +bnz arraycomplement_18_l7 +load 121 +int 2 +int 1 +* +int 2 ++ +int 2 ++ +extract_uint16 +int 2 ++ +arraycomplement_18_l4: +substring3 +store 124 +load 121 +load 121 +int 2 +int 2 +* +int 2 ++ +extract_uint16 +int 2 ++ +int 2 +int 1 ++ +load 121 +int 0 +extract_uint16 +store 128 +load 128 +== +bnz arraycomplement_18_l6 +load 121 +int 2 +int 2 +* +int 2 ++ +int 2 ++ +extract_uint16 +int 2 ++ +b arraycomplement_18_l9 +arraycomplement_18_l6: +load 121 +len +b arraycomplement_18_l9 +arraycomplement_18_l7: +load 121 +len +b arraycomplement_18_l4 +arraycomplement_18_l8: +load 121 +len +b arraycomplement_18_l2 +arraycomplement_18_l9: +substring3 +store 125 +load 123 +callsub stringreverse_17 +store 123 +load 124 +callsub stringreverse_17 +store 124 +load 125 +callsub stringreverse_17 +store 125 +int 3 +store 139 +load 139 +itob +extract 6 0 +load 123 +store 138 +load 138 +store 137 +int 6 +store 135 +load 135 +load 138 +len ++ +store 136 +load 136 +int 65536 +< +assert +load 135 +itob +extract 6 0 +load 124 +store 138 +load 137 +load 138 +concat +store 137 +load 136 +store 135 +load 135 +load 138 +len ++ +store 136 +load 136 +int 65536 +< +assert +load 135 +itob +extract 6 0 +concat +load 125 +store 138 +load 137 +load 138 +concat +store 137 +load 136 +store 135 +load 135 +itob +extract 6 0 +concat +load 137 +concat +concat +store 122 +load 122 +retsub + +// bool_comp +boolcomp_19: +store 144 +load 144 +! +! +! +store 145 +load 145 +retsub + +// array_complement +arraycomplement_20: +store 140 +load 140 +int 0 +getbit +store 142 +load 140 +int 1 +getbit +store 143 +load 142 +callsub boolcomp_19 +store 142 +load 143 +callsub boolcomp_19 +store 143 +byte 0x00 +int 0 +load 142 +setbit +int 1 +load 143 +setbit +store 141 +load 141 +retsub + +// tuple_complement +tuplecomplement_21: +store 146 +load 146 +int 0 +getbyte +store 118 +load 118 +callsub numericalcomp_23 +store 118 +byte 0x00 +int 0 +load 118 +setbyte +store 147 +load 147 +retsub + +// numerical_comp +numericalcomp_22: +store 150 +int 255 +load 150 +- +store 151 +load 151 +int 256 +< +assert +load 151 +retsub + +// numerical_comp +numericalcomp_23: +store 148 +int 255 +load 148 +- +store 149 +load 149 +int 256 +< +assert +load 149 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,uint64).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,uint64).teal new file mode 100644 index 000000000..ea4a6ef9b --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,byte,address,string,uint64).teal @@ -0,0 +1,703 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 6 +load 6 +callsub roundtripper_1 +store 5 +byte 0x151f7c75 +load 5 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 11 +load 11 +int 0 +getbit +store 0 +load 11 +int 1 +getbyte +store 1 +load 11 +extract 2 32 +store 2 +load 11 +load 11 +int 34 +extract_uint16 +dig 1 +len +substring3 +store 3 +load 11 +int 36 +extract_uint64 +store 4 +load 0 +callsub boolcomp_2 +store 0 +load 1 +callsub numericalcomp_3 +store 1 +load 2 +callsub arraycomplement_5 +store 2 +load 3 +callsub stringreverse_6 +store 3 +load 4 +callsub numericalcomp_7 +store 4 +byte 0x00 +int 0 +load 0 +setbit +byte 0x00 +int 0 +load 1 +setbyte +concat +load 2 +concat +load 3 +store 63 +load 63 +store 62 +int 44 +store 61 +load 61 +itob +extract 6 0 +concat +load 4 +itob +concat +load 62 +concat +store 12 +load 12 +retsub + +// round_tripper +roundtripper_1: +store 7 +load 7 +callsub tuplecomplement_0 +store 9 +load 9 +callsub tuplecomplement_0 +store 10 +load 7 +store 67 +load 67 +store 66 +int 6 +store 64 +load 64 +load 67 +len ++ +store 65 +load 65 +int 65536 +< +assert +load 64 +itob +extract 6 0 +load 9 +store 67 +load 66 +load 67 +concat +store 66 +load 65 +store 64 +load 64 +load 67 +len ++ +store 65 +load 65 +int 65536 +< +assert +load 64 +itob +extract 6 0 +concat +load 10 +store 67 +load 66 +load 67 +concat +store 66 +load 65 +store 64 +load 64 +itob +extract 6 0 +concat +load 66 +concat +store 8 +load 8 +retsub + +// bool_comp +boolcomp_2: +store 13 +load 13 +! +! +! +store 14 +load 14 +retsub + +// numerical_comp +numericalcomp_3: +store 15 +int 255 +load 15 +- +store 16 +load 16 +int 256 +< +assert +load 16 +retsub + +// numerical_comp +numericalcomp_4: +store 51 +int 255 +load 51 +- +store 52 +load 52 +int 256 +< +assert +load 52 +retsub + +// array_complement +arraycomplement_5: +store 17 +load 17 +int 1 +int 0 +* +getbyte +store 19 +load 17 +int 1 +int 1 +* +getbyte +store 20 +load 17 +int 1 +int 2 +* +getbyte +store 21 +load 17 +int 1 +int 3 +* +getbyte +store 22 +load 17 +int 1 +int 4 +* +getbyte +store 23 +load 17 +int 1 +int 5 +* +getbyte +store 24 +load 17 +int 1 +int 6 +* +getbyte +store 25 +load 17 +int 1 +int 7 +* +getbyte +store 26 +load 17 +int 1 +int 8 +* +getbyte +store 27 +load 17 +int 1 +int 9 +* +getbyte +store 28 +load 17 +int 1 +int 10 +* +getbyte +store 29 +load 17 +int 1 +int 11 +* +getbyte +store 30 +load 17 +int 1 +int 12 +* +getbyte +store 31 +load 17 +int 1 +int 13 +* +getbyte +store 32 +load 17 +int 1 +int 14 +* +getbyte +store 33 +load 17 +int 1 +int 15 +* +getbyte +store 34 +load 17 +int 1 +int 16 +* +getbyte +store 35 +load 17 +int 1 +int 17 +* +getbyte +store 36 +load 17 +int 1 +int 18 +* +getbyte +store 37 +load 17 +int 1 +int 19 +* +getbyte +store 38 +load 17 +int 1 +int 20 +* +getbyte +store 39 +load 17 +int 1 +int 21 +* +getbyte +store 40 +load 17 +int 1 +int 22 +* +getbyte +store 41 +load 17 +int 1 +int 23 +* +getbyte +store 42 +load 17 +int 1 +int 24 +* +getbyte +store 43 +load 17 +int 1 +int 25 +* +getbyte +store 44 +load 17 +int 1 +int 26 +* +getbyte +store 45 +load 17 +int 1 +int 27 +* +getbyte +store 46 +load 17 +int 1 +int 28 +* +getbyte +store 47 +load 17 +int 1 +int 29 +* +getbyte +store 48 +load 17 +int 1 +int 30 +* +getbyte +store 49 +load 17 +int 1 +int 31 +* +getbyte +store 50 +load 19 +callsub numericalcomp_4 +store 19 +load 20 +callsub numericalcomp_4 +store 20 +load 21 +callsub numericalcomp_4 +store 21 +load 22 +callsub numericalcomp_4 +store 22 +load 23 +callsub numericalcomp_4 +store 23 +load 24 +callsub numericalcomp_4 +store 24 +load 25 +callsub numericalcomp_4 +store 25 +load 26 +callsub numericalcomp_4 +store 26 +load 27 +callsub numericalcomp_4 +store 27 +load 28 +callsub numericalcomp_4 +store 28 +load 29 +callsub numericalcomp_4 +store 29 +load 30 +callsub numericalcomp_4 +store 30 +load 31 +callsub numericalcomp_4 +store 31 +load 32 +callsub numericalcomp_4 +store 32 +load 33 +callsub numericalcomp_4 +store 33 +load 34 +callsub numericalcomp_4 +store 34 +load 35 +callsub numericalcomp_4 +store 35 +load 36 +callsub numericalcomp_4 +store 36 +load 37 +callsub numericalcomp_4 +store 37 +load 38 +callsub numericalcomp_4 +store 38 +load 39 +callsub numericalcomp_4 +store 39 +load 40 +callsub numericalcomp_4 +store 40 +load 41 +callsub numericalcomp_4 +store 41 +load 42 +callsub numericalcomp_4 +store 42 +load 43 +callsub numericalcomp_4 +store 43 +load 44 +callsub numericalcomp_4 +store 44 +load 45 +callsub numericalcomp_4 +store 45 +load 46 +callsub numericalcomp_4 +store 46 +load 47 +callsub numericalcomp_4 +store 47 +load 48 +callsub numericalcomp_4 +store 48 +load 49 +callsub numericalcomp_4 +store 49 +load 50 +callsub numericalcomp_4 +store 50 +byte 0x00 +int 0 +load 19 +setbyte +byte 0x00 +int 0 +load 20 +setbyte +concat +byte 0x00 +int 0 +load 21 +setbyte +concat +byte 0x00 +int 0 +load 22 +setbyte +concat +byte 0x00 +int 0 +load 23 +setbyte +concat +byte 0x00 +int 0 +load 24 +setbyte +concat +byte 0x00 +int 0 +load 25 +setbyte +concat +byte 0x00 +int 0 +load 26 +setbyte +concat +byte 0x00 +int 0 +load 27 +setbyte +concat +byte 0x00 +int 0 +load 28 +setbyte +concat +byte 0x00 +int 0 +load 29 +setbyte +concat +byte 0x00 +int 0 +load 30 +setbyte +concat +byte 0x00 +int 0 +load 31 +setbyte +concat +byte 0x00 +int 0 +load 32 +setbyte +concat +byte 0x00 +int 0 +load 33 +setbyte +concat +byte 0x00 +int 0 +load 34 +setbyte +concat +byte 0x00 +int 0 +load 35 +setbyte +concat +byte 0x00 +int 0 +load 36 +setbyte +concat +byte 0x00 +int 0 +load 37 +setbyte +concat +byte 0x00 +int 0 +load 38 +setbyte +concat +byte 0x00 +int 0 +load 39 +setbyte +concat +byte 0x00 +int 0 +load 40 +setbyte +concat +byte 0x00 +int 0 +load 41 +setbyte +concat +byte 0x00 +int 0 +load 42 +setbyte +concat +byte 0x00 +int 0 +load 43 +setbyte +concat +byte 0x00 +int 0 +load 44 +setbyte +concat +byte 0x00 +int 0 +load 45 +setbyte +concat +byte 0x00 +int 0 +load 46 +setbyte +concat +byte 0x00 +int 0 +load 47 +setbyte +concat +byte 0x00 +int 0 +load 48 +setbyte +concat +byte 0x00 +int 0 +load 49 +setbyte +concat +byte 0x00 +int 0 +load 50 +setbyte +concat +store 18 +load 18 +retsub + +// string_reverse +stringreverse_6: +store 53 +load 53 +int 1 +int 0 +* +int 2 ++ +getbyte +store 57 +load 53 +int 1 +int 1 +* +int 2 ++ +getbyte +store 56 +load 53 +int 1 +int 2 +* +int 2 ++ +getbyte +store 55 +int 3 +store 58 +load 58 +itob +extract 6 0 +byte 0x00 +int 0 +load 55 +setbyte +byte 0x00 +int 0 +load 56 +setbyte +concat +byte 0x00 +int 0 +load 57 +setbyte +concat +concat +store 54 +load 54 +retsub + +// numerical_comp +numericalcomp_7: +store 59 +int 18446744073709551615 +load 59 +- +store 60 +load 60 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(bool,uint64,uint32).teal b/tests/integration/teal/roundtrip/app_roundtrip_(bool,uint64,uint32).teal new file mode 100644 index 000000000..0f9c956b2 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(bool,uint64,uint32).teal @@ -0,0 +1,104 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 4 +load 4 +callsub roundtripper_1 +store 3 +byte 0x151f7c75 +load 3 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 9 +load 9 +int 0 +getbit +store 0 +load 9 +int 1 +extract_uint64 +store 1 +load 9 +int 9 +extract_uint32 +store 2 +load 0 +callsub boolcomp_2 +store 0 +load 1 +callsub numericalcomp_3 +store 1 +load 2 +callsub numericalcomp_4 +store 2 +byte 0x00 +int 0 +load 0 +setbit +load 1 +itob +concat +load 2 +itob +extract 4 0 +concat +store 10 +load 10 +retsub + +// round_tripper +roundtripper_1: +store 5 +load 5 +callsub tuplecomplement_0 +store 7 +load 7 +callsub tuplecomplement_0 +store 8 +load 5 +load 7 +concat +load 8 +concat +store 6 +load 6 +retsub + +// bool_comp +boolcomp_2: +store 11 +load 11 +! +! +! +store 12 +load 12 +retsub + +// numerical_comp +numericalcomp_3: +store 13 +int 18446744073709551615 +load 13 +- +store 14 +load 14 +retsub + +// numerical_comp +numericalcomp_4: +store 15 +int 4294967295 +load 15 +- +store 16 +load 16 +int 4294967296 +< +assert +load 16 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(byte).teal b/tests/integration/teal/roundtrip/app_roundtrip_(byte).teal new file mode 100644 index 000000000..4e2b05add --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(byte).teal @@ -0,0 +1,62 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 2 +load 2 +callsub roundtripper_1 +store 1 +byte 0x151f7c75 +load 1 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 7 +load 7 +int 0 +getbyte +store 0 +load 0 +callsub numericalcomp_2 +store 0 +byte 0x00 +int 0 +load 0 +setbyte +store 8 +load 8 +retsub + +// round_tripper +roundtripper_1: +store 3 +load 3 +callsub tuplecomplement_0 +store 5 +load 5 +callsub tuplecomplement_0 +store 6 +load 3 +load 5 +concat +load 6 +concat +store 4 +load 4 +retsub + +// numerical_comp +numericalcomp_2: +store 9 +int 255 +load 9 +- +store 10 +load 10 +int 256 +< +assert +load 10 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(byte,bool,uint64).teal b/tests/integration/teal/roundtrip/app_roundtrip_(byte,bool,uint64).teal new file mode 100644 index 000000000..acbad3d03 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(byte,bool,uint64).teal @@ -0,0 +1,105 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 4 +load 4 +callsub roundtripper_1 +store 3 +byte 0x151f7c75 +load 3 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 9 +load 9 +int 0 +getbyte +store 0 +load 9 +int 8 +getbit +store 1 +load 9 +int 2 +extract_uint64 +store 2 +load 0 +callsub numericalcomp_2 +store 0 +load 1 +callsub boolcomp_3 +store 1 +load 2 +callsub numericalcomp_4 +store 2 +byte 0x00 +int 0 +load 0 +setbyte +byte 0x00 +int 0 +load 1 +setbit +concat +load 2 +itob +concat +store 10 +load 10 +retsub + +// round_tripper +roundtripper_1: +store 5 +load 5 +callsub tuplecomplement_0 +store 7 +load 7 +callsub tuplecomplement_0 +store 8 +load 5 +load 7 +concat +load 8 +concat +store 6 +load 6 +retsub + +// numerical_comp +numericalcomp_2: +store 11 +int 255 +load 11 +- +store 12 +load 12 +int 256 +< +assert +load 12 +retsub + +// bool_comp +boolcomp_3: +store 13 +load 13 +! +! +! +store 14 +load 14 +retsub + +// numerical_comp +numericalcomp_4: +store 15 +int 18446744073709551615 +load 15 +- +store 16 +load 16 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(byte[4],(bool,bool),uint64,address)[]_<7>.teal b/tests/integration/teal/roundtrip/app_roundtrip_(byte[4],(bool,bool),uint64,address)[]_<7>.teal new file mode 100644 index 000000000..6e7ce0eb3 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(byte[4],(bool,bool),uint64,address)[]_<7>.teal @@ -0,0 +1,832 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 5 +load 5 +callsub roundtripper_2 +store 4 +byte 0x151f7c75 +load 4 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 19 +load 19 +extract 0 4 +store 0 +load 19 +extract 4 1 +store 1 +load 19 +int 5 +extract_uint64 +store 2 +load 19 +extract 13 0 +store 3 +load 0 +callsub arraycomplement_4 +store 0 +load 1 +callsub tuplecomplement_5 +store 1 +load 2 +callsub numericalcomp_6 +store 2 +load 3 +callsub arraycomplement_8 +store 3 +load 0 +load 1 +concat +load 2 +itob +concat +load 3 +concat +store 20 +load 20 +retsub + +// array_complement +arraycomplement_1: +store 10 +load 10 +int 45 +int 0 +* +int 2 ++ +int 45 +extract3 +store 12 +load 10 +int 45 +int 1 +* +int 2 ++ +int 45 +extract3 +store 13 +load 10 +int 45 +int 2 +* +int 2 ++ +int 45 +extract3 +store 14 +load 10 +int 45 +int 3 +* +int 2 ++ +int 45 +extract3 +store 15 +load 10 +int 45 +int 4 +* +int 2 ++ +int 45 +extract3 +store 16 +load 10 +int 45 +int 5 +* +int 2 ++ +int 45 +extract3 +store 17 +load 10 +int 45 +int 6 +* +int 2 ++ +int 45 +extract3 +store 18 +load 12 +callsub tuplecomplement_0 +store 12 +load 13 +callsub tuplecomplement_0 +store 13 +load 14 +callsub tuplecomplement_0 +store 14 +load 15 +callsub tuplecomplement_0 +store 15 +load 16 +callsub tuplecomplement_0 +store 16 +load 17 +callsub tuplecomplement_0 +store 17 +load 18 +callsub tuplecomplement_0 +store 18 +int 7 +store 75 +load 75 +itob +extract 6 0 +load 12 +load 13 +concat +load 14 +concat +load 15 +concat +load 16 +concat +load 17 +concat +load 18 +concat +concat +store 11 +load 11 +retsub + +// round_tripper +roundtripper_2: +store 6 +load 6 +callsub arraycomplement_1 +store 8 +load 8 +callsub arraycomplement_1 +store 9 +load 6 +store 79 +load 79 +store 78 +int 6 +store 76 +load 76 +load 79 +len ++ +store 77 +load 77 +int 65536 +< +assert +load 76 +itob +extract 6 0 +load 8 +store 79 +load 78 +load 79 +concat +store 78 +load 77 +store 76 +load 76 +load 79 +len ++ +store 77 +load 77 +int 65536 +< +assert +load 76 +itob +extract 6 0 +concat +load 9 +store 79 +load 78 +load 79 +concat +store 78 +load 77 +store 76 +load 76 +itob +extract 6 0 +concat +load 78 +concat +store 7 +load 7 +retsub + +// numerical_comp +numericalcomp_3: +store 29 +int 255 +load 29 +- +store 30 +load 30 +int 256 +< +assert +load 30 +retsub + +// array_complement +arraycomplement_4: +store 23 +load 23 +int 1 +int 0 +* +getbyte +store 25 +load 23 +int 1 +int 1 +* +getbyte +store 26 +load 23 +int 1 +int 2 +* +getbyte +store 27 +load 23 +int 1 +int 3 +* +getbyte +store 28 +load 25 +callsub numericalcomp_3 +store 25 +load 26 +callsub numericalcomp_3 +store 26 +load 27 +callsub numericalcomp_3 +store 27 +load 28 +callsub numericalcomp_3 +store 28 +byte 0x00 +int 0 +load 25 +setbyte +byte 0x00 +int 0 +load 26 +setbyte +concat +byte 0x00 +int 0 +load 27 +setbyte +concat +byte 0x00 +int 0 +load 28 +setbyte +concat +store 24 +load 24 +retsub + +// tuple_complement +tuplecomplement_5: +store 31 +load 31 +int 0 +getbit +store 21 +load 31 +int 1 +getbit +store 22 +load 21 +callsub boolcomp_9 +store 21 +load 22 +callsub boolcomp_10 +store 22 +byte 0x00 +int 0 +load 21 +setbit +int 1 +load 22 +setbit +store 32 +load 32 +retsub + +// numerical_comp +numericalcomp_6: +store 37 +int 18446744073709551615 +load 37 +- +store 38 +load 38 +retsub + +// numerical_comp +numericalcomp_7: +store 73 +int 255 +load 73 +- +store 74 +load 74 +int 256 +< +assert +load 74 +retsub + +// array_complement +arraycomplement_8: +store 39 +load 39 +int 1 +int 0 +* +getbyte +store 41 +load 39 +int 1 +int 1 +* +getbyte +store 42 +load 39 +int 1 +int 2 +* +getbyte +store 43 +load 39 +int 1 +int 3 +* +getbyte +store 44 +load 39 +int 1 +int 4 +* +getbyte +store 45 +load 39 +int 1 +int 5 +* +getbyte +store 46 +load 39 +int 1 +int 6 +* +getbyte +store 47 +load 39 +int 1 +int 7 +* +getbyte +store 48 +load 39 +int 1 +int 8 +* +getbyte +store 49 +load 39 +int 1 +int 9 +* +getbyte +store 50 +load 39 +int 1 +int 10 +* +getbyte +store 51 +load 39 +int 1 +int 11 +* +getbyte +store 52 +load 39 +int 1 +int 12 +* +getbyte +store 53 +load 39 +int 1 +int 13 +* +getbyte +store 54 +load 39 +int 1 +int 14 +* +getbyte +store 55 +load 39 +int 1 +int 15 +* +getbyte +store 56 +load 39 +int 1 +int 16 +* +getbyte +store 57 +load 39 +int 1 +int 17 +* +getbyte +store 58 +load 39 +int 1 +int 18 +* +getbyte +store 59 +load 39 +int 1 +int 19 +* +getbyte +store 60 +load 39 +int 1 +int 20 +* +getbyte +store 61 +load 39 +int 1 +int 21 +* +getbyte +store 62 +load 39 +int 1 +int 22 +* +getbyte +store 63 +load 39 +int 1 +int 23 +* +getbyte +store 64 +load 39 +int 1 +int 24 +* +getbyte +store 65 +load 39 +int 1 +int 25 +* +getbyte +store 66 +load 39 +int 1 +int 26 +* +getbyte +store 67 +load 39 +int 1 +int 27 +* +getbyte +store 68 +load 39 +int 1 +int 28 +* +getbyte +store 69 +load 39 +int 1 +int 29 +* +getbyte +store 70 +load 39 +int 1 +int 30 +* +getbyte +store 71 +load 39 +int 1 +int 31 +* +getbyte +store 72 +load 41 +callsub numericalcomp_7 +store 41 +load 42 +callsub numericalcomp_7 +store 42 +load 43 +callsub numericalcomp_7 +store 43 +load 44 +callsub numericalcomp_7 +store 44 +load 45 +callsub numericalcomp_7 +store 45 +load 46 +callsub numericalcomp_7 +store 46 +load 47 +callsub numericalcomp_7 +store 47 +load 48 +callsub numericalcomp_7 +store 48 +load 49 +callsub numericalcomp_7 +store 49 +load 50 +callsub numericalcomp_7 +store 50 +load 51 +callsub numericalcomp_7 +store 51 +load 52 +callsub numericalcomp_7 +store 52 +load 53 +callsub numericalcomp_7 +store 53 +load 54 +callsub numericalcomp_7 +store 54 +load 55 +callsub numericalcomp_7 +store 55 +load 56 +callsub numericalcomp_7 +store 56 +load 57 +callsub numericalcomp_7 +store 57 +load 58 +callsub numericalcomp_7 +store 58 +load 59 +callsub numericalcomp_7 +store 59 +load 60 +callsub numericalcomp_7 +store 60 +load 61 +callsub numericalcomp_7 +store 61 +load 62 +callsub numericalcomp_7 +store 62 +load 63 +callsub numericalcomp_7 +store 63 +load 64 +callsub numericalcomp_7 +store 64 +load 65 +callsub numericalcomp_7 +store 65 +load 66 +callsub numericalcomp_7 +store 66 +load 67 +callsub numericalcomp_7 +store 67 +load 68 +callsub numericalcomp_7 +store 68 +load 69 +callsub numericalcomp_7 +store 69 +load 70 +callsub numericalcomp_7 +store 70 +load 71 +callsub numericalcomp_7 +store 71 +load 72 +callsub numericalcomp_7 +store 72 +byte 0x00 +int 0 +load 41 +setbyte +byte 0x00 +int 0 +load 42 +setbyte +concat +byte 0x00 +int 0 +load 43 +setbyte +concat +byte 0x00 +int 0 +load 44 +setbyte +concat +byte 0x00 +int 0 +load 45 +setbyte +concat +byte 0x00 +int 0 +load 46 +setbyte +concat +byte 0x00 +int 0 +load 47 +setbyte +concat +byte 0x00 +int 0 +load 48 +setbyte +concat +byte 0x00 +int 0 +load 49 +setbyte +concat +byte 0x00 +int 0 +load 50 +setbyte +concat +byte 0x00 +int 0 +load 51 +setbyte +concat +byte 0x00 +int 0 +load 52 +setbyte +concat +byte 0x00 +int 0 +load 53 +setbyte +concat +byte 0x00 +int 0 +load 54 +setbyte +concat +byte 0x00 +int 0 +load 55 +setbyte +concat +byte 0x00 +int 0 +load 56 +setbyte +concat +byte 0x00 +int 0 +load 57 +setbyte +concat +byte 0x00 +int 0 +load 58 +setbyte +concat +byte 0x00 +int 0 +load 59 +setbyte +concat +byte 0x00 +int 0 +load 60 +setbyte +concat +byte 0x00 +int 0 +load 61 +setbyte +concat +byte 0x00 +int 0 +load 62 +setbyte +concat +byte 0x00 +int 0 +load 63 +setbyte +concat +byte 0x00 +int 0 +load 64 +setbyte +concat +byte 0x00 +int 0 +load 65 +setbyte +concat +byte 0x00 +int 0 +load 66 +setbyte +concat +byte 0x00 +int 0 +load 67 +setbyte +concat +byte 0x00 +int 0 +load 68 +setbyte +concat +byte 0x00 +int 0 +load 69 +setbyte +concat +byte 0x00 +int 0 +load 70 +setbyte +concat +byte 0x00 +int 0 +load 71 +setbyte +concat +byte 0x00 +int 0 +load 72 +setbyte +concat +store 40 +load 40 +retsub + +// bool_comp +boolcomp_9: +store 33 +load 33 +! +! +! +store 34 +load 34 +retsub + +// bool_comp +boolcomp_10: +store 35 +load 35 +! +! +! +store 36 +load 36 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint16).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint16).teal new file mode 100644 index 000000000..ec95bd989 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint16).teal @@ -0,0 +1,61 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 2 +load 2 +callsub roundtripper_1 +store 1 +byte 0x151f7c75 +load 1 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 7 +load 7 +int 0 +extract_uint16 +store 0 +load 0 +callsub numericalcomp_2 +store 0 +load 0 +itob +extract 6 0 +store 8 +load 8 +retsub + +// round_tripper +roundtripper_1: +store 3 +load 3 +callsub tuplecomplement_0 +store 5 +load 5 +callsub tuplecomplement_0 +store 6 +load 3 +load 5 +concat +load 6 +concat +store 4 +load 4 +retsub + +// numerical_comp +numericalcomp_2: +store 9 +int 65535 +load 9 +- +store 10 +load 10 +int 65536 +< +assert +load 10 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint16,uint8,byte).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint16,uint8,byte).teal new file mode 100644 index 000000000..0186d716b --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint16,uint8,byte).teal @@ -0,0 +1,113 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 4 +load 4 +callsub roundtripper_1 +store 3 +byte 0x151f7c75 +load 3 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 9 +load 9 +int 0 +extract_uint16 +store 0 +load 9 +int 2 +getbyte +store 1 +load 9 +int 3 +getbyte +store 2 +load 0 +callsub numericalcomp_2 +store 0 +load 1 +callsub numericalcomp_3 +store 1 +load 2 +callsub numericalcomp_4 +store 2 +load 0 +itob +extract 6 0 +byte 0x00 +int 0 +load 1 +setbyte +concat +byte 0x00 +int 0 +load 2 +setbyte +concat +store 10 +load 10 +retsub + +// round_tripper +roundtripper_1: +store 5 +load 5 +callsub tuplecomplement_0 +store 7 +load 7 +callsub tuplecomplement_0 +store 8 +load 5 +load 7 +concat +load 8 +concat +store 6 +load 6 +retsub + +// numerical_comp +numericalcomp_2: +store 11 +int 65535 +load 11 +- +store 12 +load 12 +int 65536 +< +assert +load 12 +retsub + +// numerical_comp +numericalcomp_3: +store 13 +int 255 +load 13 +- +store 14 +load 14 +int 256 +< +assert +load 14 +retsub + +// numerical_comp +numericalcomp_4: +store 15 +int 255 +load 15 +- +store 16 +load 16 +int 256 +< +assert +load 16 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint32).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint32).teal new file mode 100644 index 000000000..e7f7c8437 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint32).teal @@ -0,0 +1,61 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 2 +load 2 +callsub roundtripper_1 +store 1 +byte 0x151f7c75 +load 1 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 7 +load 7 +int 0 +extract_uint32 +store 0 +load 0 +callsub numericalcomp_2 +store 0 +load 0 +itob +extract 4 0 +store 8 +load 8 +retsub + +// round_tripper +roundtripper_1: +store 3 +load 3 +callsub tuplecomplement_0 +store 5 +load 5 +callsub tuplecomplement_0 +store 6 +load 3 +load 5 +concat +load 6 +concat +store 4 +load 4 +retsub + +// numerical_comp +numericalcomp_2: +store 9 +int 4294967295 +load 9 +- +store 10 +load 10 +int 4294967296 +< +assert +load 10 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint32,uint16,uint8).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint32,uint16,uint8).teal new file mode 100644 index 000000000..d0e0db70d --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint32,uint16,uint8).teal @@ -0,0 +1,112 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 4 +load 4 +callsub roundtripper_1 +store 3 +byte 0x151f7c75 +load 3 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 9 +load 9 +int 0 +extract_uint32 +store 0 +load 9 +int 4 +extract_uint16 +store 1 +load 9 +int 6 +getbyte +store 2 +load 0 +callsub numericalcomp_2 +store 0 +load 1 +callsub numericalcomp_3 +store 1 +load 2 +callsub numericalcomp_4 +store 2 +load 0 +itob +extract 4 0 +load 1 +itob +extract 6 0 +concat +byte 0x00 +int 0 +load 2 +setbyte +concat +store 10 +load 10 +retsub + +// round_tripper +roundtripper_1: +store 5 +load 5 +callsub tuplecomplement_0 +store 7 +load 7 +callsub tuplecomplement_0 +store 8 +load 5 +load 7 +concat +load 8 +concat +store 6 +load 6 +retsub + +// numerical_comp +numericalcomp_2: +store 11 +int 4294967295 +load 11 +- +store 12 +load 12 +int 4294967296 +< +assert +load 12 +retsub + +// numerical_comp +numericalcomp_3: +store 13 +int 65535 +load 13 +- +store 14 +load 14 +int 65536 +< +assert +load 14 +retsub + +// numerical_comp +numericalcomp_4: +store 15 +int 255 +load 15 +- +store 16 +load 16 +int 256 +< +assert +load 16 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint64).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint64).teal new file mode 100644 index 000000000..d04d1c0ae --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint64).teal @@ -0,0 +1,55 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 2 +load 2 +callsub roundtripper_1 +store 1 +byte 0x151f7c75 +load 1 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 7 +load 7 +btoi +store 0 +load 0 +callsub numericalcomp_2 +store 0 +load 0 +itob +store 8 +load 8 +retsub + +// round_tripper +roundtripper_1: +store 3 +load 3 +callsub tuplecomplement_0 +store 5 +load 5 +callsub tuplecomplement_0 +store 6 +load 3 +load 5 +concat +load 6 +concat +store 4 +load 4 +retsub + +// numerical_comp +numericalcomp_2: +store 9 +int 18446744073709551615 +load 9 +- +store 10 +load 10 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint64,uint32,uint16).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint64,uint32,uint16).teal new file mode 100644 index 000000000..c47b327be --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint64,uint32,uint16).teal @@ -0,0 +1,106 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 4 +load 4 +callsub roundtripper_1 +store 3 +byte 0x151f7c75 +load 3 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 9 +load 9 +int 0 +extract_uint64 +store 0 +load 9 +int 8 +extract_uint32 +store 1 +load 9 +int 12 +extract_uint16 +store 2 +load 0 +callsub numericalcomp_2 +store 0 +load 1 +callsub numericalcomp_3 +store 1 +load 2 +callsub numericalcomp_4 +store 2 +load 0 +itob +load 1 +itob +extract 4 0 +concat +load 2 +itob +extract 6 0 +concat +store 10 +load 10 +retsub + +// round_tripper +roundtripper_1: +store 5 +load 5 +callsub tuplecomplement_0 +store 7 +load 7 +callsub tuplecomplement_0 +store 8 +load 5 +load 7 +concat +load 8 +concat +store 6 +load 6 +retsub + +// numerical_comp +numericalcomp_2: +store 11 +int 18446744073709551615 +load 11 +- +store 12 +load 12 +retsub + +// numerical_comp +numericalcomp_3: +store 13 +int 4294967295 +load 13 +- +store 14 +load 14 +int 4294967296 +< +assert +load 14 +retsub + +// numerical_comp +numericalcomp_4: +store 15 +int 65535 +load 15 +- +store 16 +load 16 +int 65536 +< +assert +load 16 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint8).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint8).teal new file mode 100644 index 000000000..4e2b05add --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint8).teal @@ -0,0 +1,62 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 2 +load 2 +callsub roundtripper_1 +store 1 +byte 0x151f7c75 +load 1 +concat +log +int 1 +return + +// tuple_complement +tuplecomplement_0: +store 7 +load 7 +int 0 +getbyte +store 0 +load 0 +callsub numericalcomp_2 +store 0 +byte 0x00 +int 0 +load 0 +setbyte +store 8 +load 8 +retsub + +// round_tripper +roundtripper_1: +store 3 +load 3 +callsub tuplecomplement_0 +store 5 +load 5 +callsub tuplecomplement_0 +store 6 +load 3 +load 5 +concat +load 6 +concat +store 4 +load 4 +retsub + +// numerical_comp +numericalcomp_2: +store 9 +int 255 +load 9 +- +store 10 +load 10 +int 256 +< +assert +load 10 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_(uint8,byte,bool).teal b/tests/integration/teal/roundtrip/app_roundtrip_(uint8,byte,bool).teal new file mode 100644 index 000000000..251e70b5e --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_(uint8,byte,bool).teal @@ -0,0 +1,111 @@ +#pragma version 6 +txna ApplicationArgs 0 // [uint8|byte|bool] +store 4 // 4 -> uint8|byte|bool +load 4 // [uint8|byte|bool] +callsub roundtripper_1 // [uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool] +store 3 // 3 -> uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool +byte 0x151f7c75 // [0x151f7c75] +load 3 // [0x151f7c75, uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool] +concat // [0x151f7c75 | uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool] +log // log(0x151f7c75 | uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool) +int 1 // [1] +return // PASSED + +// tuple_complement +tuplecomplement_0: // [uint8|byte|bool] +store 9 // 9 -> uint8|byte|bool +load 9 // [uint8|byte|bool] +int 0 // [uint8|byte|bool, 0] +getbyte // [uint8] +store 0 // 0 -> uint8 +load 9 // [uint8|byte|bool] +int 1 // [uint8|byte|bool, 1] +getbyte // [byte] +store 1 // 1 -> byte +load 9 // [uint8|byte|bool] +int 16 // [uint8|byte|bool, 16] +getbit // bool +store 2 // 2 -> bool +load 0 // [uint8] +callsub numericalcomp_2 // [255 - uint8] +store 0 // 0 -> 255 - uint8 +load 1 // [byte] +callsub numericalcomp_3 // [255 - byte] +store 1 // 1 -> 255 - byte +load 2 // [bool] +callsub boolcomp_4 // [!bool] +store 2 // 2 -> !bool +byte 0x00 // [0x00] +int 0 // [0x00, 0] +load 0 // [0x00, 0, 255 - uint8] +setbyte // [255 - uint8] +byte 0x00 // [255 - uint8, 0x00] +int 0 // [255 - uint8, 0x00, 0] +load 1 // [255 - uint8, 0x00, 0, 255 - byte] +setbyte // [255 - uint8, 255 - byte] +concat // [255 - uint8 | 255 - byte] +byte 0x00 // [255 - uint8 | 255 - byte, 0x00] +int 0 // [255 - uint8 | 255 - byte, 0x00, 0] +load 2 // [255 - uint8 | 255 - byte, 0x00, 0, !bool] +setbit // [255 - uint8 | 255 - byte, !bool] +concat // [255 - uint8 | 255 - byte | !bool] +store 10 // 10 -> 255 - uint8 | 255 - byte | !bool +load 10 // [255 - uint8 | 255 - byte | !bool] +retsub + +// round_tripper +roundtripper_1: // [uint8|byte|bool] +store 5 // 5 -> uint8|byte|bool +load 5 // [uint8|byte|bool] +callsub tuplecomplement_0 // [255 - uint8 | 255 - byte | !bool] +store 7 // 7 -> 255 - uint8 | 255 - byte | !bool +load 7 // [255 - uint8 | 255 - byte | !bool] +callsub tuplecomplement_0 // [255 - (255 - uint8) | 255 - (255 - byte) | !!bool] +store 8 // 8 -> uint8|byte|bool +load 5 // [uint8|byte|bool] +load 7 // [uint8|byte|bool, 255 - uint8 | 255 - byte | !bool] +concat // [uint8|byte|bool | 255 - uint8 | 255 - byte | !bool] +load 8 // [uint8|byte|bool | 255 - uint8 | 255 - byte | !bool, uint8|byte|bool] +concat // [uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool] +store 6 // 6 -> uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool +load 6 // [uint8|byte|bool | 255 - uint8 | 255 - byte | !bool | uint8|byte|bool] +retsub + +// numerical_comp +numericalcomp_2: // [uint8] +store 11 // 11 -> uint8 +int 255 // [255] +load 11 // [255, uint8] +- // [255 - uint8] +store 12 // 12 -> 255 - uint8 +load 12 // [255 - uint8] +int 256 // [255 - uint8, 256] +< // [1] +assert // [] +load 12 // [255 - uint8] +retsub + +// numerical_comp +numericalcomp_3: // [byte] +store 13 // 13 -> byte +int 255 // [255] +load 13 // [255, byte] +- // [255 - byte] +store 14 // 14 -> 255 - byte +load 14 // [255 - byte] +int 256 // [255 - byte, 256] +< // [1] +assert // [] +load 14 // [255 - byte] +retsub + +// bool_comp +boolcomp_4: // [bool] +store 15 // 15 -> bool +load 15 // [bool] +! // [!bool] +! +! +store 16 // 16 -> !bool +load 16 // [!bool] +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_address.teal b/tests/integration/teal/roundtrip/app_roundtrip_address.teal new file mode 100644 index 000000000..1a621ddc5 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_address.teal @@ -0,0 +1,498 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 40 +int 255 +load 40 +- +store 41 +load 41 +int 256 +< +assert +load 41 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 1 +int 0 +* +getbyte +store 8 +load 6 +int 1 +int 1 +* +getbyte +store 9 +load 6 +int 1 +int 2 +* +getbyte +store 10 +load 6 +int 1 +int 3 +* +getbyte +store 11 +load 6 +int 1 +int 4 +* +getbyte +store 12 +load 6 +int 1 +int 5 +* +getbyte +store 13 +load 6 +int 1 +int 6 +* +getbyte +store 14 +load 6 +int 1 +int 7 +* +getbyte +store 15 +load 6 +int 1 +int 8 +* +getbyte +store 16 +load 6 +int 1 +int 9 +* +getbyte +store 17 +load 6 +int 1 +int 10 +* +getbyte +store 18 +load 6 +int 1 +int 11 +* +getbyte +store 19 +load 6 +int 1 +int 12 +* +getbyte +store 20 +load 6 +int 1 +int 13 +* +getbyte +store 21 +load 6 +int 1 +int 14 +* +getbyte +store 22 +load 6 +int 1 +int 15 +* +getbyte +store 23 +load 6 +int 1 +int 16 +* +getbyte +store 24 +load 6 +int 1 +int 17 +* +getbyte +store 25 +load 6 +int 1 +int 18 +* +getbyte +store 26 +load 6 +int 1 +int 19 +* +getbyte +store 27 +load 6 +int 1 +int 20 +* +getbyte +store 28 +load 6 +int 1 +int 21 +* +getbyte +store 29 +load 6 +int 1 +int 22 +* +getbyte +store 30 +load 6 +int 1 +int 23 +* +getbyte +store 31 +load 6 +int 1 +int 24 +* +getbyte +store 32 +load 6 +int 1 +int 25 +* +getbyte +store 33 +load 6 +int 1 +int 26 +* +getbyte +store 34 +load 6 +int 1 +int 27 +* +getbyte +store 35 +load 6 +int 1 +int 28 +* +getbyte +store 36 +load 6 +int 1 +int 29 +* +getbyte +store 37 +load 6 +int 1 +int 30 +* +getbyte +store 38 +load 6 +int 1 +int 31 +* +getbyte +store 39 +load 8 +callsub numericalcomp_0 +store 8 +load 9 +callsub numericalcomp_0 +store 9 +load 10 +callsub numericalcomp_0 +store 10 +load 11 +callsub numericalcomp_0 +store 11 +load 12 +callsub numericalcomp_0 +store 12 +load 13 +callsub numericalcomp_0 +store 13 +load 14 +callsub numericalcomp_0 +store 14 +load 15 +callsub numericalcomp_0 +store 15 +load 16 +callsub numericalcomp_0 +store 16 +load 17 +callsub numericalcomp_0 +store 17 +load 18 +callsub numericalcomp_0 +store 18 +load 19 +callsub numericalcomp_0 +store 19 +load 20 +callsub numericalcomp_0 +store 20 +load 21 +callsub numericalcomp_0 +store 21 +load 22 +callsub numericalcomp_0 +store 22 +load 23 +callsub numericalcomp_0 +store 23 +load 24 +callsub numericalcomp_0 +store 24 +load 25 +callsub numericalcomp_0 +store 25 +load 26 +callsub numericalcomp_0 +store 26 +load 27 +callsub numericalcomp_0 +store 27 +load 28 +callsub numericalcomp_0 +store 28 +load 29 +callsub numericalcomp_0 +store 29 +load 30 +callsub numericalcomp_0 +store 30 +load 31 +callsub numericalcomp_0 +store 31 +load 32 +callsub numericalcomp_0 +store 32 +load 33 +callsub numericalcomp_0 +store 33 +load 34 +callsub numericalcomp_0 +store 34 +load 35 +callsub numericalcomp_0 +store 35 +load 36 +callsub numericalcomp_0 +store 36 +load 37 +callsub numericalcomp_0 +store 37 +load 38 +callsub numericalcomp_0 +store 38 +load 39 +callsub numericalcomp_0 +store 39 +byte 0x00 +int 0 +load 8 +setbyte +byte 0x00 +int 0 +load 9 +setbyte +concat +byte 0x00 +int 0 +load 10 +setbyte +concat +byte 0x00 +int 0 +load 11 +setbyte +concat +byte 0x00 +int 0 +load 12 +setbyte +concat +byte 0x00 +int 0 +load 13 +setbyte +concat +byte 0x00 +int 0 +load 14 +setbyte +concat +byte 0x00 +int 0 +load 15 +setbyte +concat +byte 0x00 +int 0 +load 16 +setbyte +concat +byte 0x00 +int 0 +load 17 +setbyte +concat +byte 0x00 +int 0 +load 18 +setbyte +concat +byte 0x00 +int 0 +load 19 +setbyte +concat +byte 0x00 +int 0 +load 20 +setbyte +concat +byte 0x00 +int 0 +load 21 +setbyte +concat +byte 0x00 +int 0 +load 22 +setbyte +concat +byte 0x00 +int 0 +load 23 +setbyte +concat +byte 0x00 +int 0 +load 24 +setbyte +concat +byte 0x00 +int 0 +load 25 +setbyte +concat +byte 0x00 +int 0 +load 26 +setbyte +concat +byte 0x00 +int 0 +load 27 +setbyte +concat +byte 0x00 +int 0 +load 28 +setbyte +concat +byte 0x00 +int 0 +load 29 +setbyte +concat +byte 0x00 +int 0 +load 30 +setbyte +concat +byte 0x00 +int 0 +load 31 +setbyte +concat +byte 0x00 +int 0 +load 32 +setbyte +concat +byte 0x00 +int 0 +load 33 +setbyte +concat +byte 0x00 +int 0 +load 34 +setbyte +concat +byte 0x00 +int 0 +load 35 +setbyte +concat +byte 0x00 +int 0 +load 36 +setbyte +concat +byte 0x00 +int 0 +load 37 +setbyte +concat +byte 0x00 +int 0 +load 38 +setbyte +concat +byte 0x00 +int 0 +load 39 +setbyte +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +load 4 +concat +load 5 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_address[]_<10>.teal b/tests/integration/teal/roundtrip/app_roundtrip_address[]_<10>.teal new file mode 100644 index 000000000..5667061f6 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_address[]_<10>.teal @@ -0,0 +1,698 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_3 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 52 +int 255 +load 52 +- +store 53 +load 53 +int 256 +< +assert +load 53 +retsub + +// array_complement +arraycomplement_1: +store 18 +load 18 +int 1 +int 0 +* +getbyte +store 20 +load 18 +int 1 +int 1 +* +getbyte +store 21 +load 18 +int 1 +int 2 +* +getbyte +store 22 +load 18 +int 1 +int 3 +* +getbyte +store 23 +load 18 +int 1 +int 4 +* +getbyte +store 24 +load 18 +int 1 +int 5 +* +getbyte +store 25 +load 18 +int 1 +int 6 +* +getbyte +store 26 +load 18 +int 1 +int 7 +* +getbyte +store 27 +load 18 +int 1 +int 8 +* +getbyte +store 28 +load 18 +int 1 +int 9 +* +getbyte +store 29 +load 18 +int 1 +int 10 +* +getbyte +store 30 +load 18 +int 1 +int 11 +* +getbyte +store 31 +load 18 +int 1 +int 12 +* +getbyte +store 32 +load 18 +int 1 +int 13 +* +getbyte +store 33 +load 18 +int 1 +int 14 +* +getbyte +store 34 +load 18 +int 1 +int 15 +* +getbyte +store 35 +load 18 +int 1 +int 16 +* +getbyte +store 36 +load 18 +int 1 +int 17 +* +getbyte +store 37 +load 18 +int 1 +int 18 +* +getbyte +store 38 +load 18 +int 1 +int 19 +* +getbyte +store 39 +load 18 +int 1 +int 20 +* +getbyte +store 40 +load 18 +int 1 +int 21 +* +getbyte +store 41 +load 18 +int 1 +int 22 +* +getbyte +store 42 +load 18 +int 1 +int 23 +* +getbyte +store 43 +load 18 +int 1 +int 24 +* +getbyte +store 44 +load 18 +int 1 +int 25 +* +getbyte +store 45 +load 18 +int 1 +int 26 +* +getbyte +store 46 +load 18 +int 1 +int 27 +* +getbyte +store 47 +load 18 +int 1 +int 28 +* +getbyte +store 48 +load 18 +int 1 +int 29 +* +getbyte +store 49 +load 18 +int 1 +int 30 +* +getbyte +store 50 +load 18 +int 1 +int 31 +* +getbyte +store 51 +load 20 +callsub numericalcomp_0 +store 20 +load 21 +callsub numericalcomp_0 +store 21 +load 22 +callsub numericalcomp_0 +store 22 +load 23 +callsub numericalcomp_0 +store 23 +load 24 +callsub numericalcomp_0 +store 24 +load 25 +callsub numericalcomp_0 +store 25 +load 26 +callsub numericalcomp_0 +store 26 +load 27 +callsub numericalcomp_0 +store 27 +load 28 +callsub numericalcomp_0 +store 28 +load 29 +callsub numericalcomp_0 +store 29 +load 30 +callsub numericalcomp_0 +store 30 +load 31 +callsub numericalcomp_0 +store 31 +load 32 +callsub numericalcomp_0 +store 32 +load 33 +callsub numericalcomp_0 +store 33 +load 34 +callsub numericalcomp_0 +store 34 +load 35 +callsub numericalcomp_0 +store 35 +load 36 +callsub numericalcomp_0 +store 36 +load 37 +callsub numericalcomp_0 +store 37 +load 38 +callsub numericalcomp_0 +store 38 +load 39 +callsub numericalcomp_0 +store 39 +load 40 +callsub numericalcomp_0 +store 40 +load 41 +callsub numericalcomp_0 +store 41 +load 42 +callsub numericalcomp_0 +store 42 +load 43 +callsub numericalcomp_0 +store 43 +load 44 +callsub numericalcomp_0 +store 44 +load 45 +callsub numericalcomp_0 +store 45 +load 46 +callsub numericalcomp_0 +store 46 +load 47 +callsub numericalcomp_0 +store 47 +load 48 +callsub numericalcomp_0 +store 48 +load 49 +callsub numericalcomp_0 +store 49 +load 50 +callsub numericalcomp_0 +store 50 +load 51 +callsub numericalcomp_0 +store 51 +byte 0x00 +int 0 +load 20 +setbyte +byte 0x00 +int 0 +load 21 +setbyte +concat +byte 0x00 +int 0 +load 22 +setbyte +concat +byte 0x00 +int 0 +load 23 +setbyte +concat +byte 0x00 +int 0 +load 24 +setbyte +concat +byte 0x00 +int 0 +load 25 +setbyte +concat +byte 0x00 +int 0 +load 26 +setbyte +concat +byte 0x00 +int 0 +load 27 +setbyte +concat +byte 0x00 +int 0 +load 28 +setbyte +concat +byte 0x00 +int 0 +load 29 +setbyte +concat +byte 0x00 +int 0 +load 30 +setbyte +concat +byte 0x00 +int 0 +load 31 +setbyte +concat +byte 0x00 +int 0 +load 32 +setbyte +concat +byte 0x00 +int 0 +load 33 +setbyte +concat +byte 0x00 +int 0 +load 34 +setbyte +concat +byte 0x00 +int 0 +load 35 +setbyte +concat +byte 0x00 +int 0 +load 36 +setbyte +concat +byte 0x00 +int 0 +load 37 +setbyte +concat +byte 0x00 +int 0 +load 38 +setbyte +concat +byte 0x00 +int 0 +load 39 +setbyte +concat +byte 0x00 +int 0 +load 40 +setbyte +concat +byte 0x00 +int 0 +load 41 +setbyte +concat +byte 0x00 +int 0 +load 42 +setbyte +concat +byte 0x00 +int 0 +load 43 +setbyte +concat +byte 0x00 +int 0 +load 44 +setbyte +concat +byte 0x00 +int 0 +load 45 +setbyte +concat +byte 0x00 +int 0 +load 46 +setbyte +concat +byte 0x00 +int 0 +load 47 +setbyte +concat +byte 0x00 +int 0 +load 48 +setbyte +concat +byte 0x00 +int 0 +load 49 +setbyte +concat +byte 0x00 +int 0 +load 50 +setbyte +concat +byte 0x00 +int 0 +load 51 +setbyte +concat +store 19 +load 19 +retsub + +// array_complement +arraycomplement_2: +store 6 +load 6 +int 32 +int 0 +* +int 2 ++ +int 32 +extract3 +store 8 +load 6 +int 32 +int 1 +* +int 2 ++ +int 32 +extract3 +store 9 +load 6 +int 32 +int 2 +* +int 2 ++ +int 32 +extract3 +store 10 +load 6 +int 32 +int 3 +* +int 2 ++ +int 32 +extract3 +store 11 +load 6 +int 32 +int 4 +* +int 2 ++ +int 32 +extract3 +store 12 +load 6 +int 32 +int 5 +* +int 2 ++ +int 32 +extract3 +store 13 +load 6 +int 32 +int 6 +* +int 2 ++ +int 32 +extract3 +store 14 +load 6 +int 32 +int 7 +* +int 2 ++ +int 32 +extract3 +store 15 +load 6 +int 32 +int 8 +* +int 2 ++ +int 32 +extract3 +store 16 +load 6 +int 32 +int 9 +* +int 2 ++ +int 32 +extract3 +store 17 +load 8 +callsub arraycomplement_1 +store 8 +load 9 +callsub arraycomplement_1 +store 9 +load 10 +callsub arraycomplement_1 +store 10 +load 11 +callsub arraycomplement_1 +store 11 +load 12 +callsub arraycomplement_1 +store 12 +load 13 +callsub arraycomplement_1 +store 13 +load 14 +callsub arraycomplement_1 +store 14 +load 15 +callsub arraycomplement_1 +store 15 +load 16 +callsub arraycomplement_1 +store 16 +load 17 +callsub arraycomplement_1 +store 17 +int 10 +store 54 +load 54 +itob +extract 6 0 +load 8 +load 9 +concat +load 10 +concat +load 11 +concat +load 12 +concat +load 13 +concat +load 14 +concat +load 15 +concat +load 16 +concat +load 17 +concat +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_3: +store 2 +load 2 +callsub arraycomplement_2 +store 4 +load 4 +callsub arraycomplement_2 +store 5 +load 2 +store 58 +load 58 +store 57 +int 6 +store 55 +load 55 +load 58 +len ++ +store 56 +load 56 +int 65536 +< +assert +load 55 +itob +extract 6 0 +load 4 +store 58 +load 57 +load 58 +concat +store 57 +load 56 +store 55 +load 55 +load 58 +len ++ +store 56 +load 56 +int 65536 +< +assert +load 55 +itob +extract 6 0 +concat +load 5 +store 58 +load 57 +load 58 +concat +store 57 +load 56 +store 55 +load 55 +itob +extract 6 0 +concat +load 57 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool.teal new file mode 100644 index 000000000..497b052bf --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_bool.teal @@ -0,0 +1,50 @@ +#pragma version 6 +txna ApplicationArgs 0 +int 0 +int 8 +* +getbit +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// bool_comp +boolcomp_0: +store 6 +load 6 +! +! +! +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub boolcomp_0 +store 4 +load 4 +callsub boolcomp_0 +store 5 +byte 0x00 +int 0 +load 2 +setbit +int 1 +load 4 +setbit +int 2 +load 5 +setbit +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[1].teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[1].teal new file mode 100644 index 000000000..dd10dff76 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[1].teal @@ -0,0 +1,59 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// bool_comp +boolcomp_0: +store 9 +load 9 +! +! +! +store 10 +load 10 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 0 +getbit +store 8 +load 8 +callsub boolcomp_0 +store 8 +byte 0x00 +int 0 +load 8 +setbit +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +load 4 +concat +load 5 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[3][]_<11>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[3][]_<11>.teal new file mode 100644 index 000000000..f921307ab --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[3][]_<11>.teal @@ -0,0 +1,293 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_3 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// bool_comp +boolcomp_0: +store 24 +load 24 +! +! +! +store 25 +load 25 +retsub + +// array_complement +arraycomplement_1: +store 19 +load 19 +int 0 +getbit +store 21 +load 19 +int 1 +getbit +store 22 +load 19 +int 2 +getbit +store 23 +load 21 +callsub boolcomp_0 +store 21 +load 22 +callsub boolcomp_0 +store 22 +load 23 +callsub boolcomp_0 +store 23 +byte 0x00 +int 0 +load 21 +setbit +int 1 +load 22 +setbit +int 2 +load 23 +setbit +store 20 +load 20 +retsub + +// array_complement +arraycomplement_2: +store 6 +load 6 +int 1 +int 0 +* +int 2 ++ +int 1 +extract3 +store 8 +load 6 +int 1 +int 1 +* +int 2 ++ +int 1 +extract3 +store 9 +load 6 +int 1 +int 2 +* +int 2 ++ +int 1 +extract3 +store 10 +load 6 +int 1 +int 3 +* +int 2 ++ +int 1 +extract3 +store 11 +load 6 +int 1 +int 4 +* +int 2 ++ +int 1 +extract3 +store 12 +load 6 +int 1 +int 5 +* +int 2 ++ +int 1 +extract3 +store 13 +load 6 +int 1 +int 6 +* +int 2 ++ +int 1 +extract3 +store 14 +load 6 +int 1 +int 7 +* +int 2 ++ +int 1 +extract3 +store 15 +load 6 +int 1 +int 8 +* +int 2 ++ +int 1 +extract3 +store 16 +load 6 +int 1 +int 9 +* +int 2 ++ +int 1 +extract3 +store 17 +load 6 +int 1 +int 10 +* +int 2 ++ +int 1 +extract3 +store 18 +load 8 +callsub arraycomplement_1 +store 8 +load 9 +callsub arraycomplement_1 +store 9 +load 10 +callsub arraycomplement_1 +store 10 +load 11 +callsub arraycomplement_1 +store 11 +load 12 +callsub arraycomplement_1 +store 12 +load 13 +callsub arraycomplement_1 +store 13 +load 14 +callsub arraycomplement_1 +store 14 +load 15 +callsub arraycomplement_1 +store 15 +load 16 +callsub arraycomplement_1 +store 16 +load 17 +callsub arraycomplement_1 +store 17 +load 18 +callsub arraycomplement_1 +store 18 +int 11 +store 26 +load 26 +itob +extract 6 0 +load 8 +load 9 +concat +load 10 +concat +load 11 +concat +load 12 +concat +load 13 +concat +load 14 +concat +load 15 +concat +load 16 +concat +load 17 +concat +load 18 +concat +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_3: +store 2 +load 2 +callsub arraycomplement_2 +store 4 +load 4 +callsub arraycomplement_2 +store 5 +load 2 +store 30 +load 30 +store 29 +int 6 +store 27 +load 27 +load 30 +len ++ +store 28 +load 28 +int 65536 +< +assert +load 27 +itob +extract 6 0 +load 4 +store 30 +load 29 +load 30 +concat +store 29 +load 28 +store 27 +load 27 +load 30 +len ++ +store 28 +load 28 +int 65536 +< +assert +load 27 +itob +extract 6 0 +concat +load 5 +store 30 +load 29 +load 30 +concat +store 29 +load 28 +store 27 +load 27 +itob +extract 6 0 +concat +load 29 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[42].teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[42].teal new file mode 100644 index 000000000..707a88f59 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[42].teal @@ -0,0 +1,469 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// bool_comp +boolcomp_0: +store 50 +load 50 +! +! +! +store 51 +load 51 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 0 +getbit +store 8 +load 6 +int 1 +getbit +store 9 +load 6 +int 2 +getbit +store 10 +load 6 +int 3 +getbit +store 11 +load 6 +int 4 +getbit +store 12 +load 6 +int 5 +getbit +store 13 +load 6 +int 6 +getbit +store 14 +load 6 +int 7 +getbit +store 15 +load 6 +int 8 +getbit +store 16 +load 6 +int 9 +getbit +store 17 +load 6 +int 10 +getbit +store 18 +load 6 +int 11 +getbit +store 19 +load 6 +int 12 +getbit +store 20 +load 6 +int 13 +getbit +store 21 +load 6 +int 14 +getbit +store 22 +load 6 +int 15 +getbit +store 23 +load 6 +int 16 +getbit +store 24 +load 6 +int 17 +getbit +store 25 +load 6 +int 18 +getbit +store 26 +load 6 +int 19 +getbit +store 27 +load 6 +int 20 +getbit +store 28 +load 6 +int 21 +getbit +store 29 +load 6 +int 22 +getbit +store 30 +load 6 +int 23 +getbit +store 31 +load 6 +int 24 +getbit +store 32 +load 6 +int 25 +getbit +store 33 +load 6 +int 26 +getbit +store 34 +load 6 +int 27 +getbit +store 35 +load 6 +int 28 +getbit +store 36 +load 6 +int 29 +getbit +store 37 +load 6 +int 30 +getbit +store 38 +load 6 +int 31 +getbit +store 39 +load 6 +int 32 +getbit +store 40 +load 6 +int 33 +getbit +store 41 +load 6 +int 34 +getbit +store 42 +load 6 +int 35 +getbit +store 43 +load 6 +int 36 +getbit +store 44 +load 6 +int 37 +getbit +store 45 +load 6 +int 38 +getbit +store 46 +load 6 +int 39 +getbit +store 47 +load 6 +int 40 +getbit +store 48 +load 6 +int 41 +getbit +store 49 +load 8 +callsub boolcomp_0 +store 8 +load 9 +callsub boolcomp_0 +store 9 +load 10 +callsub boolcomp_0 +store 10 +load 11 +callsub boolcomp_0 +store 11 +load 12 +callsub boolcomp_0 +store 12 +load 13 +callsub boolcomp_0 +store 13 +load 14 +callsub boolcomp_0 +store 14 +load 15 +callsub boolcomp_0 +store 15 +load 16 +callsub boolcomp_0 +store 16 +load 17 +callsub boolcomp_0 +store 17 +load 18 +callsub boolcomp_0 +store 18 +load 19 +callsub boolcomp_0 +store 19 +load 20 +callsub boolcomp_0 +store 20 +load 21 +callsub boolcomp_0 +store 21 +load 22 +callsub boolcomp_0 +store 22 +load 23 +callsub boolcomp_0 +store 23 +load 24 +callsub boolcomp_0 +store 24 +load 25 +callsub boolcomp_0 +store 25 +load 26 +callsub boolcomp_0 +store 26 +load 27 +callsub boolcomp_0 +store 27 +load 28 +callsub boolcomp_0 +store 28 +load 29 +callsub boolcomp_0 +store 29 +load 30 +callsub boolcomp_0 +store 30 +load 31 +callsub boolcomp_0 +store 31 +load 32 +callsub boolcomp_0 +store 32 +load 33 +callsub boolcomp_0 +store 33 +load 34 +callsub boolcomp_0 +store 34 +load 35 +callsub boolcomp_0 +store 35 +load 36 +callsub boolcomp_0 +store 36 +load 37 +callsub boolcomp_0 +store 37 +load 38 +callsub boolcomp_0 +store 38 +load 39 +callsub boolcomp_0 +store 39 +load 40 +callsub boolcomp_0 +store 40 +load 41 +callsub boolcomp_0 +store 41 +load 42 +callsub boolcomp_0 +store 42 +load 43 +callsub boolcomp_0 +store 43 +load 44 +callsub boolcomp_0 +store 44 +load 45 +callsub boolcomp_0 +store 45 +load 46 +callsub boolcomp_0 +store 46 +load 47 +callsub boolcomp_0 +store 47 +load 48 +callsub boolcomp_0 +store 48 +load 49 +callsub boolcomp_0 +store 49 +byte 0x000000000000 +int 0 +load 8 +setbit +int 1 +load 9 +setbit +int 2 +load 10 +setbit +int 3 +load 11 +setbit +int 4 +load 12 +setbit +int 5 +load 13 +setbit +int 6 +load 14 +setbit +int 7 +load 15 +setbit +int 8 +load 16 +setbit +int 9 +load 17 +setbit +int 10 +load 18 +setbit +int 11 +load 19 +setbit +int 12 +load 20 +setbit +int 13 +load 21 +setbit +int 14 +load 22 +setbit +int 15 +load 23 +setbit +int 16 +load 24 +setbit +int 17 +load 25 +setbit +int 18 +load 26 +setbit +int 19 +load 27 +setbit +int 20 +load 28 +setbit +int 21 +load 29 +setbit +int 22 +load 30 +setbit +int 23 +load 31 +setbit +int 24 +load 32 +setbit +int 25 +load 33 +setbit +int 26 +load 34 +setbit +int 27 +load 35 +setbit +int 28 +load 36 +setbit +int 29 +load 37 +setbit +int 30 +load 38 +setbit +int 31 +load 39 +setbit +int 32 +load 40 +setbit +int 33 +load 41 +setbit +int 34 +load 42 +setbit +int 35 +load 43 +setbit +int 36 +load 44 +setbit +int 37 +load 45 +setbit +int 38 +load 46 +setbit +int 39 +load 47 +setbit +int 40 +load 48 +setbit +int 41 +load 49 +setbit +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +load 4 +concat +load 5 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<0>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<0>.teal new file mode 100644 index 000000000..710b6a859 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<0>.teal @@ -0,0 +1,92 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// array_complement +arraycomplement_0: +store 6 +int 0 +store 8 +load 8 +itob +extract 6 0 +byte "" +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub arraycomplement_0 +store 4 +load 4 +callsub arraycomplement_0 +store 5 +load 2 +store 12 +load 12 +store 11 +int 6 +store 9 +load 9 +load 12 +len ++ +store 10 +load 10 +int 65536 +< +assert +load 9 +itob +extract 6 0 +load 4 +store 12 +load 11 +load 12 +concat +store 11 +load 10 +store 9 +load 9 +load 12 +len ++ +store 10 +load 10 +int 65536 +< +assert +load 9 +itob +extract 6 0 +concat +load 5 +store 12 +load 11 +load 12 +concat +store 11 +load 10 +store 9 +load 9 +itob +extract 6 0 +concat +load 11 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<1>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<1>.teal new file mode 100644 index 000000000..3202304d9 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<1>.teal @@ -0,0 +1,115 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// bool_comp +boolcomp_0: +store 9 +load 9 +! +! +! +store 10 +load 10 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 0 +int 16 ++ +getbit +store 8 +load 8 +callsub boolcomp_0 +store 8 +int 1 +store 11 +load 11 +itob +extract 6 0 +byte 0x00 +int 0 +load 8 +setbit +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +store 15 +load 15 +store 14 +int 6 +store 12 +load 12 +load 15 +len ++ +store 13 +load 13 +int 65536 +< +assert +load 12 +itob +extract 6 0 +load 4 +store 15 +load 14 +load 15 +concat +store 14 +load 13 +store 12 +load 12 +load 15 +len ++ +store 13 +load 13 +int 65536 +< +assert +load 12 +itob +extract 6 0 +concat +load 5 +store 15 +load 14 +load 15 +concat +store 14 +load 13 +store 12 +load 12 +itob +extract 6 0 +concat +load 14 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<42>.teal b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<42>.teal new file mode 100644 index 000000000..20ddb32ae --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_bool[]_<42>.teal @@ -0,0 +1,607 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// bool_comp +boolcomp_0: +store 50 +load 50 +! +! +! +store 51 +load 51 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 0 +int 16 ++ +getbit +store 8 +load 6 +int 1 +int 16 ++ +getbit +store 9 +load 6 +int 2 +int 16 ++ +getbit +store 10 +load 6 +int 3 +int 16 ++ +getbit +store 11 +load 6 +int 4 +int 16 ++ +getbit +store 12 +load 6 +int 5 +int 16 ++ +getbit +store 13 +load 6 +int 6 +int 16 ++ +getbit +store 14 +load 6 +int 7 +int 16 ++ +getbit +store 15 +load 6 +int 8 +int 16 ++ +getbit +store 16 +load 6 +int 9 +int 16 ++ +getbit +store 17 +load 6 +int 10 +int 16 ++ +getbit +store 18 +load 6 +int 11 +int 16 ++ +getbit +store 19 +load 6 +int 12 +int 16 ++ +getbit +store 20 +load 6 +int 13 +int 16 ++ +getbit +store 21 +load 6 +int 14 +int 16 ++ +getbit +store 22 +load 6 +int 15 +int 16 ++ +getbit +store 23 +load 6 +int 16 +int 16 ++ +getbit +store 24 +load 6 +int 17 +int 16 ++ +getbit +store 25 +load 6 +int 18 +int 16 ++ +getbit +store 26 +load 6 +int 19 +int 16 ++ +getbit +store 27 +load 6 +int 20 +int 16 ++ +getbit +store 28 +load 6 +int 21 +int 16 ++ +getbit +store 29 +load 6 +int 22 +int 16 ++ +getbit +store 30 +load 6 +int 23 +int 16 ++ +getbit +store 31 +load 6 +int 24 +int 16 ++ +getbit +store 32 +load 6 +int 25 +int 16 ++ +getbit +store 33 +load 6 +int 26 +int 16 ++ +getbit +store 34 +load 6 +int 27 +int 16 ++ +getbit +store 35 +load 6 +int 28 +int 16 ++ +getbit +store 36 +load 6 +int 29 +int 16 ++ +getbit +store 37 +load 6 +int 30 +int 16 ++ +getbit +store 38 +load 6 +int 31 +int 16 ++ +getbit +store 39 +load 6 +int 32 +int 16 ++ +getbit +store 40 +load 6 +int 33 +int 16 ++ +getbit +store 41 +load 6 +int 34 +int 16 ++ +getbit +store 42 +load 6 +int 35 +int 16 ++ +getbit +store 43 +load 6 +int 36 +int 16 ++ +getbit +store 44 +load 6 +int 37 +int 16 ++ +getbit +store 45 +load 6 +int 38 +int 16 ++ +getbit +store 46 +load 6 +int 39 +int 16 ++ +getbit +store 47 +load 6 +int 40 +int 16 ++ +getbit +store 48 +load 6 +int 41 +int 16 ++ +getbit +store 49 +load 8 +callsub boolcomp_0 +store 8 +load 9 +callsub boolcomp_0 +store 9 +load 10 +callsub boolcomp_0 +store 10 +load 11 +callsub boolcomp_0 +store 11 +load 12 +callsub boolcomp_0 +store 12 +load 13 +callsub boolcomp_0 +store 13 +load 14 +callsub boolcomp_0 +store 14 +load 15 +callsub boolcomp_0 +store 15 +load 16 +callsub boolcomp_0 +store 16 +load 17 +callsub boolcomp_0 +store 17 +load 18 +callsub boolcomp_0 +store 18 +load 19 +callsub boolcomp_0 +store 19 +load 20 +callsub boolcomp_0 +store 20 +load 21 +callsub boolcomp_0 +store 21 +load 22 +callsub boolcomp_0 +store 22 +load 23 +callsub boolcomp_0 +store 23 +load 24 +callsub boolcomp_0 +store 24 +load 25 +callsub boolcomp_0 +store 25 +load 26 +callsub boolcomp_0 +store 26 +load 27 +callsub boolcomp_0 +store 27 +load 28 +callsub boolcomp_0 +store 28 +load 29 +callsub boolcomp_0 +store 29 +load 30 +callsub boolcomp_0 +store 30 +load 31 +callsub boolcomp_0 +store 31 +load 32 +callsub boolcomp_0 +store 32 +load 33 +callsub boolcomp_0 +store 33 +load 34 +callsub boolcomp_0 +store 34 +load 35 +callsub boolcomp_0 +store 35 +load 36 +callsub boolcomp_0 +store 36 +load 37 +callsub boolcomp_0 +store 37 +load 38 +callsub boolcomp_0 +store 38 +load 39 +callsub boolcomp_0 +store 39 +load 40 +callsub boolcomp_0 +store 40 +load 41 +callsub boolcomp_0 +store 41 +load 42 +callsub boolcomp_0 +store 42 +load 43 +callsub boolcomp_0 +store 43 +load 44 +callsub boolcomp_0 +store 44 +load 45 +callsub boolcomp_0 +store 45 +load 46 +callsub boolcomp_0 +store 46 +load 47 +callsub boolcomp_0 +store 47 +load 48 +callsub boolcomp_0 +store 48 +load 49 +callsub boolcomp_0 +store 49 +int 42 +store 52 +load 52 +itob +extract 6 0 +byte 0x000000000000 +int 0 +load 8 +setbit +int 1 +load 9 +setbit +int 2 +load 10 +setbit +int 3 +load 11 +setbit +int 4 +load 12 +setbit +int 5 +load 13 +setbit +int 6 +load 14 +setbit +int 7 +load 15 +setbit +int 8 +load 16 +setbit +int 9 +load 17 +setbit +int 10 +load 18 +setbit +int 11 +load 19 +setbit +int 12 +load 20 +setbit +int 13 +load 21 +setbit +int 14 +load 22 +setbit +int 15 +load 23 +setbit +int 16 +load 24 +setbit +int 17 +load 25 +setbit +int 18 +load 26 +setbit +int 19 +load 27 +setbit +int 20 +load 28 +setbit +int 21 +load 29 +setbit +int 22 +load 30 +setbit +int 23 +load 31 +setbit +int 24 +load 32 +setbit +int 25 +load 33 +setbit +int 26 +load 34 +setbit +int 27 +load 35 +setbit +int 28 +load 36 +setbit +int 29 +load 37 +setbit +int 30 +load 38 +setbit +int 31 +load 39 +setbit +int 32 +load 40 +setbit +int 33 +load 41 +setbit +int 34 +load 42 +setbit +int 35 +load 43 +setbit +int 36 +load 44 +setbit +int 37 +load 45 +setbit +int 38 +load 46 +setbit +int 39 +load 47 +setbit +int 40 +load 48 +setbit +int 41 +load 49 +setbit +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +store 56 +load 56 +store 55 +int 6 +store 53 +load 53 +load 56 +len ++ +store 54 +load 54 +int 65536 +< +assert +load 53 +itob +extract 6 0 +load 4 +store 56 +load 55 +load 56 +concat +store 55 +load 54 +store 53 +load 53 +load 56 +len ++ +store 54 +load 54 +int 65536 +< +assert +load 53 +itob +extract 6 0 +concat +load 5 +store 56 +load 55 +load 56 +concat +store 55 +load 54 +store 53 +load 53 +itob +extract 6 0 +concat +load 55 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_byte.teal b/tests/integration/teal/roundtrip/app_roundtrip_byte.teal new file mode 100644 index 000000000..b3d294763 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_byte.teal @@ -0,0 +1,55 @@ +#pragma version 6 +txna ApplicationArgs 0 +int 0 +getbyte +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 6 +int 255 +load 6 +- +store 7 +load 7 +int 256 +< +assert +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub numericalcomp_0 +store 4 +load 4 +callsub numericalcomp_0 +store 5 +byte 0x00 +int 0 +load 2 +setbyte +byte 0x00 +int 0 +load 4 +setbyte +concat +byte 0x00 +int 0 +load 5 +setbyte +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_string_<0>.teal b/tests/integration/teal/roundtrip/app_roundtrip_string_<0>.teal new file mode 100644 index 000000000..6ab75fba8 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_string_<0>.teal @@ -0,0 +1,92 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// string_reverse +stringreverse_0: +store 6 +int 0 +store 8 +load 8 +itob +extract 6 0 +byte "" +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub stringreverse_0 +store 4 +load 4 +callsub stringreverse_0 +store 5 +load 2 +store 12 +load 12 +store 11 +int 6 +store 9 +load 9 +load 12 +len ++ +store 10 +load 10 +int 65536 +< +assert +load 9 +itob +extract 6 0 +load 4 +store 12 +load 11 +load 12 +concat +store 11 +load 10 +store 9 +load 9 +load 12 +len ++ +store 10 +load 10 +int 65536 +< +assert +load 9 +itob +extract 6 0 +concat +load 5 +store 12 +load 11 +load 12 +concat +store 11 +load 10 +store 9 +load 9 +itob +extract 6 0 +concat +load 11 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_string_<13>.teal b/tests/integration/teal/roundtrip/app_roundtrip_string_<13>.teal new file mode 100644 index 000000000..086b2b970 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_string_<13>.teal @@ -0,0 +1,259 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// string_reverse +stringreverse_0: +store 6 +load 6 +int 1 +int 0 +* +int 2 ++ +getbyte +store 20 +load 6 +int 1 +int 1 +* +int 2 ++ +getbyte +store 19 +load 6 +int 1 +int 2 +* +int 2 ++ +getbyte +store 18 +load 6 +int 1 +int 3 +* +int 2 ++ +getbyte +store 17 +load 6 +int 1 +int 4 +* +int 2 ++ +getbyte +store 16 +load 6 +int 1 +int 5 +* +int 2 ++ +getbyte +store 15 +load 6 +int 1 +int 6 +* +int 2 ++ +getbyte +store 14 +load 6 +int 1 +int 7 +* +int 2 ++ +getbyte +store 13 +load 6 +int 1 +int 8 +* +int 2 ++ +getbyte +store 12 +load 6 +int 1 +int 9 +* +int 2 ++ +getbyte +store 11 +load 6 +int 1 +int 10 +* +int 2 ++ +getbyte +store 10 +load 6 +int 1 +int 11 +* +int 2 ++ +getbyte +store 9 +load 6 +int 1 +int 12 +* +int 2 ++ +getbyte +store 8 +int 13 +store 21 +load 21 +itob +extract 6 0 +byte 0x00 +int 0 +load 8 +setbyte +byte 0x00 +int 0 +load 9 +setbyte +concat +byte 0x00 +int 0 +load 10 +setbyte +concat +byte 0x00 +int 0 +load 11 +setbyte +concat +byte 0x00 +int 0 +load 12 +setbyte +concat +byte 0x00 +int 0 +load 13 +setbyte +concat +byte 0x00 +int 0 +load 14 +setbyte +concat +byte 0x00 +int 0 +load 15 +setbyte +concat +byte 0x00 +int 0 +load 16 +setbyte +concat +byte 0x00 +int 0 +load 17 +setbyte +concat +byte 0x00 +int 0 +load 18 +setbyte +concat +byte 0x00 +int 0 +load 19 +setbyte +concat +byte 0x00 +int 0 +load 20 +setbyte +concat +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub stringreverse_0 +store 4 +load 4 +callsub stringreverse_0 +store 5 +load 2 +store 25 +load 25 +store 24 +int 6 +store 22 +load 22 +load 25 +len ++ +store 23 +load 23 +int 65536 +< +assert +load 22 +itob +extract 6 0 +load 4 +store 25 +load 24 +load 25 +concat +store 24 +load 23 +store 22 +load 22 +load 25 +len ++ +store 23 +load 23 +int 65536 +< +assert +load 22 +itob +extract 6 0 +concat +load 5 +store 25 +load 24 +load 25 +concat +store 24 +load 23 +store 22 +load 22 +itob +extract 6 0 +concat +load 24 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_string_<1>.teal b/tests/integration/teal/roundtrip/app_roundtrip_string_<1>.teal new file mode 100644 index 000000000..a97b99d60 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_string_<1>.teal @@ -0,0 +1,103 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// string_reverse +stringreverse_0: +store 6 +load 6 +int 1 +int 0 +* +int 2 ++ +getbyte +store 8 +int 1 +store 9 +load 9 +itob +extract 6 0 +byte 0x00 +int 0 +load 8 +setbyte +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub stringreverse_0 +store 4 +load 4 +callsub stringreverse_0 +store 5 +load 2 +store 13 +load 13 +store 12 +int 6 +store 10 +load 10 +load 13 +len ++ +store 11 +load 11 +int 65536 +< +assert +load 10 +itob +extract 6 0 +load 4 +store 13 +load 12 +load 13 +concat +store 12 +load 11 +store 10 +load 10 +load 13 +len ++ +store 11 +load 11 +int 65536 +< +assert +load 10 +itob +extract 6 0 +concat +load 5 +store 13 +load 12 +load 13 +concat +store 12 +load 11 +store 10 +load 10 +itob +extract 6 0 +concat +load 12 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint16.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint16.teal new file mode 100644 index 000000000..81e911ee3 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint16.teal @@ -0,0 +1,52 @@ +#pragma version 6 +txna ApplicationArgs 0 +int 0 +extract_uint16 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 6 +int 65535 +load 6 +- +store 7 +load 7 +int 65536 +< +assert +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub numericalcomp_0 +store 4 +load 4 +callsub numericalcomp_0 +store 5 +load 2 +itob +extract 6 0 +load 4 +itob +extract 6 0 +concat +load 5 +itob +extract 6 0 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint32.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint32.teal new file mode 100644 index 000000000..ff6b2cb0a --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint32.teal @@ -0,0 +1,52 @@ +#pragma version 6 +txna ApplicationArgs 0 +int 0 +extract_uint32 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 6 +int 4294967295 +load 6 +- +store 7 +load 7 +int 4294967296 +< +assert +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub numericalcomp_0 +store 4 +load 4 +callsub numericalcomp_0 +store 5 +load 2 +itob +extract 4 0 +load 4 +itob +extract 4 0 +concat +load 5 +itob +extract 4 0 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64.teal new file mode 100644 index 000000000..c20d6fec7 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64.teal @@ -0,0 +1,44 @@ +#pragma version 6 +txna ApplicationArgs 0 +btoi +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 6 +int 18446744073709551615 +load 6 +- +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub numericalcomp_0 +store 4 +load 4 +callsub numericalcomp_0 +store 5 +load 2 +itob +load 4 +itob +concat +load 5 +itob +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[1].teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[1].teal new file mode 100644 index 000000000..11a1cc180 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[1].teal @@ -0,0 +1,58 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 9 +int 18446744073709551615 +load 9 +- +store 10 +load 10 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 8 +int 0 +* +extract_uint64 +store 8 +load 8 +callsub numericalcomp_0 +store 8 +load 8 +itob +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +load 4 +concat +load 5 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[42].teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[42].teal new file mode 100644 index 000000000..1f780d0f4 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[42].teal @@ -0,0 +1,550 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 50 +int 18446744073709551615 +load 50 +- +store 51 +load 51 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 8 +int 0 +* +extract_uint64 +store 8 +load 6 +int 8 +int 1 +* +extract_uint64 +store 9 +load 6 +int 8 +int 2 +* +extract_uint64 +store 10 +load 6 +int 8 +int 3 +* +extract_uint64 +store 11 +load 6 +int 8 +int 4 +* +extract_uint64 +store 12 +load 6 +int 8 +int 5 +* +extract_uint64 +store 13 +load 6 +int 8 +int 6 +* +extract_uint64 +store 14 +load 6 +int 8 +int 7 +* +extract_uint64 +store 15 +load 6 +int 8 +int 8 +* +extract_uint64 +store 16 +load 6 +int 8 +int 9 +* +extract_uint64 +store 17 +load 6 +int 8 +int 10 +* +extract_uint64 +store 18 +load 6 +int 8 +int 11 +* +extract_uint64 +store 19 +load 6 +int 8 +int 12 +* +extract_uint64 +store 20 +load 6 +int 8 +int 13 +* +extract_uint64 +store 21 +load 6 +int 8 +int 14 +* +extract_uint64 +store 22 +load 6 +int 8 +int 15 +* +extract_uint64 +store 23 +load 6 +int 8 +int 16 +* +extract_uint64 +store 24 +load 6 +int 8 +int 17 +* +extract_uint64 +store 25 +load 6 +int 8 +int 18 +* +extract_uint64 +store 26 +load 6 +int 8 +int 19 +* +extract_uint64 +store 27 +load 6 +int 8 +int 20 +* +extract_uint64 +store 28 +load 6 +int 8 +int 21 +* +extract_uint64 +store 29 +load 6 +int 8 +int 22 +* +extract_uint64 +store 30 +load 6 +int 8 +int 23 +* +extract_uint64 +store 31 +load 6 +int 8 +int 24 +* +extract_uint64 +store 32 +load 6 +int 8 +int 25 +* +extract_uint64 +store 33 +load 6 +int 8 +int 26 +* +extract_uint64 +store 34 +load 6 +int 8 +int 27 +* +extract_uint64 +store 35 +load 6 +int 8 +int 28 +* +extract_uint64 +store 36 +load 6 +int 8 +int 29 +* +extract_uint64 +store 37 +load 6 +int 8 +int 30 +* +extract_uint64 +store 38 +load 6 +int 8 +int 31 +* +extract_uint64 +store 39 +load 6 +int 8 +int 32 +* +extract_uint64 +store 40 +load 6 +int 8 +int 33 +* +extract_uint64 +store 41 +load 6 +int 8 +int 34 +* +extract_uint64 +store 42 +load 6 +int 8 +int 35 +* +extract_uint64 +store 43 +load 6 +int 8 +int 36 +* +extract_uint64 +store 44 +load 6 +int 8 +int 37 +* +extract_uint64 +store 45 +load 6 +int 8 +int 38 +* +extract_uint64 +store 46 +load 6 +int 8 +int 39 +* +extract_uint64 +store 47 +load 6 +int 8 +int 40 +* +extract_uint64 +store 48 +load 6 +int 8 +int 41 +* +extract_uint64 +store 49 +load 8 +callsub numericalcomp_0 +store 8 +load 9 +callsub numericalcomp_0 +store 9 +load 10 +callsub numericalcomp_0 +store 10 +load 11 +callsub numericalcomp_0 +store 11 +load 12 +callsub numericalcomp_0 +store 12 +load 13 +callsub numericalcomp_0 +store 13 +load 14 +callsub numericalcomp_0 +store 14 +load 15 +callsub numericalcomp_0 +store 15 +load 16 +callsub numericalcomp_0 +store 16 +load 17 +callsub numericalcomp_0 +store 17 +load 18 +callsub numericalcomp_0 +store 18 +load 19 +callsub numericalcomp_0 +store 19 +load 20 +callsub numericalcomp_0 +store 20 +load 21 +callsub numericalcomp_0 +store 21 +load 22 +callsub numericalcomp_0 +store 22 +load 23 +callsub numericalcomp_0 +store 23 +load 24 +callsub numericalcomp_0 +store 24 +load 25 +callsub numericalcomp_0 +store 25 +load 26 +callsub numericalcomp_0 +store 26 +load 27 +callsub numericalcomp_0 +store 27 +load 28 +callsub numericalcomp_0 +store 28 +load 29 +callsub numericalcomp_0 +store 29 +load 30 +callsub numericalcomp_0 +store 30 +load 31 +callsub numericalcomp_0 +store 31 +load 32 +callsub numericalcomp_0 +store 32 +load 33 +callsub numericalcomp_0 +store 33 +load 34 +callsub numericalcomp_0 +store 34 +load 35 +callsub numericalcomp_0 +store 35 +load 36 +callsub numericalcomp_0 +store 36 +load 37 +callsub numericalcomp_0 +store 37 +load 38 +callsub numericalcomp_0 +store 38 +load 39 +callsub numericalcomp_0 +store 39 +load 40 +callsub numericalcomp_0 +store 40 +load 41 +callsub numericalcomp_0 +store 41 +load 42 +callsub numericalcomp_0 +store 42 +load 43 +callsub numericalcomp_0 +store 43 +load 44 +callsub numericalcomp_0 +store 44 +load 45 +callsub numericalcomp_0 +store 45 +load 46 +callsub numericalcomp_0 +store 46 +load 47 +callsub numericalcomp_0 +store 47 +load 48 +callsub numericalcomp_0 +store 48 +load 49 +callsub numericalcomp_0 +store 49 +load 8 +itob +load 9 +itob +concat +load 10 +itob +concat +load 11 +itob +concat +load 12 +itob +concat +load 13 +itob +concat +load 14 +itob +concat +load 15 +itob +concat +load 16 +itob +concat +load 17 +itob +concat +load 18 +itob +concat +load 19 +itob +concat +load 20 +itob +concat +load 21 +itob +concat +load 22 +itob +concat +load 23 +itob +concat +load 24 +itob +concat +load 25 +itob +concat +load 26 +itob +concat +load 27 +itob +concat +load 28 +itob +concat +load 29 +itob +concat +load 30 +itob +concat +load 31 +itob +concat +load 32 +itob +concat +load 33 +itob +concat +load 34 +itob +concat +load 35 +itob +concat +load 36 +itob +concat +load 37 +itob +concat +load 38 +itob +concat +load 39 +itob +concat +load 40 +itob +concat +load 41 +itob +concat +load 42 +itob +concat +load 43 +itob +concat +load 44 +itob +concat +load 45 +itob +concat +load 46 +itob +concat +load 47 +itob +concat +load 48 +itob +concat +load 49 +itob +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +load 4 +concat +load 5 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<0>.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<0>.teal new file mode 100644 index 000000000..710b6a859 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<0>.teal @@ -0,0 +1,92 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// array_complement +arraycomplement_0: +store 6 +int 0 +store 8 +load 8 +itob +extract 6 0 +byte "" +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub arraycomplement_0 +store 4 +load 4 +callsub arraycomplement_0 +store 5 +load 2 +store 12 +load 12 +store 11 +int 6 +store 9 +load 9 +load 12 +len ++ +store 10 +load 10 +int 65536 +< +assert +load 9 +itob +extract 6 0 +load 4 +store 12 +load 11 +load 12 +concat +store 11 +load 10 +store 9 +load 9 +load 12 +len ++ +store 10 +load 10 +int 65536 +< +assert +load 9 +itob +extract 6 0 +concat +load 5 +store 12 +load 11 +load 12 +concat +store 11 +load 10 +store 9 +load 9 +itob +extract 6 0 +concat +load 11 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<1>.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<1>.teal new file mode 100644 index 000000000..570b2cb20 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<1>.teal @@ -0,0 +1,114 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 9 +int 18446744073709551615 +load 9 +- +store 10 +load 10 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 8 +int 0 +* +int 2 ++ +extract_uint64 +store 8 +load 8 +callsub numericalcomp_0 +store 8 +int 1 +store 11 +load 11 +itob +extract 6 0 +load 8 +itob +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +store 15 +load 15 +store 14 +int 6 +store 12 +load 12 +load 15 +len ++ +store 13 +load 13 +int 65536 +< +assert +load 12 +itob +extract 6 0 +load 4 +store 15 +load 14 +load 15 +concat +store 14 +load 13 +store 12 +load 12 +load 15 +len ++ +store 13 +load 13 +int 65536 +< +assert +load 12 +itob +extract 6 0 +concat +load 5 +store 15 +load 14 +load 15 +concat +store 14 +load 13 +store 12 +load 12 +itob +extract 6 0 +concat +load 14 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<42>.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<42>.teal new file mode 100644 index 000000000..869f062b1 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint64[]_<42>.teal @@ -0,0 +1,688 @@ +#pragma version 6 +txna ApplicationArgs 0 +store 1 +load 1 +callsub roundtripper_2 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 50 +int 18446744073709551615 +load 50 +- +store 51 +load 51 +retsub + +// array_complement +arraycomplement_1: +store 6 +load 6 +int 8 +int 0 +* +int 2 ++ +extract_uint64 +store 8 +load 6 +int 8 +int 1 +* +int 2 ++ +extract_uint64 +store 9 +load 6 +int 8 +int 2 +* +int 2 ++ +extract_uint64 +store 10 +load 6 +int 8 +int 3 +* +int 2 ++ +extract_uint64 +store 11 +load 6 +int 8 +int 4 +* +int 2 ++ +extract_uint64 +store 12 +load 6 +int 8 +int 5 +* +int 2 ++ +extract_uint64 +store 13 +load 6 +int 8 +int 6 +* +int 2 ++ +extract_uint64 +store 14 +load 6 +int 8 +int 7 +* +int 2 ++ +extract_uint64 +store 15 +load 6 +int 8 +int 8 +* +int 2 ++ +extract_uint64 +store 16 +load 6 +int 8 +int 9 +* +int 2 ++ +extract_uint64 +store 17 +load 6 +int 8 +int 10 +* +int 2 ++ +extract_uint64 +store 18 +load 6 +int 8 +int 11 +* +int 2 ++ +extract_uint64 +store 19 +load 6 +int 8 +int 12 +* +int 2 ++ +extract_uint64 +store 20 +load 6 +int 8 +int 13 +* +int 2 ++ +extract_uint64 +store 21 +load 6 +int 8 +int 14 +* +int 2 ++ +extract_uint64 +store 22 +load 6 +int 8 +int 15 +* +int 2 ++ +extract_uint64 +store 23 +load 6 +int 8 +int 16 +* +int 2 ++ +extract_uint64 +store 24 +load 6 +int 8 +int 17 +* +int 2 ++ +extract_uint64 +store 25 +load 6 +int 8 +int 18 +* +int 2 ++ +extract_uint64 +store 26 +load 6 +int 8 +int 19 +* +int 2 ++ +extract_uint64 +store 27 +load 6 +int 8 +int 20 +* +int 2 ++ +extract_uint64 +store 28 +load 6 +int 8 +int 21 +* +int 2 ++ +extract_uint64 +store 29 +load 6 +int 8 +int 22 +* +int 2 ++ +extract_uint64 +store 30 +load 6 +int 8 +int 23 +* +int 2 ++ +extract_uint64 +store 31 +load 6 +int 8 +int 24 +* +int 2 ++ +extract_uint64 +store 32 +load 6 +int 8 +int 25 +* +int 2 ++ +extract_uint64 +store 33 +load 6 +int 8 +int 26 +* +int 2 ++ +extract_uint64 +store 34 +load 6 +int 8 +int 27 +* +int 2 ++ +extract_uint64 +store 35 +load 6 +int 8 +int 28 +* +int 2 ++ +extract_uint64 +store 36 +load 6 +int 8 +int 29 +* +int 2 ++ +extract_uint64 +store 37 +load 6 +int 8 +int 30 +* +int 2 ++ +extract_uint64 +store 38 +load 6 +int 8 +int 31 +* +int 2 ++ +extract_uint64 +store 39 +load 6 +int 8 +int 32 +* +int 2 ++ +extract_uint64 +store 40 +load 6 +int 8 +int 33 +* +int 2 ++ +extract_uint64 +store 41 +load 6 +int 8 +int 34 +* +int 2 ++ +extract_uint64 +store 42 +load 6 +int 8 +int 35 +* +int 2 ++ +extract_uint64 +store 43 +load 6 +int 8 +int 36 +* +int 2 ++ +extract_uint64 +store 44 +load 6 +int 8 +int 37 +* +int 2 ++ +extract_uint64 +store 45 +load 6 +int 8 +int 38 +* +int 2 ++ +extract_uint64 +store 46 +load 6 +int 8 +int 39 +* +int 2 ++ +extract_uint64 +store 47 +load 6 +int 8 +int 40 +* +int 2 ++ +extract_uint64 +store 48 +load 6 +int 8 +int 41 +* +int 2 ++ +extract_uint64 +store 49 +load 8 +callsub numericalcomp_0 +store 8 +load 9 +callsub numericalcomp_0 +store 9 +load 10 +callsub numericalcomp_0 +store 10 +load 11 +callsub numericalcomp_0 +store 11 +load 12 +callsub numericalcomp_0 +store 12 +load 13 +callsub numericalcomp_0 +store 13 +load 14 +callsub numericalcomp_0 +store 14 +load 15 +callsub numericalcomp_0 +store 15 +load 16 +callsub numericalcomp_0 +store 16 +load 17 +callsub numericalcomp_0 +store 17 +load 18 +callsub numericalcomp_0 +store 18 +load 19 +callsub numericalcomp_0 +store 19 +load 20 +callsub numericalcomp_0 +store 20 +load 21 +callsub numericalcomp_0 +store 21 +load 22 +callsub numericalcomp_0 +store 22 +load 23 +callsub numericalcomp_0 +store 23 +load 24 +callsub numericalcomp_0 +store 24 +load 25 +callsub numericalcomp_0 +store 25 +load 26 +callsub numericalcomp_0 +store 26 +load 27 +callsub numericalcomp_0 +store 27 +load 28 +callsub numericalcomp_0 +store 28 +load 29 +callsub numericalcomp_0 +store 29 +load 30 +callsub numericalcomp_0 +store 30 +load 31 +callsub numericalcomp_0 +store 31 +load 32 +callsub numericalcomp_0 +store 32 +load 33 +callsub numericalcomp_0 +store 33 +load 34 +callsub numericalcomp_0 +store 34 +load 35 +callsub numericalcomp_0 +store 35 +load 36 +callsub numericalcomp_0 +store 36 +load 37 +callsub numericalcomp_0 +store 37 +load 38 +callsub numericalcomp_0 +store 38 +load 39 +callsub numericalcomp_0 +store 39 +load 40 +callsub numericalcomp_0 +store 40 +load 41 +callsub numericalcomp_0 +store 41 +load 42 +callsub numericalcomp_0 +store 42 +load 43 +callsub numericalcomp_0 +store 43 +load 44 +callsub numericalcomp_0 +store 44 +load 45 +callsub numericalcomp_0 +store 45 +load 46 +callsub numericalcomp_0 +store 46 +load 47 +callsub numericalcomp_0 +store 47 +load 48 +callsub numericalcomp_0 +store 48 +load 49 +callsub numericalcomp_0 +store 49 +int 42 +store 52 +load 52 +itob +extract 6 0 +load 8 +itob +load 9 +itob +concat +load 10 +itob +concat +load 11 +itob +concat +load 12 +itob +concat +load 13 +itob +concat +load 14 +itob +concat +load 15 +itob +concat +load 16 +itob +concat +load 17 +itob +concat +load 18 +itob +concat +load 19 +itob +concat +load 20 +itob +concat +load 21 +itob +concat +load 22 +itob +concat +load 23 +itob +concat +load 24 +itob +concat +load 25 +itob +concat +load 26 +itob +concat +load 27 +itob +concat +load 28 +itob +concat +load 29 +itob +concat +load 30 +itob +concat +load 31 +itob +concat +load 32 +itob +concat +load 33 +itob +concat +load 34 +itob +concat +load 35 +itob +concat +load 36 +itob +concat +load 37 +itob +concat +load 38 +itob +concat +load 39 +itob +concat +load 40 +itob +concat +load 41 +itob +concat +load 42 +itob +concat +load 43 +itob +concat +load 44 +itob +concat +load 45 +itob +concat +load 46 +itob +concat +load 47 +itob +concat +load 48 +itob +concat +load 49 +itob +concat +concat +store 7 +load 7 +retsub + +// round_tripper +roundtripper_2: +store 2 +load 2 +callsub arraycomplement_1 +store 4 +load 4 +callsub arraycomplement_1 +store 5 +load 2 +store 56 +load 56 +store 55 +int 6 +store 53 +load 53 +load 56 +len ++ +store 54 +load 54 +int 65536 +< +assert +load 53 +itob +extract 6 0 +load 4 +store 56 +load 55 +load 56 +concat +store 55 +load 54 +store 53 +load 53 +load 56 +len ++ +store 54 +load 54 +int 65536 +< +assert +load 53 +itob +extract 6 0 +concat +load 5 +store 56 +load 55 +load 56 +concat +store 55 +load 54 +store 53 +load 53 +itob +extract 6 0 +concat +load 55 +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/integration/teal/roundtrip/app_roundtrip_uint8.teal b/tests/integration/teal/roundtrip/app_roundtrip_uint8.teal new file mode 100644 index 000000000..b3d294763 --- /dev/null +++ b/tests/integration/teal/roundtrip/app_roundtrip_uint8.teal @@ -0,0 +1,55 @@ +#pragma version 6 +txna ApplicationArgs 0 +int 0 +getbyte +store 1 +load 1 +callsub roundtripper_1 +store 0 +byte 0x151f7c75 +load 0 +concat +log +int 1 +return + +// numerical_comp +numericalcomp_0: +store 6 +int 255 +load 6 +- +store 7 +load 7 +int 256 +< +assert +load 7 +retsub + +// round_tripper +roundtripper_1: +store 2 +load 2 +callsub numericalcomp_0 +store 4 +load 4 +callsub numericalcomp_0 +store 5 +byte 0x00 +int 0 +load 2 +setbyte +byte 0x00 +int 0 +load 4 +setbyte +concat +byte 0x00 +int 0 +load 5 +setbyte +concat +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/mock_version.py b/tests/mock_version.py new file mode 100644 index 000000000..d7473fcb6 --- /dev/null +++ b/tests/mock_version.py @@ -0,0 +1,20 @@ +import pytest +import pkg_resources + + +@pytest.fixture +def mock_version(version: str, monkeypatch: pytest.MonkeyPatch): + def mocked_require(name: str): + if ( + name == "pyteal" + and version is not None # don't mock if no version is specified + ): + return [ + pkg_resources.Distribution( + version=version, + ) + ] + else: + return pkg_resources.require(name)[0] + + monkeypatch.setattr(pkg_resources, "require", mocked_require) diff --git a/tests/teal/user_guide_snippet_dynamic_scratch_var.teal b/tests/teal/user_guide_snippet_dynamic_scratch_var.teal new file mode 100644 index 000000000..fe74bc0a3 --- /dev/null +++ b/tests/teal/user_guide_snippet_dynamic_scratch_var.teal @@ -0,0 +1,17 @@ +#pragma version 6 +int 0 +store 1 +int 7 +store 0 +load 1 +load 1 +loads +int 3 ++ +stores +load 0 +int 10 +== +assert +int 1 +return \ No newline at end of file diff --git a/tests/teal/user_guide_snippet_recursiveIsEven.teal b/tests/teal/user_guide_snippet_recursiveIsEven.teal new file mode 100644 index 000000000..61087d692 --- /dev/null +++ b/tests/teal/user_guide_snippet_recursiveIsEven.teal @@ -0,0 +1,32 @@ +#pragma version 6 +int 15 +callsub recursiveIsEven_0 +return + +// recursiveIsEven +recursiveIsEven_0: +store 0 +load 0 +int 0 +== +bnz recursiveIsEven_0_l4 +load 0 +int 1 +== +bnz recursiveIsEven_0_l3 +load 0 +int 2 +- +load 0 +swap +callsub recursiveIsEven_0 +swap +store 0 +b recursiveIsEven_0_l5 +recursiveIsEven_0_l3: +int 0 +b recursiveIsEven_0_l5 +recursiveIsEven_0_l4: +int 1 +recursiveIsEven_0_l5: +retsub \ No newline at end of file diff --git a/tests/unit/blackbox_test.py b/tests/unit/blackbox_test.py index 0cb71c80b..69ee59187 100644 --- a/tests/unit/blackbox_test.py +++ b/tests/unit/blackbox_test.py @@ -1,10 +1,11 @@ from itertools import product from pathlib import Path import pytest +from typing import Literal, Optional, Tuple import pyteal as pt -from tests.blackbox import Blackbox, blackbox_pyteal +from tests.blackbox import Blackbox, BlackboxWrapper, PyTealDryRunExecutor from tests.compile_asserts import assert_teal_as_expected @@ -12,6 +13,8 @@ FIXTURES = PATH / "teal" GENERATED = PATH / "generated" +# ---- Subroutine Unit Test Examples ---- # + @Blackbox(input_types=[]) @pt.Subroutine(pt.TealType.none) @@ -75,19 +78,84 @@ def utest_any_args(x, y, z): ] +# ---- ABI Return Subroutine Unit Test Examples ---- # + + +@Blackbox(input_types=[]) +@pt.ABIReturnSubroutine +def fn_0arg_0ret() -> pt.Expr: + return pt.Return() + + +@Blackbox(input_types=[]) +@pt.ABIReturnSubroutine +def fn_0arg_uint64_ret(*, output: pt.abi.Uint64) -> pt.Expr: + return output.set(1) + + +@Blackbox(input_types=[None]) +@pt.ABIReturnSubroutine +def fn_1arg_0ret(a: pt.abi.Uint64) -> pt.Expr: + return pt.Return() + + +@Blackbox(input_types=[None]) +@pt.ABIReturnSubroutine +def fn_1arg_1ret(a: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: + return output.set(a) + + +@Blackbox(input_types=[None, None]) +@pt.ABIReturnSubroutine +def fn_2arg_0ret( + a: pt.abi.Uint64, b: pt.abi.StaticArray[pt.abi.Byte, Literal[10]] +) -> pt.Expr: + return pt.Return() + + +@Blackbox(input_types=[pt.TealType.bytes]) +@pt.ABIReturnSubroutine +def fn_1tt_arg_uint64_ret(x, *, output: pt.abi.Uint64) -> pt.Expr: + return output.set(1) + + +@Blackbox(input_types=[None, pt.TealType.uint64, None]) +@pt.ABIReturnSubroutine +def fn_3mixed_args_0ret( + a: pt.abi.Uint64, b: pt.ScratchVar, C: pt.abi.StaticArray[pt.abi.Byte, Literal[10]] +) -> pt.Expr: + return pt.Return() + + +@Blackbox(input_types=[None, pt.TealType.bytes]) +@pt.ABIReturnSubroutine +def fn_2mixed_arg_1ret( + a: pt.abi.Uint64, b: pt.ScratchVar, *, output: pt.abi.Uint64 +) -> pt.Expr: + return pt.Seq(b.store(a.encode()), output.set(a)) + + +ABI_UNITS = [ + (fn_0arg_0ret, None), + (fn_0arg_uint64_ret, pt.abi.Uint64()), + (fn_1arg_0ret, None), + (fn_1arg_1ret, pt.abi.Uint64()), + (fn_2arg_0ret, None), + (fn_1tt_arg_uint64_ret, pt.abi.Uint64()), + (fn_3mixed_args_0ret, None), + (fn_2mixed_arg_1ret, pt.abi.Uint64()), +] + + +# ---- test functions ---- # + + @pytest.mark.parametrize("subr, mode", product(UNITS, pt.Mode)) -def test_blackbox_pyteal(subr, mode): - """ - TODO: here's an example of issue #199 at play - (the thread-safety aspect): - compare the following! - % pytest -n 2 tests/unit/blackbox_test.py::test_blackbox_pyteal - vs - % pytest -n 1 tests/unit/blackbox_test.py::test_blackbox_pyteal - """ +def test_blackbox_pyteal(subr: BlackboxWrapper, mode: pt.Mode): is_app = mode == pt.Mode.Application name = f"{'app' if is_app else 'lsig'}_{subr.name()}" - compiled = pt.compileTeal(blackbox_pyteal(subr, mode)(), mode, version=6) + compiled = PyTealDryRunExecutor(subr, mode).compile(version=6) tealdir = GENERATED / "blackbox" tealdir.mkdir(parents=True, exist_ok=True) save_to = tealdir / (name + ".teal") @@ -95,3 +163,94 @@ def test_blackbox_pyteal(subr, mode): f.write(compiled) assert_teal_as_expected(save_to, FIXTURES / "blackbox" / (name + ".teal")) + + +@pytest.mark.parametrize("subr_abi, mode", product(ABI_UNITS, pt.Mode)) +def test_abi_blackbox_pyteal( + subr_abi: Tuple[BlackboxWrapper, Optional[pt.ast.abi.BaseType]], mode: pt.Mode +): + subr, abi_return_type = subr_abi + name = f"{'app' if mode == pt.Mode.Application else 'lsig'}_{subr.name()}" + print(f"Case {subr.name()=}, {abi_return_type=}, {mode=} ------> {name=}") + + pdre = PyTealDryRunExecutor(subr, mode) + assert pdre.is_abi(), "should be an ABI subroutine" + + arg_types = pdre.abi_argument_types() + if subr.name() != "fn_1tt_arg_uint64_ret": + assert not arg_types or any( + arg_types + ), "abi_argument_types() should have had some abi info" + + if abi_return_type: + expected_sdk_return_type = pt.abi.algosdk_from_type_spec( + abi_return_type.type_spec() + ) + assert expected_sdk_return_type == pdre.abi_return_type() + else: + assert pdre.abi_return_type() is None + + compiled = pdre.compile(version=6) + tealdir = GENERATED / "abi" + tealdir.mkdir(parents=True, exist_ok=True) + save_to = tealdir / (name + ".teal") + with open(save_to, "w") as f: + f.write(compiled) + + assert_teal_as_expected(save_to, FIXTURES / "abi" / (name + ".teal")) + + +@pytest.mark.parametrize("mode", (pt.Mode.Application, pt.Mode.Signature)) +@pytest.mark.parametrize( + "fn, expected_is_abi", ((utest_noop, False), (fn_0arg_uint64_ret, True)) +) +def test_PyTealBlackboxExecutor_is_abi( + mode: pt.Mode, fn: BlackboxWrapper, expected_is_abi: bool +): + p = PyTealDryRunExecutor(fn, mode) + assert p.is_abi() == expected_is_abi + if expected_is_abi: + assert p.abi_argument_types() is not None + assert p.abi_return_type() is not None + else: + assert p.abi_argument_types() is None + assert p.abi_return_type() is None + + +@pytest.mark.parametrize("mode", (pt.Mode.Application, pt.Mode.Signature)) +@pytest.mark.parametrize( + "fn, expected_arg_count", + ( + (fn_0arg_uint64_ret, 0), + (fn_1arg_0ret, 1), + (fn_1arg_1ret, 1), + (fn_2arg_0ret, 2), + (fn_2mixed_arg_1ret, 2), + ), +) +def test_PyTealBlackboxExecutor_abi_argument_types( + mode: pt.Mode, fn: BlackboxWrapper, expected_arg_count: int +): + actual = PyTealDryRunExecutor(fn, mode).abi_argument_types() + assert actual is not None + assert len(actual) == expected_arg_count + + +@pytest.mark.parametrize("mode", (pt.Mode.Application, pt.Mode.Signature)) +@pytest.mark.parametrize( + "fn, expected_does_produce_type", + ( + (fn_0arg_uint64_ret, True), + (fn_1arg_0ret, False), + (fn_1arg_1ret, True), + (fn_2arg_0ret, False), + (fn_2mixed_arg_1ret, True), + ), +) +def test_PyTealBlackboxExecutor_abi_return_type( + mode: pt.Mode, fn: BlackboxWrapper, expected_does_produce_type: bool +): + if expected_does_produce_type: + assert PyTealDryRunExecutor(fn, mode).abi_return_type() is not None + else: + assert PyTealDryRunExecutor(fn, mode).abi_return_type() is None diff --git a/tests/unit/compile_test.py b/tests/unit/compile_test.py index 9f901da26..427c7e4c6 100644 --- a/tests/unit/compile_test.py +++ b/tests/unit/compile_test.py @@ -1,9 +1,40 @@ from pathlib import Path import pytest +import json import pyteal as pt +def test_abi_algobank(): + from examples.application.abi.algobank import ( + approval_program, + clear_state_program, + contract, + ) + + target_dir = Path.cwd() / "examples" / "application" / "abi" + + with open( + target_dir / "algobank_approval.teal", "r" + ) as expected_approval_program_file: + expected_approval_program = "".join( + expected_approval_program_file.readlines() + ).strip() + assert approval_program == expected_approval_program + + with open( + target_dir / "algobank_clear_state.teal", "r" + ) as expected_clear_state_program_file: + expected_clear_state_program = "".join( + expected_clear_state_program_file.readlines() + ).strip() + assert clear_state_program == expected_clear_state_program + + with open(target_dir / "algobank.json", "r") as expected_contract_file: + expected_contract = json.load(expected_contract_file) + assert contract.dictify() == expected_contract + + def test_basic_bank(): from examples.signature.basic import bank_for_account diff --git a/tests/unit/teal/abi/app_fn_0arg_0ret.teal b/tests/unit/teal/abi/app_fn_0arg_0ret.teal new file mode 100644 index 000000000..93cb677df --- /dev/null +++ b/tests/unit/teal/abi/app_fn_0arg_0ret.teal @@ -0,0 +1,8 @@ +#pragma version 6 +callsub fn0arg0ret_0 +int 1 +return + +// fn_0arg_0ret +fn0arg0ret_0: +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/app_fn_0arg_uint64_ret.teal b/tests/unit/teal/abi/app_fn_0arg_uint64_ret.teal new file mode 100644 index 000000000..858410b51 --- /dev/null +++ b/tests/unit/teal/abi/app_fn_0arg_uint64_ret.teal @@ -0,0 +1,17 @@ +#pragma version 6 +callsub fn0arguint64ret_0 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return + +// fn_0arg_uint64_ret +fn0arguint64ret_0: +int 1 +store 0 +load 0 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/app_fn_1arg_0ret.teal b/tests/unit/teal/abi/app_fn_1arg_0ret.teal new file mode 100644 index 000000000..a21e04c5d --- /dev/null +++ b/tests/unit/teal/abi/app_fn_1arg_0ret.teal @@ -0,0 +1,13 @@ +#pragma version 6 +txna ApplicationArgs 0 +btoi +store 1 +load 1 +callsub fn1arg0ret_0 +int 1 +return + +// fn_1arg_0ret +fn1arg0ret_0: +store 0 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/app_fn_1arg_1ret.teal b/tests/unit/teal/abi/app_fn_1arg_1ret.teal new file mode 100644 index 000000000..f9d4574c7 --- /dev/null +++ b/tests/unit/teal/abi/app_fn_1arg_1ret.teal @@ -0,0 +1,22 @@ +#pragma version 6 +txna ApplicationArgs 0 +btoi +store 3 +load 3 +callsub fn1arg1ret_0 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return + +// fn_1arg_1ret +fn1arg1ret_0: +store 0 +load 0 +store 1 +load 1 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/app_fn_1tt_arg_uint64_ret.teal b/tests/unit/teal/abi/app_fn_1tt_arg_uint64_ret.teal new file mode 100644 index 000000000..791abfc8a --- /dev/null +++ b/tests/unit/teal/abi/app_fn_1tt_arg_uint64_ret.teal @@ -0,0 +1,19 @@ +#pragma version 6 +txna ApplicationArgs 0 +callsub fn1ttarguint64ret_0 +store 2 +byte 0x151f7c75 +load 2 +itob +concat +log +int 1 +return + +// fn_1tt_arg_uint64_ret +fn1ttarguint64ret_0: +store 0 +int 1 +store 1 +load 1 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/app_fn_2arg_0ret.teal b/tests/unit/teal/abi/app_fn_2arg_0ret.teal new file mode 100644 index 000000000..aec416290 --- /dev/null +++ b/tests/unit/teal/abi/app_fn_2arg_0ret.teal @@ -0,0 +1,17 @@ +#pragma version 6 +txna ApplicationArgs 0 +btoi +store 2 +txna ApplicationArgs 1 +store 3 +load 2 +load 3 +callsub fn2arg0ret_0 +int 1 +return + +// fn_2arg_0ret +fn2arg0ret_0: +store 1 +store 0 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/app_fn_2mixed_arg_1ret.teal b/tests/unit/teal/abi/app_fn_2mixed_arg_1ret.teal new file mode 100644 index 000000000..51c058bd6 --- /dev/null +++ b/tests/unit/teal/abi/app_fn_2mixed_arg_1ret.teal @@ -0,0 +1,30 @@ +#pragma version 6 +txna ApplicationArgs 0 +btoi +store 4 +txna ApplicationArgs 1 +store 5 +load 4 +int 5 +callsub fn2mixedarg1ret_0 +store 3 +byte 0x151f7c75 +load 3 +itob +concat +log +int 1 +return + +// fn_2mixed_arg_1ret +fn2mixedarg1ret_0: +store 1 +store 0 +load 1 +load 0 +itob +stores +load 0 +store 2 +load 2 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/app_fn_3mixed_args_0ret.teal b/tests/unit/teal/abi/app_fn_3mixed_args_0ret.teal new file mode 100644 index 000000000..0264a222e --- /dev/null +++ b/tests/unit/teal/abi/app_fn_3mixed_args_0ret.teal @@ -0,0 +1,22 @@ +#pragma version 6 +txna ApplicationArgs 0 +btoi +store 3 +txna ApplicationArgs 1 +btoi +store 4 +txna ApplicationArgs 2 +store 5 +load 3 +int 4 +load 5 +callsub fn3mixedargs0ret_0 +int 1 +return + +// fn_3mixed_args_0ret +fn3mixedargs0ret_0: +store 2 +store 1 +store 0 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_0arg_0ret.teal b/tests/unit/teal/abi/lsig_fn_0arg_0ret.teal new file mode 100644 index 000000000..93cb677df --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_0arg_0ret.teal @@ -0,0 +1,8 @@ +#pragma version 6 +callsub fn0arg0ret_0 +int 1 +return + +// fn_0arg_0ret +fn0arg0ret_0: +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_0arg_uint64_ret.teal b/tests/unit/teal/abi/lsig_fn_0arg_uint64_ret.teal new file mode 100644 index 000000000..e9355f259 --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_0arg_uint64_ret.teal @@ -0,0 +1,15 @@ +#pragma version 6 +callsub fn0arguint64ret_0 +store 0 +load 0 +itob +pop +int 1 +return + +// fn_0arg_uint64_ret +fn0arguint64ret_0: +int 1 +store 1 +load 1 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_1arg_0ret.teal b/tests/unit/teal/abi/lsig_fn_1arg_0ret.teal new file mode 100644 index 000000000..cf4a713c8 --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_1arg_0ret.teal @@ -0,0 +1,13 @@ +#pragma version 6 +arg 0 +btoi +store 0 +load 0 +callsub fn1arg0ret_0 +int 1 +return + +// fn_1arg_0ret +fn1arg0ret_0: +store 1 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_1arg_1ret.teal b/tests/unit/teal/abi/lsig_fn_1arg_1ret.teal new file mode 100644 index 000000000..b9316e7df --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_1arg_1ret.teal @@ -0,0 +1,20 @@ +#pragma version 6 +arg 0 +btoi +store 1 +load 1 +callsub fn1arg1ret_0 +store 0 +load 0 +itob +pop +int 1 +return + +// fn_1arg_1ret +fn1arg1ret_0: +store 2 +load 2 +store 3 +load 3 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_1tt_arg_uint64_ret.teal b/tests/unit/teal/abi/lsig_fn_1tt_arg_uint64_ret.teal new file mode 100644 index 000000000..8ea2f5585 --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_1tt_arg_uint64_ret.teal @@ -0,0 +1,17 @@ +#pragma version 6 +arg 0 +callsub fn1ttarguint64ret_0 +store 0 +load 0 +itob +pop +int 1 +return + +// fn_1tt_arg_uint64_ret +fn1ttarguint64ret_0: +store 1 +int 1 +store 2 +load 2 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_2arg_0ret.teal b/tests/unit/teal/abi/lsig_fn_2arg_0ret.teal new file mode 100644 index 000000000..c097d4e6c --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_2arg_0ret.teal @@ -0,0 +1,17 @@ +#pragma version 6 +arg 0 +btoi +store 0 +arg 1 +store 1 +load 0 +load 1 +callsub fn2arg0ret_0 +int 1 +return + +// fn_2arg_0ret +fn2arg0ret_0: +store 3 +store 2 +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_2mixed_arg_1ret.teal b/tests/unit/teal/abi/lsig_fn_2mixed_arg_1ret.teal new file mode 100644 index 000000000..5d2ae86b2 --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_2mixed_arg_1ret.teal @@ -0,0 +1,28 @@ +#pragma version 6 +arg 0 // [a] +btoi // [btoi(a)] +store 1 // 1 -> btoi(a) +arg 1 // [b] +store 2 // 2 -> b +load 1 // [btoi(a)] +int 2 // [btoi(a), 2] +callsub fn2mixedarg1ret_0 // [btoi(a)] +store 0 // 0 -> btoi(a) +load 0 // [btoi(a)] +itob // [a] +pop // [] +int 1 // [1] +return + +// fn_2mixed_arg_1ret +fn2mixedarg1ret_0: // [btoi(a), 2] +store 4 // 4 -> 2 +store 3 // 3 -> btoi(a) +load 4 // [2] +load 3 // [2, btoi(a)] +itob // [2, a] +stores // 2 -> a +load 3 // [btoi(a)] +store 5 // 5 -> btoi(a) +load 5 // [btoi(a)] +retsub \ No newline at end of file diff --git a/tests/unit/teal/abi/lsig_fn_3mixed_args_0ret.teal b/tests/unit/teal/abi/lsig_fn_3mixed_args_0ret.teal new file mode 100644 index 000000000..fca5fd198 --- /dev/null +++ b/tests/unit/teal/abi/lsig_fn_3mixed_args_0ret.teal @@ -0,0 +1,22 @@ +#pragma version 6 +arg 0 +btoi +store 0 +arg 1 +btoi +store 1 // 1 -> btoi(a) +arg 2 +store 2 +load 0 // [a] +int 1 // [a, 1] +load 2 // [a, 1, b] +callsub fn3mixedargs0ret_0 +int 1 +return + +// fn_3mixed_args_0ret +fn3mixedargs0ret_0: +store 5 +store 4 +store 3 +retsub \ No newline at end of file diff --git a/tests/unit/teal/user_guide/user_guide_snippet_ABIReturnSubroutine.teal b/tests/unit/teal/user_guide/user_guide_snippet_ABIReturnSubroutine.teal new file mode 100644 index 000000000..3d844d44d --- /dev/null +++ b/tests/unit/teal/user_guide/user_guide_snippet_ABIReturnSubroutine.teal @@ -0,0 +1,50 @@ +#pragma version 6 +txna ApplicationArgs 1 // x = abi.DynamicArray(abi.Uint64TypeSpec()) +store 0 // 0: x +load 0 // [x] +callsub abisum_0 +store 1 +byte 0x151f7c75 +load 1 +itob +concat +log +int 1 +return + +// abi_sum +abisum_0: // [x] +store 2 // 2: x +int 0 // [0] +store 3 // 3: 0 +int 0 // [0] +store 4 // 4: 0 +abisum_0_l1: // [] +load 4 +load 2 +int 0 // [0, x, 0] +extract_uint16 // [0, len(x)] +store 6 // 6: len(x) +load 6 // [0, len(x)] +< // [1] +bz abisum_0_l3 // [0] +load 2 // ... looks promising ... +int 8 +load 4 +* +int 2 ++ +extract_uint64 +store 5 +load 3 +load 5 ++ +store 3 +load 4 +int 1 ++ +store 4 +b abisum_0_l1 +abisum_0_l3: // [] +load 3 // [0] +retsub \ No newline at end of file diff --git a/tests/unit/user_guide_test.py b/tests/unit/user_guide_test.py index 809e5137e..da5461fc6 100644 --- a/tests/unit/user_guide_test.py +++ b/tests/unit/user_guide_test.py @@ -60,9 +60,52 @@ def ILLEGAL_recursion(i: ScratchVar): return Seq(i.store(Int(15)), ILLEGAL_recursion(i), Int(1)) +def user_guide_snippet_ABIReturnSubroutine(): + from pyteal import ( + ABIReturnSubroutine, + Expr, + For, + Int, + ScratchVar, + Seq, + Txn, + TealType, + ) + from pyteal import abi + + # --- BEGIN doc-comment --- # + @ABIReturnSubroutine + def abi_sum(to_sum: abi.DynamicArray[abi.Uint64], *, output: abi.Uint64) -> Expr: + i = ScratchVar(TealType.uint64) + value_at_index = abi.Uint64() + return Seq( + output.set(0), + For( + i.store(Int(0)), i.load() < to_sum.length(), i.store(i.load() + Int(1)) + ).Do( + Seq( + to_sum[i.load()].store_into(value_at_index), + output.set(output.get() + value_at_index.get()), + ) + ), + ) + + program = Seq( + (to_sum_arr := abi.make(abi.DynamicArray[abi.Uint64])).decode( + Txn.application_args[1] + ), + (res := abi.Uint64()).set(abi_sum(to_sum_arr)), + pt.abi.MethodReturn(res), + Int(1), + ) + # --- END doc-comment --- # + return program + + USER_GUIDE_SNIPPETS_COPACETIC = [ user_guide_snippet_dynamic_scratch_var, user_guide_snippet_recursiveIsEven, + user_guide_snippet_ABIReturnSubroutine, ]