diff --git a/.flake8 b/.flake8 index d1f5e3ba7..3b523ff59 100644 --- a/.flake8 +++ b/.flake8 @@ -23,8 +23,14 @@ per-file-ignores = examples/signature/recurring_swap.py: F403, F405 examples/signature/split.py: F403, F405 pyteal/__init__.py: F401, F403 + pyteal/compiler/flatten.py: F821 pyteal/compiler/optimizer/__init__.py: F401 pyteal/ir/ops.py: E221 + pyteal/ir/tealconditionalblock.py: F821 + pyteal/ir/tealsimpleblock.py: F821 + pyteal/stack_frame.py: F821 + tests/teal/rps_helpers/program.py: F401, F403, F405 + tests/teal/rps.py: F403, F405, I252 tests/unit/module_test.py: F401, F403 # from flake8-tidy-imports diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5783fa850..29cbcc407 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: "Build workflow" +name: "On Commit Workflow" on: pull_request: push: @@ -49,7 +49,7 @@ jobs: runs-on: ubuntu-20.04 strategy: matrix: - python: [ "3.10" ] + python: ["3.10"] steps: - name: Check out code uses: actions/checkout@v2 diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 449b552f2..c8f9be018 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -1,7 +1,7 @@ name: "Nightly Build" on: schedule: - - cron: "10 17 * * *" + - cron: "10 1 * * *" jobs: nightly-slow-unit-tests: diff --git a/CHANGELOG.md b/CHANGELOG.md index 70b053300..fcdf235ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ * Clarify that `Approve` and `Reject` always exit in the documentation. ([#660](https://github.com/algorand/pyteal/pull/660)) * Added frame pointer support for router. ([#600](https://github.com/algorand/pyteal/pull/600)) - * NOTE: a backwards incompatable change was imposed in this PR: previous `build_program` method in `Router` was exported and public, now this method is hidden. Use `compile_program` only. + * NOTE: a backwards incompatable change was imposed in this PR: previous `build_program` method in `Router` was exported and public, now this method is hidden. Use `compile_program` only. +* Experimental source mapping capability. ([#650](https://github.com/algorand/pyteal/pull/650)) ## Fixed diff --git a/Makefile b/Makefile index 690804429..dd4c25e5f 100644 --- a/Makefile +++ b/Makefile @@ -21,6 +21,8 @@ bdist-wheel: bundle-docs-clean: rm -rf docs/pyteal.docset + rm -rf docs/_build + rm -f docs/pyteal.docset.tar.gz bundle-docs: bundle-docs-clean cd docs && \ @@ -51,14 +53,19 @@ lint: black flake8 mypy sdist-check # ---- Unit Tests (no algod) ---- # -# TODO: add blackbox_test.py to multithreaded tests when following issue has been fixed https://github.com/algorand/pyteal/issues/199 +# Slow test which are fast enough on python 3.11+ +test-unit-slow: + pytest tests/unit/sourcemap_constructs311_test.py -m serial + +test-unit-very-slow: + pytest tests/unit/sourcemap_constructs_allpy_test.py -m serial -NUM_PROCS = auto test-unit-async: - pytest -n $(NUM_PROCS) --durations=10 -sv pyteal tests/unit -m "not serial" + pytest -n auto --durations=10 pyteal tests/unit -m "not slow" -m "not serial" -test-unit-sync: - pytest --durations=10 -sv pyteal tests/unit -m serial +# Run tests w/ @pytest.mark.serial under ~/tests/unit each in its own proc: +test-unit-sync: test-unit-slow + find tests/unit -name '*_test.py' | sort | xargs -t -I {} pytest --suppress-no-test-exit-code --dist=no --durations=10 {} -m serial -m "not slow" test-unit: test-unit-async test-unit-sync @@ -76,13 +83,19 @@ algod-start-report: algod-start algod-version algod-stop: docker compose stop algod -integration-run: - pytest -n $(NUM_PROCS) --durations=10 -sv tests/integration -m "not serial" - pytest --durations=10 -sv tests/integration -m serial -test-integration: integration-run +test-integ-async: + pytest -n auto --durations=10 -sv tests/integration -m "not serial" + +# Run tests w/ @pytest.mark.serial under ~/tests/integration each in its own proc: +test-integ-sync: + find tests/integration -name '*_test.py' | sort | xargs -t -I {} pytest --suppress-no-test-exit-code --dist=no --durations=10 {} -m serial + +test-integration: test-integ-async test-integ-sync -all-tests: lint-and-test test-integration +all-sync: test-unit-sync test-integ-sync + +all-lint-unit-integ: lint-and-test test-integration # ---- Github Actions 1-Liners ---- # @@ -94,8 +107,7 @@ check-code-changes: git config --global --add safe.directory /__w/pyteal/pyteal [ -n "$$(git log --since='24 hours ago')" ] && (echo "should_run=true" >> $(GITHUB_ENV)) || (echo "should_run=false" >> $(GITHUB_ENV)) -nightly-slow: - echo "TODO - this is a stub for a very slow test" +nightly-slow: test-unit-very-slow # ---- Local Github Actions Simulation via `act` ---- # # assumes act is installed, e.g. via `brew install act` @@ -111,3 +123,6 @@ local-gh-simulate: coverage: pytest --cov-report html --cov=pyteal + +sourcemap-coverage: + pytest --cov-report html --cov=pyteal.stack_frame --cov=pyteal.compiler.sourcemap --cov=pyteal.compiler.compiler --dist=no tests/unit/sourcemap_monkey_unit_test.py -m serial diff --git a/docs/requirements.txt b/docs/requirements.txt index 94121dd46..117f5b9d5 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,6 +1,8 @@ sphinx==5.1.1 sphinx-rtd-theme==1.0.0 # dependencies from setup.py +docstring-parser==0.14.1 +executing==1.2.0 py-algorand-sdk>=2.0.0,<3.0.0 semantic-version>=2.9.0,<3.0.0 -docstring-parser==0.14.1 +tabulate>=0.9.0,<0.10.0 diff --git a/examples/application/abi/algobank.py b/examples/application/abi/algobank.py index 34303ffe3..c8af8859d 100644 --- a/examples/application/abi/algobank.py +++ b/examples/application/abi/algobank.py @@ -112,11 +112,11 @@ def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: ) -approval_program, clear_state_program, contract = router.compile_program( - version=6, optimize=OptimizeOptions(scratch_slots=True) -) - if __name__ == "__main__": + approval_program, clear_state_program, contract = router.compile_program( + version=6, optimize=OptimizeOptions(scratch_slots=True) + ) + with open("algobank_approval.teal", "w") as f: f.write(approval_program) diff --git a/pyteal.ini b/pyteal.ini new file mode 100644 index 000000000..c525da425 --- /dev/null +++ b/pyteal.ini @@ -0,0 +1,5 @@ +[pyteal] + +[pyteal-source-mapper] +enabled = False +debug = False diff --git a/pyteal/__init__.py b/pyteal/__init__.py index bef7d0fdd..47eead933 100644 --- a/pyteal/__init__.py +++ b/pyteal/__init__.py @@ -11,11 +11,15 @@ MIN_PROGRAM_VERSION, DEFAULT_PROGRAM_VERSION, CompileOptions, + Compilation, compileTeal, OptimizeOptions, + PyTealSourceMap, ) from pyteal.types import TealType from pyteal.errors import ( + AlgodClientError, + SourceMapDisabledError, TealInternalError, TealTypeError, TealSeqError, @@ -43,9 +47,13 @@ "DEFAULT_PROGRAM_VERSION", "CompileOptions", "pragma", + "Compilation", "compileTeal", "OptimizeOptions", + "PyTealSourceMap", "TealType", + "AlgodClientError", + "SourceMapDisabledError", "TealInternalError", "TealTypeError", "TealSeqError", diff --git a/pyteal/__init__.pyi b/pyteal/__init__.pyi index d1c6b10a4..b99ed5441 100644 --- a/pyteal/__init__.pyi +++ b/pyteal/__init__.pyi @@ -14,11 +14,15 @@ from pyteal.compiler import ( MIN_PROGRAM_VERSION, DEFAULT_PROGRAM_VERSION, CompileOptions, + Compilation, compileTeal, OptimizeOptions, + PyTealSourceMap, ) from pyteal.types import TealType from pyteal.errors import ( + AlgodClientError, + SourceMapDisabledError, TealInternalError, TealTypeError, TealSeqError, @@ -39,6 +43,7 @@ __all__ = [ "AccountParamObject", "Add", "Addr", + "AlgodClientError", "And", "App", "AppField", @@ -91,6 +96,7 @@ __all__ = [ "BytesZero", "CallConfig", "Comment", + "Compilation", "CompileOptions", "Concat", "Cond", @@ -175,6 +181,7 @@ __all__ = [ "Or", "Pop", "Pragma", + "PyTealSourceMap", "RETURN_HASH_PREFIX", "Reject", "Replace", @@ -194,6 +201,7 @@ __all__ = [ "Sha512_256", "ShiftLeft", "ShiftRight", + "SourceMapDisabledError", "Sqrt", "Subroutine", "SubroutineCall", @@ -210,6 +218,7 @@ __all__ = [ "TealInternalError", "TealLabel", "TealOp", + "TealPragma", "TealPragmaError", "TealSeqError", "TealSimpleBlock", diff --git a/pyteal/ast/abi/method_return.py b/pyteal/ast/abi/method_return.py index 24566b5bd..3a36abb95 100644 --- a/pyteal/ast/abi/method_return.py +++ b/pyteal/ast/abi/method_return.py @@ -6,8 +6,9 @@ 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 +from pyteal.ir import TealBlock, TealSimpleBlock, Op +from pyteal.stack_frame import NatalStackFrame if TYPE_CHECKING: from pyteal.compiler import CompileOptions @@ -19,15 +20,18 @@ def __init__(self, arg: BaseType): if not isinstance(arg, BaseType): raise TealInputError(f"Expecting an ABI type argument but get {arg}") self.arg = arg + self._sframes_container: Expr | None = None 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__( + start, end = Log(Concat(Bytes(RETURN_HASH_PREFIX), self.arg.encode())).__teal__( options ) + NatalStackFrame.reframe_ops_in_blocks(self._sframes_container or self, start) + return start, end def __str__(self) -> str: return f"(MethodReturn {self.arg.type_spec()})" diff --git a/pyteal/ast/app.py b/pyteal/ast/app.py index e6f9d0de9..1e01e6abc 100644 --- a/pyteal/ast/app.py +++ b/pyteal/ast/app.py @@ -25,12 +25,12 @@ class OnComplete: """An enum of values that :any:`TxnObject.on_completion()` may return.""" - NoOp = EnumInt("NoOp") - OptIn = EnumInt("OptIn") - CloseOut = EnumInt("CloseOut") - ClearState = EnumInt("ClearState") - UpdateApplication = EnumInt("UpdateApplication") - DeleteApplication = EnumInt("DeleteApplication") + NoOp = EnumInt("NoOp") # T2PT8 + OptIn = EnumInt("OptIn") # T2PT8 + CloseOut = EnumInt("CloseOut") # T2PT8 + ClearState = EnumInt("ClearState") # T2PT8 + UpdateApplication = EnumInt("UpdateApplication") # T2PT8 + DeleteApplication = EnumInt("DeleteApplication") # T2PT8 OnComplete.__module__ = "pyteal" diff --git a/pyteal/ast/assert_.py b/pyteal/ast/assert_.py index 9444e9262..5e0074919 100644 --- a/pyteal/ast/assert_.py +++ b/pyteal/ast/assert_.py @@ -30,6 +30,7 @@ def __init__( self.comment = comment self.cond = [cond] + list(additional_conds) + self._sframes_container: Expr | None = None def __teal__(self, options: "CompileOptions"): if len(self.cond) > 1: @@ -37,6 +38,7 @@ def __teal__(self, options: "CompileOptions"): for cond in self.cond: asrt = Assert(cond, comment=self.comment) asrt.trace = cond.trace + asrt._sframes_container = cond asserts.append(asrt) return Seq(*asserts).__teal__(options) diff --git a/pyteal/ast/cond.py b/pyteal/ast/cond.py index 4f4635419..6d5f0aad1 100644 --- a/pyteal/ast/cond.py +++ b/pyteal/ast/cond.py @@ -77,12 +77,12 @@ def __init__(self, *argv: List[Expr]): def __teal__(self, options: "CompileOptions"): start = None end = TealSimpleBlock([]) - prevBranch = None + prevBranch: TealConditionalBlock | None = None for i, (cond, pred) in enumerate(self.args): condStart, condEnd = cond.__teal__(options) predStart, predEnd = pred.__teal__(options) - branchBlock = TealConditionalBlock([]) + branchBlock = TealConditionalBlock([], root_expr=cond) branchBlock.setTrueBlock(predStart) condEnd.setNextBlock(branchBlock) @@ -91,6 +91,7 @@ def __teal__(self, options: "CompileOptions"): start = condStart else: cast(TealConditionalBlock, prevBranch).setFalseBlock(condStart) + prevBranch = branchBlock errBlock = TealSimpleBlock([TealOp(self, Op.err)]) diff --git a/pyteal/ast/expr.py b/pyteal/ast/expr.py index 0f57eebce..761392513 100644 --- a/pyteal/ast/expr.py +++ b/pyteal/ast/expr.py @@ -1,8 +1,9 @@ from abc import ABC, abstractmethod from typing import TYPE_CHECKING -from pyteal.types import TealType from pyteal.ir import TealBlock, TealSimpleBlock +from pyteal.stack_frame import NatalStackFrame +from pyteal.types import TealType if TYPE_CHECKING: from pyteal.compiler import CompileOptions @@ -15,6 +16,7 @@ def __init__(self): import traceback self.trace = traceback.format_stack()[0:-1] + self.stack_frames: NatalStackFrame = NatalStackFrame() def getDefinitionTrace(self) -> list[str]: return self.trace diff --git a/pyteal/ast/for_.py b/pyteal/ast/for_.py index a21ff9f7f..29bb5b2ef 100644 --- a/pyteal/ast/for_.py +++ b/pyteal/ast/for_.py @@ -55,9 +55,10 @@ def __teal__(self, options: "CompileOptions"): stepStart, stepEnd = self.step.__teal__(options) stepEnd.setNextBlock(condStart) + stepEnd._sframes_container = self doEnd.setNextBlock(stepStart) - branchBlock = TealConditionalBlock([]) + branchBlock = TealConditionalBlock([], root_expr=self) branchBlock.setTrueBlock(doStart) branchBlock.setFalseBlock(end) diff --git a/pyteal/ast/frame.py b/pyteal/ast/frame.py index 19dcec12a..c55729934 100644 --- a/pyteal/ast/frame.py +++ b/pyteal/ast/frame.py @@ -8,6 +8,7 @@ from pyteal.types import TealType, require_type from pyteal.errors import TealInputError, TealInternalError, verifyProgramVersion from pyteal.ir import TealBlock, TealSimpleBlock, TealOp, Op +from pyteal.stack_frame import NatalStackFrame if TYPE_CHECKING: from pyteal.compiler import CompileOptions @@ -190,6 +191,7 @@ def __teal__(self, options: "CompileOptions") -> tuple[TealBlock, TealSimpleBloc proto_srt, proto_end = TealBlock.FromOp(options, op) local_srt, local_end = self.mem_layout.__teal__(options) proto_end.setNextBlock(local_srt) + NatalStackFrame.reframe_ops_in_blocks(self, proto_srt) return proto_srt, local_end def __str__(self) -> str: diff --git a/pyteal/ast/if_.py b/pyteal/ast/if_.py index 18e0df3ad..8bc2a9ed8 100644 --- a/pyteal/ast/if_.py +++ b/pyteal/ast/if_.py @@ -1,13 +1,13 @@ from typing import TYPE_CHECKING +from pyteal.ast.expr import Expr +from pyteal.ast.seq import _use_seq_if_multiple from pyteal.errors import ( TealCompileError, TealInputError, ) from pyteal.types import TealType, require_type from pyteal.ir import TealSimpleBlock, TealConditionalBlock -from pyteal.ast.expr import Expr -from pyteal.ast.seq import _use_seq_if_multiple if TYPE_CHECKING: from pyteal.compiler import CompileOptions @@ -49,6 +49,8 @@ def __init__( self.thenBranch = thenBranch self.elseBranch = elseBranch + self._label_cond: Expr | None = None + def __teal__(self, options: "CompileOptions"): if self.thenBranch is None: raise TealCompileError("If expression must have a thenBranch", self) @@ -57,7 +59,7 @@ def __teal__(self, options: "CompileOptions"): thenStart, thenEnd = self.thenBranch.__teal__(options) end = TealSimpleBlock([]) - branchBlock = TealConditionalBlock([]) + branchBlock = TealConditionalBlock([], root_expr=(self._label_cond or self)) branchBlock.setTrueBlock(thenStart) condEnd.setNextBlock(branchBlock) @@ -65,10 +67,12 @@ def __teal__(self, options: "CompileOptions"): if self.elseBranch is None: branchBlock.setFalseBlock(end) + branchBlock._sframes_container = self else: elseStart, elseEnd = self.elseBranch.__teal__(options) branchBlock.setFalseBlock(elseStart) elseEnd.setNextBlock(end) + elseEnd._sframes_container = self return condStart, end @@ -120,6 +124,7 @@ def ElseIf(self, cond): if not self.elseBranch: self.elseBranch = If(cond) + self.elseBranch._label_cond = cond else: if not isinstance(self.elseBranch, If): raise TealInputError("Else-ElseIf block is malformed") diff --git a/pyteal/ast/maybe.py b/pyteal/ast/maybe.py index e22c79816..9d2182a49 100644 --- a/pyteal/ast/maybe.py +++ b/pyteal/ast/maybe.py @@ -52,6 +52,7 @@ def local_version_check(options: "CompileOptions"): compile_check=( local_version_check if compile_check is None else compile_check ), + root_expr=self, ) def hasValue(self) -> ScratchLoad: diff --git a/pyteal/ast/multi.py b/pyteal/ast/multi.py index 4fed823a6..7f8195822 100644 --- a/pyteal/ast/multi.py +++ b/pyteal/ast/multi.py @@ -1,4 +1,4 @@ -from typing import Callable, List, Union, TYPE_CHECKING +from typing import Callable, List, Union, TYPE_CHECKING, cast from pyteal.types import TealType from pyteal.ir import TealOp, Op, TealBlock @@ -6,6 +6,7 @@ from pyteal.ast.leafexpr import LeafExpr from pyteal.ast.scratch import ScratchSlot from pyteal.ast.seq import Seq +from pyteal.ast.scratch import ScratchStackStore if TYPE_CHECKING: from pyteal.compiler import CompileOptions @@ -22,6 +23,7 @@ def __init__( immediate_args: List[Union[int, str]] | None = None, args: List[Expr] | None = None, compile_check: Callable[["CompileOptions"], None] = lambda _: None, + root_expr: Expr | None = None, ): """Create a new MultiValue. @@ -39,6 +41,7 @@ def __init__( self.compile_check = compile_check self.output_slots = [ScratchSlot() for _ in self.types] + self._sframes_container = root_expr def outputReducer(self, reducer: Callable[..., Expr]) -> Expr: input = [slot.load(self.types[i]) for i, slot in enumerate(self.output_slots)] @@ -69,7 +72,8 @@ def __teal__(self, options: "CompileOptions"): # values. ie the output to stack [A, B, C] should correspond to C->output_slots[2] # B->output_slots[1], and A->output_slots[0]. for slot in reversed(self.output_slots): - store = slot.store() + store = cast(ScratchStackStore, slot.store()) + store._sframes_container = self._sframes_container storeStart, storeEnd = store.__teal__(options) curEnd.setNextBlock(storeStart) curEnd = storeEnd diff --git a/pyteal/ast/router.py b/pyteal/ast/router.py index 517f8fbf9..00de22752 100644 --- a/pyteal/ast/router.py +++ b/pyteal/ast/router.py @@ -1,40 +1,40 @@ from contextlib import contextmanager -from dataclasses import dataclass, field, fields, astuple +from dataclasses import astuple, dataclass, field from enum import IntFlag -from typing import cast, Optional, Callable +from typing import Callable, Final, Optional, cast 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 algosdk.v2client.algod import AlgodClient from pyteal.ast import abi -from pyteal.ast.subroutine import ( - OutputKwArgInfo, - Subroutine, - SubroutineCall, - SubroutineFnWrapper, - ABIReturnSubroutine, -) +from pyteal.ast.app import OnComplete from pyteal.ast.assert_ import Assert from pyteal.ast.cond import Cond from pyteal.ast.expr import Expr from pyteal.ast.frame import FrameVar, Proto, ProtoStackLayout -from pyteal.ast.app import OnComplete -from pyteal.ast.int import Int, EnumInt -from pyteal.ast.scratch import ScratchSlot -from pyteal.ast.seq import Seq +from pyteal.ast.int import EnumInt, Int 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 +from pyteal.ast.scratch import ScratchSlot +from pyteal.ast.seq import Seq +from pyteal.ast.subroutine import ( + ABIReturnSubroutine, + OutputKwArgInfo, + Subroutine, + SubroutineCall, + SubroutineFnWrapper, +) +from pyteal.ast.txn import Txn +from pyteal.compiler.compiler import DEFAULT_TEAL_VERSION, Compilation, OptimizeOptions +from pyteal.compiler.sourcemap import PyTealSourceMap, _PyTealSourceMapper +from pyteal.config import METHOD_ARG_NUM_CUTOFF +from pyteal.errors import AlgodClientError, TealInputError, TealInternalError +from pyteal.ir.ops import Mode +from pyteal.stack_frame import NatalStackFrame, sourcemapping_off_context +from pyteal.types import TealType +from pyteal.util import algod_with_assertion ActionType = Expr | SubroutineFnWrapper | ABIReturnSubroutine @@ -135,13 +135,13 @@ def approval_cond(self) -> Expr | int: MethodConfig.__module__ = "pyteal" -@dataclass(frozen=True) +@dataclass class OnCompleteAction: """ OnComplete Action, registers bare calls to one single OnCompletion case. """ - action: Optional[ActionType] = field(kw_only=True, default=None) + action: ActionType | None = field(kw_only=True, default=None) call_config: CallConfig = field(kw_only=True, default=CallConfig.NEVER) def __post_init__(self): @@ -149,6 +149,7 @@ def __post_init__(self): raise TealInputError( f"action {self.action} and call_config {self.call_config!r} contradicts" ) + self.stack_frames: NatalStackFrame = NatalStackFrame() @staticmethod def never() -> "OnCompleteAction": @@ -173,26 +174,27 @@ def is_empty(self) -> bool: 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 __post_init__(self): + def __init__( + self, + *, + close_out: OnCompleteAction = OnCompleteAction.never(), + clear_state: OnCompleteAction = OnCompleteAction.never(), + delete_application: OnCompleteAction = OnCompleteAction.never(), + no_op: OnCompleteAction = OnCompleteAction.never(), + opt_in: OnCompleteAction = OnCompleteAction.never(), + update_application: OnCompleteAction = OnCompleteAction.never(), + ): + self.close_out: Final[OnCompleteAction] = close_out + self.clear_state: Final[OnCompleteAction] = clear_state + self.delete_application: Final[OnCompleteAction] = delete_application + self.no_op: Final[OnCompleteAction] = no_op + self.opt_in: Final[OnCompleteAction] = opt_in + self.update_application: Final[OnCompleteAction] = update_application if not self.clear_state.is_empty(): raise TealInputError( "Attempt to construct clear state program from bare app call: " @@ -201,6 +203,8 @@ def __post_init__(self): "https://pyteal.readthedocs.io/en/latest/abi.html#registering-bare-app-calls" ) + self.stack_frames: NatalStackFrame = NatalStackFrame() + def asdict(self) -> dict[str, OnCompleteAction]: return { "clear_state": self.clear_state, @@ -211,12 +215,11 @@ def asdict(self) -> dict[str, OnCompleteAction]: "update_application": self.update_application, } + def aslist(self) -> list[OnCompleteAction]: + return list(self.asdict().values()) + 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 + return all([a.is_empty() for a in self.aslist()]) def approval_construction(self) -> Optional[Expr]: oc_action_pair: list[tuple[EnumInt, OnCompleteAction]] = [ @@ -228,6 +231,7 @@ def approval_construction(self) -> Optional[Expr]: ] 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(): @@ -327,12 +331,21 @@ def to_cond_node(self, use_frame_pt: bool = False) -> CondNode: res = ASTBuilder.wrap_handler(True, self.method, use_frame_pt=use_frame_pt) if isinstance(self.condition, Expr): res = Seq(Assert(self.condition), res) + NatalStackFrame.reframe_asts(self.condition.stack_frames, res) return CondNode(walk_in_cond, res) CondWithMethod.__module__ = "pyteal" +def _smap_friendly_approve(): + # TODO: Consider replacing _smap_friendly_approve() with a reframe_asts() + a = Approve() + a.stack_frames._compiler_gen = True + return a + + +@dataclass class ASTBuilder: def __init__(self): self.methods_with_conds: list[CondWithMethod] = [] @@ -346,7 +359,6 @@ def __filter_invalid_handlers_and_typecast( subroutine: ABIReturnSubroutine | SubroutineFnWrapper | Expr, ) -> ABIReturnSubroutine: """This method filters out invalid handlers that might be normal subroutine, Expr, or unroutable ABIReturnSubroutine. - It accepts only routable ABIReturnSubroutine, and shrink the type to ABIReturnSubroutine from argument's union type. """ if not isinstance(subroutine, ABIReturnSubroutine): @@ -503,15 +515,12 @@ def wrap_handler( use_frame_pt: bool = False, ) -> 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`. @@ -531,7 +540,11 @@ def wrap_handler( raise TealInputError( f"{wrap_to_name} handler should be TealType.none not {handler.type_of()}." ) - return handler if handler.has_return() else Seq(handler, Approve()) + return ( + handler + if handler.has_return() + else Seq(handler, _smap_friendly_approve()) + ) case SubroutineFnWrapper(): if handler.type_of() != TealType.none: raise TealInputError( @@ -542,7 +555,7 @@ def wrap_handler( f"subroutine call should take 0 arg for {wrap_to_name}. " f"this subroutine takes {handler.subroutine.argument_count()}." ) - return Seq(handler(), Approve()) + return Seq(handler(), _smap_friendly_approve()) case ABIReturnSubroutine(): if handler.type_of() != "void": raise TealInputError( @@ -553,7 +566,7 @@ def wrap_handler( f"abi-returning subroutine call should take 0 arg for {wrap_to_name}. " f"this abi-returning subroutine takes {handler.subroutine.argument_count()}." ) - return Seq(cast(Expr, handler()), Approve()) + return Seq(cast(Expr, handler()), _smap_friendly_approve()) case _: raise TealInputError( f"{wrap_to_name} can only accept: none type Expr, or Subroutine/ABIReturnSubroutine with none return and no arg" @@ -629,7 +642,7 @@ def __de_abify_subroutine_vanilla(handler: ActionType) -> Expr: return Seq( *decode_instructions, cast(SubroutineCall, handler(*arg_vals)), - Approve(), + _smap_friendly_approve(), ) else: output_temp: abi.BaseType = cast( @@ -642,7 +655,7 @@ def __de_abify_subroutine_vanilla(handler: ActionType) -> Expr: *decode_instructions, returned_val.store_into(output_temp), abi.MethodReturn(output_temp), - Approve(), + _smap_friendly_approve(), ) @staticmethod @@ -742,7 +755,7 @@ def __de_abify_subroutine_frame_pointers(handler: ActionType) -> Expr: def declaration(): return Seq(*decoding_steps, *returning_steps) - return Seq(subroutine_caster(declaration)(), Approve()) + return Seq(subroutine_caster(declaration)(), _smap_friendly_approve()) def add_method_to_ast( self, method_signature: str, cond: Expr | int, handler: ABIReturnSubroutine @@ -765,6 +778,94 @@ def program_construction(self, use_frame_pt: bool = False) -> Expr: ASTBuilder.__module__ = "pyteal" +@dataclass(frozen=True) +class RouterResults: + approval_teal: str + clear_teal: str + abi_contract: sdk_abi.Contract + approval_sourcemap: Optional[PyTealSourceMap] = None + clear_sourcemap: Optional[PyTealSourceMap] = None + + +@dataclass +class _RouterBundle: + """Private class that includes a full sourcemapper object""" + + approval_program: Expr + clear_program: Expr + abi_contract: sdk_abi.Contract + approval_teal: str + clear_teal: str + approval_sourcemapper: Optional[_PyTealSourceMapper] = None + clear_sourcemapper: Optional[_PyTealSourceMapper] = None + input: Optional["_RouterCompileInput"] = None + + def get_results(self) -> RouterResults: + approval_sm: PyTealSourceMap | None = None + clear_sm: PyTealSourceMap | None = None + if self.approval_sourcemapper: + approval_sm = self.approval_sourcemapper.get_sourcemap(self.approval_teal) + if self.clear_sourcemapper: + clear_sm = self.clear_sourcemapper.get_sourcemap(self.clear_teal) + + return RouterResults( + self.approval_teal, + self.clear_teal, + self.abi_contract, + approval_sm, + clear_sm, + ) + + +@dataclass +class _RouterCompileInput: + version: int + assemble_constants: bool + optimize: Optional[OptimizeOptions] = None + with_sourcemaps: bool = False + pcs_in_sourcemaps: bool = False + approval_filename: Optional[str] = None + clear_filename: Optional[str] = None + algod_client: Optional[AlgodClient] = None + annotate_teal: bool = False + annotate_teal_headers: bool = False + annotate_teal_concise: bool = True + + def __post_init__(self): + # The following params are non-sensical when truthy without sourcemaps. + # However, they are not defining anything actionable so are simple ignored + # rather than erroring when `with_source == False`: + # * pcs_in_sourcemap + # * approval_filename + # * clear_filename + # * algod_client + + # On the other hand, self.annotate_teal indicates a user request which cannot + # be provided on when there isn't a sourcemap + if self.annotate_teal and not self.with_sourcemaps: + raise ValueError( + "In order annotate generated teal source, must set with_sourcemap True" + ) + + if self.pcs_in_sourcemaps: + # bootstrap an algod_client if not provided, and in either case, run a healthcheck + try: + self.algod_client = algod_with_assertion(self.algod_client) + except AlgodClientError as ace: + raise ResourceWarning( + "algod_with_assertion has failed: are you sure there is an available node such as Sandbox?" + ) from ace + + def get_compilation(self, program: Expr) -> Compilation: + return Compilation( + ast=program, + mode=Mode.Application, + version=self.version, + assemble_constants=self.assemble_constants, + optimize=self.optimize, + ) + + class Router: """ The Router class helps construct the approval and clear state programs for an ARC-4 compliant @@ -925,7 +1026,6 @@ def method( # - None # - CallConfig.Never # both cases evaluate to False in if statement. - if clear_state is not None: raise TealInputError( "Attempt to register ABI method for clear state program: " @@ -937,27 +1037,24 @@ def method( def wrap(_func) -> ABIReturnSubroutine: wrapped_subroutine = ABIReturnSubroutine(_func, overriding_name=name) call_configs: MethodConfig - if ( - no_op is None - and opt_in is None - and close_out is None - and update_application is None - and delete_application is None - ): + + ocs = dict( + no_op=no_op, + opt_in=opt_in, + close_out=close_out, + clear_state=clear_state, + update_application=update_application, + delete_application=delete_application, + ) + if all(oc is None for oc in ocs.values()): call_configs = MethodConfig(no_op=CallConfig.CALL) else: - _no_op = no_op or CallConfig.NEVER - _opt_in = opt_in or CallConfig.NEVER - _close_out = close_out or CallConfig.NEVER - _update_app = update_application or CallConfig.NEVER - _delete_app = delete_application or CallConfig.NEVER + + def none_to_never(x: None | CallConfig): + return CallConfig.NEVER if x is None else x call_configs = MethodConfig( - no_op=_no_op, - opt_in=_opt_in, - close_out=_close_out, - update_application=_update_app, - delete_application=_delete_app, + **{k: none_to_never(v) for k, v in ocs.items()} ) return self.add_method_handler( wrapped_subroutine, name, call_configs, description @@ -1005,10 +1102,12 @@ def _build_program( if bare_call_approval: self.approval_ast.bare_calls = [ CondNode( - Txn.application_args.length() == Int(0), - cast(Expr, bare_call_approval), + cond := Txn.application_args.length() == Int(0), + act := cast(Expr, bare_call_approval), ) ] + NatalStackFrame.reframe_asts(bare_call_approval.stack_frames, cond) + act.stack_frames = bare_call_approval.stack_frames optimize = optimize or OptimizeOptions() use_frame_pt = optimize.use_frame_pointers(version) @@ -1035,11 +1134,13 @@ def compile_program( optimize: Optional[OptimizeOptions] = None, ) -> tuple[str, str, sdk_abi.Contract]: """ + DEPRECATED BUT KEPT FOR BACKWARDS COMPATIBILITY. PREFER Router.compile(). + 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 `Router._build_program` and :any:`compileTeal`. + This method combines `Router._build_program` and `Compilation.compile`. 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. @@ -1051,23 +1152,139 @@ def compile_program( * clear_state_program: compiled clear-state program string * contract: a Python SDK Contract object to allow clients to make off-chain calls """ + input = _RouterCompileInput( + version=version, + assemble_constants=assemble_constants, + optimize=optimize, + ) + cpb = self._build_impl(input) + + return cpb.approval_teal, cpb.clear_teal, cpb.abi_contract + + def compile( + self, + *, + version: int = DEFAULT_TEAL_VERSION, + assemble_constants: bool = False, + optimize: Optional[OptimizeOptions] = None, + approval_filename: Optional[str] = None, + clear_filename: Optional[str] = None, + with_sourcemaps: bool = False, + pcs_in_sourcemap: bool = False, + algod_client: Optional[AlgodClient] = None, + annotate_teal: bool = False, + annotate_teal_headers: bool = False, + annotate_teal_concise: bool = True, + ) -> RouterResults: + """ + 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 `Router._build_program` and `Compilation.compile`. + + 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 RouterResults containing the following: + * approval_teal (str): compiled approval program + * clear_teal (str): compiled clear-state program + * abi_contract (abi.Contract): a Python SDK Contract object to allow clients to make off-chain calls + * approval_sourcemap (PyTealSourceMap | None): source map results for approval program + * clear_sourcemap (PyTealSourceMap | None): source map results for clear-state program + """ + approval_filename = approval_filename or f"{self.name}_approval.teal" + clear_filename = clear_filename or f"{self.name}_clear.teal" + + input = _RouterCompileInput( + version=version, + assemble_constants=assemble_constants, + optimize=optimize, + with_sourcemaps=with_sourcemaps, + approval_filename=approval_filename, + clear_filename=clear_filename, + pcs_in_sourcemaps=pcs_in_sourcemap, + algod_client=algod_client, + annotate_teal=annotate_teal, + annotate_teal_headers=annotate_teal_headers, + annotate_teal_concise=annotate_teal_concise, + ) + return self._build_impl(input).get_results() + + def _build_impl(self, input: _RouterCompileInput) -> _RouterBundle: with self._cleaning_context(): - ap, csp, contract = self._build_program(version=version, optimize=optimize) - ap_compiled = compileTeal( - ap, - Mode.Application, - version=version, - assembleConstants=assemble_constants, - optimize=optimize, + ap, csp, contract = self._build_program( + version=input.version, optimize=input.optimize + ) + + abundle = input.get_compilation(ap)._compile_impl( + with_sourcemap=input.with_sourcemaps, + teal_filename=input.approval_filename, + pcs_in_sourcemap=input.pcs_in_sourcemaps, + algod_client=input.algod_client, + annotate_teal=input.annotate_teal, + annotate_teal_headers=input.annotate_teal_headers, + annotate_teal_concise=input.annotate_teal_concise, ) - csp_compiled = compileTeal( - csp, - Mode.Application, - version=version, - assembleConstants=assemble_constants, - optimize=optimize, + + # TODO: ideally, the clear-state compilation ought to be in it's own + # _cleaning_context to allow for fresh slot numbering. However, + # the side effects of separating is not yet obvious and + # clear state programs generally aren't so complex so this isn't + # of high urgency + csbundle = input.get_compilation(csp)._compile_impl( + with_sourcemap=input.with_sourcemaps, + teal_filename=input.clear_filename, + pcs_in_sourcemap=input.pcs_in_sourcemaps, + algod_client=input.algod_client, + annotate_teal=input.annotate_teal, + annotate_teal_headers=input.annotate_teal_headers, + annotate_teal_concise=input.annotate_teal_concise, ) - return ap_compiled, csp_compiled, contract + + if input.with_sourcemaps: + # rerun the build and compilation without the source mapper + # and verify that the teal programs are the same + with self._cleaning_context(), sourcemapping_off_context(): + assert NatalStackFrame.sourcemapping_is_off() + + ap_wo, csp_wo, _ = self._build_program( + version=input.version, optimize=input.optimize + ) + input_wo = _RouterCompileInput( + version=input.version, + assemble_constants=input.assemble_constants, + optimize=input.optimize, + with_sourcemaps=False, + ) + abundle_wo = input_wo.get_compilation(ap_wo)._compile_impl( + with_sourcemap=False + ) + csbundle_wo = input_wo.get_compilation(csp_wo)._compile_impl( + with_sourcemap=False + ) + _PyTealSourceMapper._validate_teal_identical( + abundle.teal, + abundle_wo.teal, + msg="FATAL ERROR. Approval Program without sourcemaps (LEFT) differs from Approval Program with (RIGHT)", + ) + _PyTealSourceMapper._validate_teal_identical( + csbundle.teal, + csbundle_wo.teal, + msg="FATAL ERROR. Clear Program without sourcemaps (LEFT) differs from Clear Program with (RIGHT)", + ) + + return _RouterBundle( + approval_program=ap, + clear_program=csp, + abi_contract=contract, + approval_teal=abundle.teal, + clear_teal=csbundle.teal, + approval_sourcemapper=abundle.sourcemapper, + clear_sourcemapper=csbundle.sourcemapper, + input=input, + ) Router.__module__ = "pyteal" diff --git a/pyteal/ast/router_test.py b/pyteal/ast/router_test.py index 2eb1fb937..9388259e6 100644 --- a/pyteal/ast/router_test.py +++ b/pyteal/ast/router_test.py @@ -377,7 +377,9 @@ def test_bare_call_config_clear_state_failure(): ) ) - assert "Attempt to construct clear state program from bare app call" in str(tie) + assert "Attempt to construct clear state program from bare app call" in str( + tie.value + ) def test_BareCallActions_asdict(): @@ -409,6 +411,29 @@ def test_BareCallActions_asdict(): } +def test_BareCallActions_aslist(): + no_action = pt.OnCompleteAction() + optin_action = pt.OnCompleteAction(action=pt.Int(1), call_config=pt.CallConfig.ALL) + update_action = pt.OnCompleteAction( + action=pt.Int(2), call_config=pt.CallConfig.CALL + ) + + bca = pt.BareCallActions( + update_application=update_action, + opt_in=optin_action, + ) + + bcal = bca.aslist() + assert bcal == [ + no_action, + no_action, + no_action, + no_action, + optin_action, + update_action, + ] + + def test_BareCallActions_get_method_config(): from pyteal.ast.router import MethodConfig, CallConfig diff --git a/pyteal/ast/scratch.py b/pyteal/ast/scratch.py index 6b2cf4702..c3a675c0c 100644 --- a/pyteal/ast/scratch.py +++ b/pyteal/ast/scratch.py @@ -249,6 +249,7 @@ def __init__(self, slot: ScratchSlot): """ super().__init__() self.slot = slot + self._sframes_container: Expr | None = None def __str__(self): return "(StackStore {})".format(self.slot) @@ -257,6 +258,7 @@ def __teal__(self, options: "CompileOptions"): from pyteal.ir import TealOp, Op, TealBlock op = TealOp(self, Op.store, self.slot) + op._sframes_container = self._sframes_container return TealBlock.FromOp(options, op) def type_of(self): diff --git a/pyteal/ast/subroutine.py b/pyteal/ast/subroutine.py index fa1cb6823..90f1aff1f 100644 --- a/pyteal/ast/subroutine.py +++ b/pyteal/ast/subroutine.py @@ -6,7 +6,7 @@ from docstring_parser import parse as parse_docstring from inspect import isclass, Parameter, signature, get_annotations from types import MappingProxyType, NoneType -from typing import Any, Callable, Final, Optional, TYPE_CHECKING, cast, ClassVar +from typing import Any, Callable, Final, TYPE_CHECKING, cast, ClassVar from pyteal.ast import abi from pyteal.ast.expr import Expr @@ -15,6 +15,7 @@ from pyteal.ast.frame import FrameBury, Proto, FrameVar, ProtoStackLayout from pyteal.errors import TealInputError, TealInternalError, verifyProgramVersion from pyteal.ir import TealOp, Op, TealBlock +from pyteal.stack_frame import NatalStackFrame from pyteal.types import TealType if TYPE_CHECKING: @@ -24,7 +25,7 @@ class _SubroutineDeclByOption: def __init__(self, subroutine_def: "SubroutineDefinition") -> None: self.subroutine: SubroutineDefinition = subroutine_def - self.option_map: dict[bool, Optional[SubroutineDeclaration]] = { + self.option_map: dict[bool, SubroutineDeclaration | None] = { True: None, False: None, } @@ -32,8 +33,8 @@ def __init__(self, subroutine_def: "SubroutineDefinition") -> None: True: SubroutineEval.fp_evaluator(), False: SubroutineEval.normal_evaluator(), } - self.has_return: Optional[bool] = None - self.type_of: Optional[TealType] = None + self.has_return: bool | None = None + self.type_of: TealType | None = None def get_declaration(self) -> "SubroutineDeclaration": warnings.warn( @@ -101,7 +102,7 @@ def __init__( self, implementation: Callable[..., Expr], return_type: TealType, - name_str: Optional[str] = None, + name_str: str | None = None, has_abi_output: bool = False, ) -> None: """ @@ -117,7 +118,7 @@ def __init__( SubroutineDefinition.nextSubroutineId += 1 self.return_type = return_type - self.declaration: Optional["SubroutineDeclaration"] = None + self.declaration: "SubroutineDeclaration | None" = None self.declarations: _SubroutineDeclByOption = _SubroutineDeclByOption(self) self.implementation: Callable = implementation @@ -141,6 +142,8 @@ def __init__( self.__name: str = name_str if name_str else self.implementation.__name__ + self.stack_frames: NatalStackFrame = NatalStackFrame() + def _validate( self, input_types: list[TealType | None] | None = None ) -> tuple[ @@ -396,12 +399,13 @@ def __init__( self, subroutine: SubroutineDefinition, body: Expr, - deferred_expr: Optional[Expr] = None, + deferred_expr: Expr | None = None, ) -> None: super().__init__() self.subroutine = subroutine self.body = body self.deferred_expr = deferred_expr + self._sframes_container: SubroutineDefinition = subroutine def __teal__(self, options: "CompileOptions"): return self.body.__teal__(options) @@ -425,7 +429,7 @@ class OutputKwArgInfo: abi_type: abi.TypeSpec @staticmethod - def from_dict(kwarg_info: dict[str, abi.TypeSpec]) -> Optional["OutputKwArgInfo"]: + def from_dict(kwarg_info: dict[str, abi.TypeSpec]) -> "OutputKwArgInfo | None": match list(kwarg_info.keys()): case []: return None @@ -496,17 +500,22 @@ def __teal__(self, options: "CompileOptions"): ) def handle_arg(arg: Expr | ScratchVar | abi.BaseType) -> Expr: + ret_expr: Expr if isinstance(arg, ScratchVar): - return arg.index() + ret_expr = arg.index() + ret_expr.stack_frames = self.subroutine.stack_frames elif isinstance(arg, Expr): - return arg + ret_expr = arg elif isinstance(arg, abi.BaseType): - return arg._stored_value.load() + ret_expr = arg._stored_value.load() + ret_expr.stack_frames = self.subroutine.stack_frames else: raise TealInputError( f"cannot handle current arg: {arg} to put it on stack" ) + return ret_expr + op = TealOp(self, Op.callsub, self.subroutine) return TealBlock.FromOp(options, op, *[handle_arg(x) for x in self.args]) @@ -536,7 +545,7 @@ def __init__( self, fn_implementation: Callable[..., Expr], return_type: TealType, - name: Optional[str] = None, + name: str | None = None, ) -> None: self.subroutine = SubroutineDefinition( fn_implementation, @@ -610,9 +619,9 @@ def __init__( fn_implementation: Callable[..., Expr], /, *, - overriding_name: Optional[str] = None, + overriding_name: str | None = None, ) -> None: - self.output_kwarg_info: Optional[OutputKwArgInfo] = self._get_output_kwarg_info( + self.output_kwarg_info: OutputKwArgInfo | None = self._get_output_kwarg_info( fn_implementation ) self.subroutine = SubroutineDefinition( @@ -632,7 +641,7 @@ def wrapper(fn_impl: Callable[..., Expr]) -> ABIReturnSubroutine: @classmethod def _get_output_kwarg_info( cls, fn_implementation: Callable[..., Expr] - ) -> Optional[OutputKwArgInfo]: + ) -> OutputKwArgInfo | None: if not callable(fn_implementation): raise TealInputError("Input to ABIReturnSubroutine is not callable") sig = signature(fn_implementation) @@ -809,7 +818,7 @@ def mySubroutine(a: Expr, b: Expr) -> Expr: ]) """ - def __init__(self, return_type: TealType, name: Optional[str] = None) -> None: + def __init__(self, return_type: TealType, name: str | None = None) -> None: """Define a new subroutine with the given return type. Args: @@ -907,11 +916,11 @@ class SubroutineEval: """ var_n_loaded_method: Callable[ - [SubroutineDefinition, str, Optional[Proto]], - tuple[Optional[ScratchVar], ScratchVar | abi.BaseType | Expr], + [SubroutineDefinition, str, Proto | None], + tuple[ScratchVar | None, ScratchVar | abi.BaseType | Expr], ] use_frame_pt: bool = False - _current_proto: ClassVar[Optional[Proto]] = None + _current_proto: ClassVar[Proto | None] = None @staticmethod def _new_abi_instance_from_storage( @@ -934,7 +943,7 @@ def _new_abi_instance_from_storage( def var_n_loaded_scratch( subroutine: SubroutineDefinition, param: str, - _: Optional[Proto] = None, + _: Proto | None = None, ) -> tuple[ScratchVar, ScratchVar | abi.BaseType | Expr]: loaded_var: ScratchVar | abi.BaseType | Expr argument_var: ScratchVar @@ -957,15 +966,15 @@ def var_n_loaded_scratch( def var_n_loaded_fp( subroutine: SubroutineDefinition, param: str, - proto: Optional[Proto], - ) -> tuple[Optional[ScratchVar], ScratchVar | abi.BaseType | Expr]: + proto: Proto | None, + ) -> tuple[ScratchVar | None, ScratchVar | abi.BaseType | Expr]: if not proto: raise TealInternalError( "proto should be available for frame pointer based subroutine." ) loaded_var: ScratchVar | abi.BaseType | Expr - argument_var: Optional[ScratchVar] + argument_var: ScratchVar | None if param in subroutine.by_ref_args: argument_var = DynamicScratchVar(TealType.anytype) @@ -1021,7 +1030,9 @@ def __proto(subroutine: SubroutineDefinition) -> Proto: else int(subroutine.return_type != TealType.none) ) - return Proto(subroutine.argument_count(), num_stack_outputs, mem_layout=layout) + proto = Proto(subroutine.argument_count(), num_stack_outputs, mem_layout=layout) + NatalStackFrame.mark_asts_as_compiler_gen(proto) + return proto def evaluate(self, subroutine: SubroutineDefinition) -> SubroutineDeclaration: proto = self.__proto(subroutine) @@ -1039,7 +1050,7 @@ def evaluate(self, subroutine: SubroutineDefinition) -> SubroutineDeclaration: abi_output_kwargs: dict[str, abi.BaseType] = {} output_kwarg_info = OutputKwArgInfo.from_dict(subroutine.output_kwarg) - output_carrying_abi: Optional[abi.BaseType] = None + output_carrying_abi: abi.BaseType | None = None if output_kwarg_info: if not self.use_frame_pt: @@ -1065,7 +1076,7 @@ def evaluate(self, subroutine: SubroutineDefinition) -> SubroutineDeclaration: f"Subroutine function does not return a PyTeal expression. Got type {type(subroutine_body)}." ) - deferred_expr: Optional[Expr] = None + deferred_expr: Expr | None = None # if there is an output keyword argument for ABI # place the storing on the stack with deferred expr only when compile to scratch var diff --git a/pyteal/ast/txn.py b/pyteal/ast/txn.py index d8de1bd84..99cc97d24 100644 --- a/pyteal/ast/txn.py +++ b/pyteal/ast/txn.py @@ -21,13 +21,13 @@ class TxnType: """Enum of all possible transaction types.""" - Unknown = EnumInt("unknown") - Payment = EnumInt("pay") - KeyRegistration = EnumInt("keyreg") - AssetConfig = EnumInt("acfg") - AssetTransfer = EnumInt("axfer") - AssetFreeze = EnumInt("afrz") - ApplicationCall = EnumInt("appl") + Unknown = EnumInt("unknown") # T2PT7 + Payment = EnumInt("pay") # T2PT7 + KeyRegistration = EnumInt("keyreg") # T2PT7 + AssetConfig = EnumInt("acfg") # T2PT7 + AssetTransfer = EnumInt("axfer") # T2PT7 + AssetFreeze = EnumInt("afrz") # T2PT7 + ApplicationCall = EnumInt("appl") # T2PT7 TxnType.__module__ = "pyteal" diff --git a/pyteal/ast/while_.py b/pyteal/ast/while_.py index 176830aad..0d8dee108 100644 --- a/pyteal/ast/while_.py +++ b/pyteal/ast/while_.py @@ -48,8 +48,9 @@ def __teal__(self, options: "CompileOptions"): end = TealSimpleBlock([]) doEnd.setNextBlock(condStart) + doEnd._sframes_container = self - branchBlock = TealConditionalBlock([]) + branchBlock = TealConditionalBlock([], root_expr=self) branchBlock.setTrueBlock(doStart) branchBlock.setFalseBlock(end) @@ -86,6 +87,7 @@ def Do(self, doBlock: Expr, *do_block_multi: Expr): doBlock = _use_seq_if_multiple(doBlock, *do_block_multi) require_type(doBlock, TealType.none) + self.doBlock = doBlock return self diff --git a/pyteal/compiler/__init__.py b/pyteal/compiler/__init__.py index e0d29450d..00646cf77 100644 --- a/pyteal/compiler/__init__.py +++ b/pyteal/compiler/__init__.py @@ -6,10 +6,11 @@ MIN_PROGRAM_VERSION, DEFAULT_PROGRAM_VERSION, CompileOptions, + Compilation, compileTeal, ) - from pyteal.compiler.optimizer import OptimizeOptions +from pyteal.compiler.sourcemap import PyTealSourceMap __all__ = [ "MAX_TEAL_VERSION", @@ -19,6 +20,8 @@ "MIN_PROGRAM_VERSION", "DEFAULT_PROGRAM_VERSION", "CompileOptions", + "Compilation", "compileTeal", "OptimizeOptions", + "PyTealSourceMap", ] diff --git a/pyteal/compiler/compiler.py b/pyteal/compiler/compiler.py index 8186822cd..efca355cf 100644 --- a/pyteal/compiler/compiler.py +++ b/pyteal/compiler/compiler.py @@ -1,22 +1,38 @@ -from typing import Final, List, Tuple, Set, Dict, Optional, cast +from dataclasses import dataclass +from typing import Dict, Final, List, Optional, Set, Tuple, cast -from pyteal.compiler.optimizer import OptimizeOptions, apply_global_optimizations +from algosdk.v2client.algod import AlgodClient -from pyteal.ast import Expr, Return, Seq, SubroutineDefinition, SubroutineDeclaration -from pyteal.compiler.sort import sortBlocks +from pyteal.ast import Expr, Return, Seq, SubroutineDeclaration, SubroutineDefinition +from pyteal.compiler.constants import createConstantBlocks from pyteal.compiler.flatten import flattenBlocks, flattenSubroutines +from pyteal.compiler.optimizer import OptimizeOptions, apply_global_optimizations from pyteal.compiler.scratchslots import ( assignScratchSlotsToSubroutines, collect_unoptimized_slots, ) +from pyteal.compiler.sort import sortBlocks +from pyteal.compiler.sourcemap import ( + _PyTealSourceMapper, + PyTealSourceMap, +) from pyteal.compiler.subroutines import ( - spillLocalSlotsDuringRecursion, resolveSubroutines, + spillLocalSlotsDuringRecursion, ) -from pyteal.compiler.constants import createConstantBlocks -from pyteal.errors import TealInputError, TealInternalError -from pyteal.ir import Mode, Op, TealComponent, TealOp, TealBlock, TealSimpleBlock +from pyteal.errors import SourceMapDisabledError, TealInputError, TealInternalError +from pyteal.ir import ( + Mode, + Op, + TealBlock, + TealComponent, + TealOp, + TealPragma, + TealSimpleBlock, +) +from pyteal.stack_frame import NatalStackFrame, sourcemapping_off_context from pyteal.types import TealType +from pyteal.util import algod_with_assertion MAX_PROGRAM_VERSION = 8 @@ -133,15 +149,16 @@ def compileSubroutine( else None ) + ret_expr: Optional[Expr] = None if not ast.has_return(): if ast.type_of() == TealType.none: - ret_expr = Return() + ret_expr = Return() # T2PT2 ret_expr.trace = ast.trace seq_expr = Seq([ast, ret_expr]) seq_expr.trace = ret_expr.trace ast = seq_expr else: - ret_expr = Return(ast) + ret_expr = Return(ast) # T2PT3 ret_expr.trace = ast.trace ast = ret_expr @@ -150,49 +167,41 @@ def compileSubroutine( start, end = ast.__teal__(options) start.addIncoming() start.validateTree() + if currentSubroutine: + decl = currentSubroutine.get_declaration_by_option(options.use_frame_pointers) + if end.ops: + end.ops[0]._sframes_container = decl + + if deferred_expr := decl.deferred_expr: + # this represents code that should be inserted before each retsub op + 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" + ) - if ( - currentSubroutine - and currentSubroutine.get_declaration_by_option( - options.use_frame_pointers - ).deferred_expr - ): - # this represents code that should be inserted before each retsub op - deferred_expr = cast( - Expr, - currentSubroutine.get_declaration_by_option( - options.use_frame_pointers - ).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" - ) - - # 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() + # 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() - # insert deferred blocks between the previous block(s) and this one - deferred_start.incoming = block.incoming - block.incoming = [deferred_end] - deferred_end.nextBlock = block + # 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) + 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 + if block is start: + # this is the start block, replace start + start = deferred_start start.validateTree() @@ -214,7 +223,7 @@ def compileSubroutine( newSubroutines = referencedSubroutines - subroutine_start_blocks.keys() for subroutine in sorted(newSubroutines, key=lambda subroutine: subroutine.id): compileSubroutine( - subroutine.get_declaration_by_option(options.use_frame_pointers), + subroutine.get_declaration_by_option(options.use_frame_pointers), # T2PT4 options, subroutineGraph, subroutine_start_blocks, @@ -236,13 +245,292 @@ def sort_subroutine_blocks( return subroutine_mapping +@dataclass(frozen=True) +class CompileResults: + """Summary of compilation""" + + teal: str + sourcemap: PyTealSourceMap | None = None + + +@dataclass +class _FullCompilationBundle: + """ + Private class that groups together various artifacts required and produced by the compiler. + + The following artifacts should _NOT_ be returned to the user, as they could + interfere with the compiler's idempotency. For example, keeping these + artifacts around could make it difficult to guarantee that scratchslots + are allocated as efficiently as possible, and assuming that such artifacts + might continue existing after compilation interferes with the goal of making + the compiler as reliable and efficient as possible: + * teal_chunks + * components + + NOTE: `annotated_teal` can grow quite large and become unsuitable for compilation as algod's compile + endpoint may throw a "request body too large" error. + Therefore, it is recommended that `teal` be used for algod compilation purposes. + """ + + ast: Expr + mode: Mode + version: int + assemble_constants: bool + optimize: Optional[OptimizeOptions] + teal: str + teal_chunks: list[str] + components: list[TealComponent] + sourcemapper: _PyTealSourceMapper | None = None + annotated_teal: str | None = None + + def get_results(self) -> CompileResults: + sourcemap: PyTealSourceMap | None = None + if self.sourcemapper: + sourcemap = self.sourcemapper.get_sourcemap(self.teal) + + return CompileResults(self.teal, sourcemap) + + +class Compilation: + """ + A class that encapsulates the data needed to compile a PyTeal expression + """ + + def __init__( + self, + ast: Expr, + mode: Mode, + *, + version: int = DEFAULT_PROGRAM_VERSION, + assemble_constants: bool = False, + optimize: OptimizeOptions | None = None, + ): + """ + Instantiate a Compilation object providing the necessary data to compile a PyTeal expression. + + Args: + ast: The PyTeal expression to assemble + mode: The program's mode for execution. Either `Mode.Signature` or `Mode.Application` + version (optional): The program version used to assemble the program. This will determine which + expressions and fields are able to be used in the program and how expressions compile to + TEAL opcodes. Defaults to 2 if not included. + assembleConstants (optional): When `True`, the compiler will produce a program with fully + assembled constants, rather than using the pseudo-ops `int`, `byte`, and `addr`. These + constants will be assembled in the most space-efficient way, so enabling this may reduce + the compiled program's size. Enabling this option requires a minimum program version of 3. + Defaults to `False`. + optimize (optional): `OptimizeOptions` that determine which optimizations will be applied. + """ + self.ast = ast + self.mode = mode + self.version = version + self.assemble_constants = assemble_constants + self.optimize: OptimizeOptions = optimize or OptimizeOptions() + + def compile( + self, + *, + with_sourcemap: bool = False, + teal_filename: str | None = None, + pcs_in_sourcemap: bool = False, + algod_client: AlgodClient | None = None, + annotate_teal: bool = False, + annotate_teal_headers: bool = False, + annotate_teal_concise: bool = False, + ) -> CompileResults: + """Execute the compilation, producing a TEAL assembly along with other optional artifacts. + + Args: + with_sourcemap (optional): When `True`, the compiler will produce a sourcemap that maps the + generated TEAL assembly back to the original PyTeal source code. Defaults to `False`. + teal_filename (optional): The filename to use in the sourcemap. Defaults to `None`. + pcs_in_sourcemap (optional): When `True`, the compiler will include the program counter in + relevant sourcemap artifacts. This requires an `AlgodClient` (see next param). Defaults to `False`. + algod_client (optional): An `AlgodClient` to use to fetch the program counter. Defaults to `None`. + When `pcs_in_sourcemap` is `True` and `algod_client` is not provided, the compiler will + assume that an Algorand Sandbox algod client is running on the default port (4001) and -if + this is not the case- will raise an exception. + annotate_teal (optional): When `True`, the compiler will produce a TEAL assembly with comments + that describe the PyTeal source code that generated each line of the assembly. Defaults to `False`. + annotate_teal_headers (optional): When `True` along with `annotate_teal` being `True`, a header + line with column names will be added at the top of the annotated teal. Defaults to `False`. + annotate_teal_concise (optional): When `True` along with `annotate_teal` being `True`, the compiler + will provide fewer columns in the annotated teal. Defaults to `False`. + + Returns: + A `CompileResults` object with the following data: + * teal: the TEAL assembly + * sourcemap (optional): if `with_sourcemap` is `True`, the following source map data is provided: + * teal_filename (optional): the TEAL filename, if this was provided + * r3_sourcemap: an `R3SourceMap` object that maps the generated TEAL assembly back to the original PyTeal source code and conforms to the specs of the `Source Map Revision 3 Proposal `_ + * pc_sourcemap (optional): if `pcs_in_sourcemap` is `True`, a `PCSourceMap` object that maps the program counters assembled by the `AlgodClient` which was utilized in the compilation back to the TEAL assembly which was generated by the compiler. This conforms to the specs of the `Source Map Revision 3 Proposal `_ + * annotated_teal (optional): if `annotate_teal` is `True`, the TEAL assembly with comments that describe the PyTeal source code that generated each line of the assembly + + Raises: + TealInputError: if an operation in ast is not supported by the supplied mode and version. + TealInternalError: if an internal error is encountered during compilation. + """ + return self._compile_impl( + with_sourcemap=with_sourcemap, + teal_filename=teal_filename, + pcs_in_sourcemap=pcs_in_sourcemap, + algod_client=algod_client, + annotate_teal=annotate_teal, + annotate_teal_headers=annotate_teal_headers, + annotate_teal_concise=annotate_teal_concise, + ).get_results() + + def _compile_impl( + self, + with_sourcemap: bool = True, + teal_filename: str | None = None, + pcs_in_sourcemap: bool = False, + algod_client: AlgodClient | None = None, + annotate_teal: bool = False, + annotate_teal_headers: bool = False, + annotate_teal_concise: bool = True, + ) -> _FullCompilationBundle: + if ( + not (MIN_PROGRAM_VERSION <= self.version <= MAX_PROGRAM_VERSION) + or type(self.version) is not int + ): + raise TealInputError( + "Unsupported program version: {}. Excepted an integer in the range [{}, {}]".format( + self.version, MIN_PROGRAM_VERSION, MAX_PROGRAM_VERSION + ) + ) + + if with_sourcemap and NatalStackFrame.sourcemapping_is_off(): + raise SourceMapDisabledError() + + if annotate_teal and not with_sourcemap: + raise ValueError( + "In order annotate generated teal source, must set with_sourcemap True" + ) + + if pcs_in_sourcemap: + # bootstrap an algod_client if not provided, and in either case, run a healthcheck + algod_client = algod_with_assertion( + algod_client, msg="Adding PC's to sourcemap requires live Algod" + ) + + options = CompileOptions( + mode=self.mode, version=self.version, optimize=self.optimize + ) + + subroutineGraph: Dict[SubroutineDefinition, Set[SubroutineDefinition]] = dict() + subroutine_start_blocks: Dict[ + Optional[SubroutineDefinition], TealBlock + ] = dict() + subroutine_end_blocks: Dict[Optional[SubroutineDefinition], TealBlock] = dict() + compileSubroutine( + self.ast, + options, + subroutineGraph, + subroutine_start_blocks, + subroutine_end_blocks, + ) + + # note: optimizations are off by default, in which case, apply_global_optimizations + # won't make any changes. Because the optimizer is invoked on a subroutine's + # control flow graph, the optimizer requires context across block boundaries. This + # is necessary for the dependency checking of local slots. Global slots, slots + # used by DynamicScratchVar, and reserved slots are not optimized. + if options.optimize.optimize_scratch_slots(self.version): + options.optimize._skip_slots = collect_unoptimized_slots( + subroutine_start_blocks + ) + for start in subroutine_start_blocks.values(): + apply_global_optimizations(start, options.optimize, self.version) + + localSlotAssignments: Dict[ + Optional[SubroutineDefinition], Set[int] + ] = assignScratchSlotsToSubroutines(subroutine_start_blocks) + + subroutineMapping: Dict[ + Optional[SubroutineDefinition], List[TealComponent] + ] = sort_subroutine_blocks(subroutine_start_blocks, subroutine_end_blocks) + + spillLocalSlotsDuringRecursion( + self.version, subroutineMapping, subroutineGraph, localSlotAssignments + ) + + subroutineLabels = resolveSubroutines(subroutineMapping) + components: list[TealComponent] = flattenSubroutines( + subroutineMapping, subroutineLabels, options + ) + + verifyOpsForVersion(components, options.version) + verifyOpsForMode(components, options.mode) + + if self.assemble_constants: + if self.version < 3: + raise TealInternalError( + f"The minimum program version required to enable assembleConstants is 3. The current version is {self.version}." + ) + components = createConstantBlocks(components) + + components = [TealPragma(self.version)] + components # T2PT0 + teal_chunks = [tl.assemble() for tl in components] + teal_code = "\n".join(teal_chunks) + + full_cpb = _FullCompilationBundle( + ast=self.ast, + mode=self.mode, + version=self.version, + assemble_constants=self.assemble_constants, + optimize=self.optimize, + teal=teal_code, + teal_chunks=teal_chunks, + components=components, + ) + if not with_sourcemap: + return full_cpb + + # Below is purely for the source mapper: + + source_mapper = _PyTealSourceMapper( + teal_chunks=teal_chunks, + components=components, + build=True, + teal_filename=teal_filename, + include_pcs=pcs_in_sourcemap, + algod=algod_client, + annotate_teal=annotate_teal, + annotate_teal_headers=annotate_teal_headers, + annotate_teal_concise=annotate_teal_concise, + ) + full_cpb.sourcemapper = source_mapper + + # run a second time without, and assert that the same teal is produced + with sourcemapping_off_context(): + assert NatalStackFrame.sourcemapping_is_off() + + # implicitly recursive call!! + teal_code_wo = compileTeal( + self.ast, + self.mode, + version=self.version, + assembleConstants=self.assemble_constants, + optimize=self.optimize, + ) + + _PyTealSourceMapper._validate_teal_identical( + teal_code_wo, + teal_code, + msg="FATAL ERROR. Program without sourcemaps (LEFT) differs from Program with (RIGHT)", + ) + + return full_cpb + + def compileTeal( ast: Expr, mode: Mode, *, version: int = DEFAULT_PROGRAM_VERSION, assembleConstants: bool = False, - optimize: Optional[OptimizeOptions] = None, + optimize: OptimizeOptions | None = None, ) -> str: """Compile a PyTeal expression into TEAL assembly. @@ -264,62 +552,13 @@ def compileTeal( Raises: TealInputError: if an operation in ast is not supported by the supplied mode and version. - TealInternalError: if an internal error is encounter during compilation. + TealInternalError: if an internal error is encountered during compilation. """ - if ( - not (MIN_PROGRAM_VERSION <= version <= MAX_PROGRAM_VERSION) - or type(version) is not int - ): - raise TealInputError( - "Unsupported program version: {}. Excepted an integer in the range [{}, {}]".format( - version, MIN_PROGRAM_VERSION, MAX_PROGRAM_VERSION - ) - ) - - options = CompileOptions(mode=mode, version=version, optimize=optimize) - - subroutineGraph: Dict[SubroutineDefinition, Set[SubroutineDefinition]] = dict() - subroutine_start_blocks: Dict[Optional[SubroutineDefinition], TealBlock] = dict() - subroutine_end_blocks: Dict[Optional[SubroutineDefinition], TealBlock] = dict() - compileSubroutine( - ast, options, subroutineGraph, subroutine_start_blocks, subroutine_end_blocks - ) - - # note: optimizations are off by default, in which case, apply_global_optimizations - # won't make any changes. Because the optimizer is invoked on a subroutine's - # control flow graph, the optimizer requires context across block boundaries. This - # is necessary for the dependency checking of local slots. Global slots, slots - # used by DynamicScratchVar, and reserved slots are not optimized. - if options.optimize.optimize_scratch_slots(version): - options.optimize._skip_slots = collect_unoptimized_slots( - subroutine_start_blocks - ) - for start in subroutine_start_blocks.values(): - apply_global_optimizations(start, options.optimize, version) - - localSlotAssignments = assignScratchSlotsToSubroutines(subroutine_start_blocks) - - subroutineMapping: Dict[ - Optional[SubroutineDefinition], List[TealComponent] - ] = sort_subroutine_blocks(subroutine_start_blocks, subroutine_end_blocks) - - spillLocalSlotsDuringRecursion( - version, subroutineMapping, subroutineGraph, localSlotAssignments - ) - - subroutineLabels = resolveSubroutines(subroutineMapping) - teal = flattenSubroutines(subroutineMapping, subroutineLabels) - - verifyOpsForVersion(teal, options.version) - verifyOpsForMode(teal, options.mode) - - if assembleConstants: - if version < 3: - raise TealInternalError( - f"The minimum program version required to enable assembleConstants is 3. The current version is {version}." - ) - teal = createConstantBlocks(teal) - - lines = ["#pragma version {}".format(version)] - lines += [i.assemble() for i in teal] - return "\n".join(lines) + bundle = Compilation( + ast, + mode, + version=version, + assemble_constants=assembleConstants, + optimize=optimize, + )._compile_impl(with_sourcemap=False) + return bundle.teal diff --git a/pyteal/compiler/flatten.py b/pyteal/compiler/flatten.py index 99674836c..8a0dfac48 100644 --- a/pyteal/compiler/flatten.py +++ b/pyteal/compiler/flatten.py @@ -1,7 +1,8 @@ -from typing import List, Dict, DefaultDict, Optional from collections import defaultdict -from pyteal.ast import SubroutineDefinition +from pyteal.ast import Expr, SubroutineDeclaration, SubroutineDefinition +from pyteal import compiler +from pyteal.errors import TealInternalError, TealInputError from pyteal.ir import ( Op, TealOp, @@ -12,19 +13,23 @@ TealConditionalBlock, LabelReference, ) -from pyteal.errors import TealInternalError -def flattenBlocks(blocks: List[TealBlock]) -> List[TealComponent]: +def flattenBlocks(blocks: list[TealBlock]) -> list[TealComponent]: """Lowers a list of TealBlocks into a list of TealComponents. Args: blocks: The blocks to lower. """ - codeblocks = [] - references: DefaultDict[int, int] = defaultdict(int) + codeblocks: list[list[TealOp]] = [] + references: defaultdict[int, int] = defaultdict(int) + referer: dict[int, int] = {} - labelRefs: Dict[int, LabelReference] = dict() + def add_if_new(nextIndex, i): + if nextIndex not in referer: + referer[nextIndex] = i + + labelRefs: dict[int, LabelReference] = dict() def indexToLabel(index: int) -> LabelReference: if index not in labelRefs: @@ -37,12 +42,15 @@ def blockIndexByReference(block: TealBlock) -> int: return i raise ValueError("Block not present in list: {}".format(block)) + root_expr: Expr | None = None for i, block in enumerate(blocks): code = list(block.ops) codeblocks.append(code) if block.isTerminal(): continue + root_expr = block._sframes_container + if type(block) is TealSimpleBlock: assert block.nextBlock is not None @@ -50,7 +58,8 @@ def blockIndexByReference(block: TealBlock) -> int: if nextIndex != i + 1: references[nextIndex] += 1 - code.append(TealOp(None, Op.b, indexToLabel(nextIndex))) + add_if_new(nextIndex, i) + code.append(TealOp(root_expr, Op.b, indexToLabel(nextIndex))) # T2PT5 elif type(block) is TealConditionalBlock: assert block.trueBlock is not None @@ -61,35 +70,46 @@ def blockIndexByReference(block: TealBlock) -> int: if falseIndex == i + 1: references[trueIndex] += 1 - code.append(TealOp(None, Op.bnz, indexToLabel(trueIndex))) + add_if_new(trueIndex, i) + code.append(TealOp(root_expr, Op.bnz, indexToLabel(trueIndex))) # T2PT5 continue if trueIndex == i + 1: references[falseIndex] += 1 - code.append(TealOp(None, Op.bz, indexToLabel(falseIndex))) + add_if_new(falseIndex, i) + code.append(TealOp(root_expr, Op.bz, indexToLabel(falseIndex))) # T2PT5 continue references[trueIndex] += 1 - code.append(TealOp(None, Op.bnz, indexToLabel(trueIndex))) + add_if_new(trueIndex, i) + code.append(TealOp(root_expr, Op.bnz, indexToLabel(trueIndex))) # T2PT5 references[falseIndex] += 1 - code.append(TealOp(None, Op.b, indexToLabel(falseIndex))) + add_if_new(falseIndex, i) + code.append(TealOp(root_expr, Op.b, indexToLabel(falseIndex))) # T2PT5 else: raise TealInternalError("Unrecognized block type: {}".format(type(block))) - teal: List[TealComponent] = [] + teal: list[TealComponent] = [] + root_expr = None for i, code in enumerate(codeblocks): if references[i] != 0: - teal.append(TealLabel(None, indexToLabel(i))) + root_expr = ( + blocks[i]._sframes_container + or blocks[referer[i]]._sframes_container + or root_expr + ) + teal.append(TealLabel(root_expr, indexToLabel(i))) # T2PT6 teal += code return teal def flattenSubroutines( - subroutineMapping: Dict[Optional[SubroutineDefinition], List[TealComponent]], - subroutineToLabel: Dict[SubroutineDefinition, str], -) -> List[TealComponent]: + subroutineMapping: dict[SubroutineDefinition | None, list[TealComponent]], + subroutineToLabel: dict[SubroutineDefinition, str], + options: "compiler.CompileOptions", +) -> list[TealComponent]: """Combines each subroutine's list of TealComponents into a single list of TealComponents that represents the entire program. @@ -101,7 +121,7 @@ def flattenSubroutines( Returns: A single list of TealComponents representing the entire program. """ - combinedOps: List[TealComponent] = [] + combinedOps: list[TealComponent] = [] # By default all branch labels in each subroutine will start from "l0". To # make each subroutine have unique labels, we prefix "main_" to the ones @@ -123,7 +143,14 @@ def flattenSubroutines( if isinstance(stmt, TealLabel): stmt.getLabelRef().addPrefix(labelPrefix) - combinedOps.append(TealLabel(None, LabelReference(label), comment)) + # this is needed for source map generation + dexpr: SubroutineDeclaration | None + try: + dexpr = subroutine.get_declaration_by_option(options.use_frame_pointers) + except TealInputError: + dexpr = None + + combinedOps.append(TealLabel(dexpr, LabelReference(label), comment)) # T2PT1 combinedOps += subroutineOps return combinedOps diff --git a/pyteal/compiler/flatten_test.py b/pyteal/compiler/flatten_test.py index a01c45d64..80c63ea9d 100644 --- a/pyteal/compiler/flatten_test.py +++ b/pyteal/compiler/flatten_test.py @@ -295,11 +295,21 @@ def test_flattenSubroutines_no_subroutines(): pt.TealOp(None, pt.Op.return_), ] - actual = flattenSubroutines(subroutineMapping, subroutineToLabel) + opts = pt.CompileOptions() + actual = flattenSubroutines(subroutineMapping, subroutineToLabel, opts) assert actual == expected +def without_expr(comp): + if isinstance(comp, pt.TealOp): + return pt.TealOp(None, comp.op, *comp.args) + if isinstance(comp, pt.TealLabel): + return pt.TealLabel(None, comp.label, comp.comment) + + assert False, "should never get here" + + def test_flattenSubroutines_1_subroutine(): subroutine = pt.SubroutineDefinition( lambda: pt.Int(1) + pt.Int(2) + pt.Int(3), pt.TealType.uint64 @@ -354,9 +364,10 @@ def test_flattenSubroutines_1_subroutine(): pt.TealOp(None, pt.Op.retsub), ] - actual = flattenSubroutines(subroutineMapping, subroutineToLabel) + opts = pt.CompileOptions() + actual = flattenSubroutines(subroutineMapping, subroutineToLabel, opts) - assert actual == expected + assert list(map(without_expr, actual)) == expected def test_flattenSubroutines_multiple_subroutines(): @@ -539,6 +550,7 @@ def sub3Impl(a1, a2, a3): pt.TealOp(None, pt.Op.b, expectedSubroutine3L1Label), ] - actual = flattenSubroutines(subroutineMapping, subroutineToLabel) + opts = pt.CompileOptions() + actual = flattenSubroutines(subroutineMapping, subroutineToLabel, opts) assert actual == expected diff --git a/pyteal/compiler/sourcemap.py b/pyteal/compiler/sourcemap.py new file mode 100644 index 000000000..48c60354f --- /dev/null +++ b/pyteal/compiler/sourcemap.py @@ -0,0 +1,1318 @@ +import bisect +from collections import defaultdict +from dataclasses import dataclass, field +from difflib import unified_diff +from functools import partial +from itertools import count +import re +from typing import Any, Final, Literal, Mapping, OrderedDict, TypedDict, cast + +from tabulate import tabulate # type: ignore + +from algosdk.source_map import SourceMap as PCSourceMap # disambiguate +from algosdk.v2client.algod import AlgodClient + +import pyteal as pt +from pyteal.errors import TealInternalError +from pyteal.stack_frame import ( + PT_GENERATED, + NatalStackFrame, + PyTealFrame, + PyTealFrameStatus, +) +from pyteal.util import algod_with_assertion + +# ### ---- R3SourceMap is based on mjpieters code snippets ---- ### # +# +# Modified from the original `SourceMap` available under MIT License here (as of Nov. 12, 2022): https://gist.github.com/mjpieters/86b0d152bb51d5f5979346d11005588b +# `R3` is a nod to "Revision 3" of John Lenz's Source Map Proposal: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1 +# +# ### + +_b64chars = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/" +_b64table = [-1] * (max(_b64chars) + 1) +for i, b in enumerate(_b64chars): + _b64table[b] = i + +shiftsize, flag, mask = 5, 1 << 5, (1 << 5) - 1 + + +def _base64vlq_decode(vlqval: str) -> list[int]: + """Decode Base64 VLQ value""" + results = [] + shift = value = 0 + # use byte values and a table to go from base64 characters to integers + for v in map(_b64table.__getitem__, vlqval.encode("ascii")): + v = cast(int, v) + value += (v & mask) << shift + if v & flag: + shift += shiftsize + continue + # determine sign and add to results + results.append((value >> 1) * (-1 if value & 1 else 1)) + shift = value = 0 + return results + + +def _base64vlq_encode(*values: int) -> str: + """Encode integers to a VLQ value""" + results: list[int] = [] + add = results.append + for v in values: + # add sign bit + v = (abs(v) << 1) | int(v < 0) + while True: + toencode, v = v & mask, v >> shiftsize + add(toencode | (v and flag)) + if not v: + break + # TODO: latest version of gist avoids the decode() step + return bytes(map(_b64chars.__getitem__, results)).decode() + + +class autoindex(defaultdict): + def __init__(self, *args, **kwargs): + super().__init__(partial(next, count()), *args, **kwargs) + + +@dataclass(frozen=False) +class R3SourceMapping: + line: int + # line_end: Optional[int] = None #### NOT PROVIDED (AND NOT CONFORMING WITH R3 SPEC) AS TARGETS ARE ASSUMED TO SPAN AT MOST ONE LINE #### + column: int + source: str | None = None + source_line: int | None = None + source_column: int | None = None + source_content: list[str] | None = None + source_extract: str | None = None + target_extract: str | None = None + name: str | None = None + source_line_end: int | None = None + source_column_end: int | None = None + column_end: int | None = None + + def __post_init__(self): + if self.source is not None and ( + self.source_line is None or self.source_column is None + ): + raise TypeError( + "Invalid source mapping; missing line and column for source file" + ) + if self.name is not None and self.source is None: + raise TypeError( + "Invalid source mapping; name entry without source location info" + ) + + def __lt__(self, other: "R3SourceMapping") -> bool: + assert isinstance(other, type(self)), f"received incomparable {type(other)}" + + return (self.line, self.column) < (other.line, other.column) + + def __ge__(self, other: "R3SourceMapping") -> bool: + return not self < other + + def location(self, source=False) -> tuple[str, int, int]: + return ( + ( + self.source if self.source else "", + self.source_line if self.source_line else -1, + self.source_column if self.source_column else -1, + ) + if source + else ("", self.line, self.column) + ) + + @classmethod + def extract_window( + cls, + source_lines: list[str] | None, + line: int, + column: int, + right_column: int | None, + ) -> str | None: + return ( + ( + source_lines[line][column:right_column] + if right_column is not None + else source_lines[line][column:] + ) + if source_lines + else None + ) + + def __str__(self) -> str: + def swindow(file, line, col, rcol, extract): + if file == "unknown": + file = None + if not rcol: + rcol = "" + if extract is None: + extract = "?" + return f"{file + '::' if file else ''}L{line}C{col}-{rcol}='{extract}'" + + return ( + f"{swindow(self.source, self.source_line, self.source_column, self.source_column_end, self.source_extract)} <- " + f"{swindow(None, self.line, self.column, self.column_end, self.target_extract)}" + ) + + __repr__ = __str__ + + +class R3SourceMapJSON(TypedDict, total=False): + version: Literal[3] + file: str | None + sourceRoot: str | None + sources: list[str] + sourcesContent: list[str | None] | None + names: list[str] + mappings: str + + +@dataclass(frozen=True) +class R3SourceMap: + """ + This class is renames mjpieters' SourceMap + (https://gist.github.com/mjpieters/86b0d152bb51d5f5979346d11005588b#file-sourcemap-py-L62) + and tweaks it a bit, adding the following functionality: + * adds fields `file_liens`, `source_files`, `entries` + * __post_init__ (new) - runs a sanity check validation on the ordering of provided entries + * __repr__ - printing out "R3SourceMap(...)" instead of "MJPSourceMap(...)" + * from_json - accepting new params `sources_override`, `sources_content_override`, `target`, `add_right_bounds` + * add_right_bounds (new) - allow specifying the right column bounds + * to_json - accepting new param `with_contents` + + The main methods for this class are `from_json` and `to_json` which + follow the encoding conventions outlined in the Source Map Revison 3 Proposal + (https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?hl=en_US&pli=1&pli=1) + """ + + filename: str | None + source_root: str | None + entries: Mapping[tuple[int, int], "R3SourceMapping"] + index: list[tuple[int, ...]] = field(default_factory=list) + file_lines: list[str] | None = None + source_files: list[str] | None = None + source_files_lines: list[list[str] | None] | None = None + + def __post_init__(self): + entries = list(self.entries.values()) + for i, entry in enumerate(entries): + if i + 1 >= len(entries): + return + + if entry >= entries[i + 1]: + raise TypeError( + f"Invalid source map as entries aren't properly ordered: entries[{i}] = {entry} >= entries[{i+1}] = {entries[i + 1]}" + ) + + def __repr__(self) -> str: + parts = [] + if self.filename is not None: + parts += [f"file={self.filename!r}"] + if self.source_root is not None: + parts += [f"source_root={self.source_root!r}"] + parts += [f"len={len(self.entries)}"] + return f"" + + @classmethod + def from_json( + cls, + smap: R3SourceMapJSON, + sources_override: list[str] | None = None, + sources_content_override: list[str] = [], + target: str | None = None, + add_right_bounds: bool = True, + ) -> "R3SourceMap": + """ + NOTE about `*_if_missing` arguments + * sources_override - STRICTLY SPEAKING `sources` OUGHT NOT BE MISSING OR EMPTY in R3SourceMapJSON. + However, currently the POST v2/teal/compile endpoint populate this field with an empty list, as it is not provided the name of the + Teal file which is being compiled. In order comply with the R3 spec, this field is populated with ["unknown"] when either missing or empty + in the JSON and not supplied during construction. + An error will be raised when attempting to replace a nonempty R3SourceMapJSON.sources. + * sources_content_override - `sourcesContent` is optional and this provides a way at runtime to supply the actual source. + When provided, and the R3SourceMapJSON is either missing or empty, this will be substituted. + An error will be raised when attempting to replace a nonempty R3SourceMapJSON.sourcesContent. + """ + if smap.get("version") != 3: + raise ValueError("Only version 3 sourcemaps are supported ") + entries: dict[tuple[int, int], R3SourceMapping] = {} + index: list[list[int]] = [] + spos = npos = sline = scol = 0 + + sources = smap.get("sources") + if sources and sources_override: + raise AssertionError("ambiguous sources from JSON and method argument") + sources = sources or sources_override or ["unknown"] + + contents: list[str | None] | list[str] | None = smap.get("sourcesContent") + if contents and sources_content_override: + raise AssertionError( + "ambiguous sourcesContent from JSON and method argument" + ) + contents = contents or sources_content_override + + names = smap.get("names") + + tcont, sp_conts = ( + target.splitlines() if target else None, + [c.splitlines() if c else None for c in contents], + ) + + if "mappings" in smap: + for gline, vlqs in enumerate(smap["mappings"].split(";")): + index += [[]] + if not vlqs: + continue + gcol = 0 + for gcd, *ref in map(_base64vlq_decode, vlqs.split(",")): + gcol += gcd + kwargs = {} + if len(ref) >= 3: + sd, sld, scd, *namedelta = ref + spos, sline, scol = spos + sd, sline + sld, scol + scd + scont = sp_conts[spos] if len(sp_conts) > spos else None # type: ignore + # extract the referenced source till the end of the current line + extract = R3SourceMapping.extract_window + kwargs = { + "source": sources[spos] if spos < len(sources) else None, + "source_line": sline, + "source_column": scol, + "source_content": scont, + "source_extract": extract(scont, sline, scol, None), + "target_extract": extract(tcont, gline, gcol, None), + } + if namedelta and names: + npos += namedelta[0] + kwargs["name"] = names[npos] + entries[gline, gcol] = R3SourceMapping( + line=gline, column=gcol, **kwargs # type: ignore + ) + index[gline].append(gcol) + + sourcemap = cls( + smap.get("file"), + smap.get("sourceRoot"), + entries, + [tuple(cs) for cs in index], + tcont, + sources, + sp_conts, + ) + if add_right_bounds: + sourcemap.add_right_bounds() + return sourcemap + + def add_right_bounds(self) -> None: + entries = list(self.entries.values()) + for i, entry in enumerate(entries): + if i + 1 >= len(entries): + continue + + next_entry = entries[i + 1] + + def same_line_less_than(lc, nlc): + return (lc[0], lc[1]) == (nlc[0], nlc[1]) and lc[2] < nlc[2] + + if not same_line_less_than(entry.location(), next_entry.location()): + continue + entry.column_end = next_entry.column + entry.target_extract = entry.extract_window( + self.file_lines, entry.line, entry.column, entry.column_end + ) + + if not all( + [ + self.source_files, + self.source_files_lines, + next_entry.source, + same_line_less_than( + entry.location(source=True), + next_entry.location(source=True), + ), + ] + ): + continue + entry.source_column_end = next_entry.source_column + try: + fidx = self.source_files.index(next_entry.source) # type: ignore + except ValueError: + continue + if ( + self.source_files_lines + and isinstance(entry.source_line, int) + and isinstance(entry.source_column, int) + ): + entry.source_extract = entry.extract_window( + self.source_files_lines[fidx], + entry.source_line, + entry.source_column, + next_entry.source_column, + ) + + def to_json(self, with_contents: bool = False) -> R3SourceMapJSON: + content: list[str | None] = [] + mappings: list[str] = [] + sources, names = autoindex(), autoindex() + entries = self.entries + spos = sline = scol = npos = 0 + for gline, cols in enumerate(self.index): + gcol = 0 + mapping = [] + for col in cols: + entry = entries[gline, col] + ds, gcol = [col - gcol], col + + if entry.source is not None: + assert entry.source_line is not None + assert entry.source_column is not None + ds += ( + sources[entry.source] - spos, + entry.source_line - sline, + entry.source_column - scol, + ) + spos, sline, scol = ( + spos + ds[1], + sline + ds[2], + scol + ds[3], + ) + if spos == len(content): + c = entry.source_content + content.append("\n".join(c) if c else None) + if entry.name is not None: + ds += (names[entry.name] - npos,) + npos += ds[-1] + mapping.append(_base64vlq_encode(*ds)) + + mappings.append(",".join(mapping)) + + encoded = { + "version": 3, + "sources": [s for s, _ in sorted(sources.items(), key=lambda si: si[1])], + "names": [n for n, _ in sorted(names.items(), key=lambda ni: ni[1])], + "mappings": ";".join(mappings), + } + if with_contents: + encoded["sourcesContent"] = content + if self.filename is not None: + encoded["file"] = self.filename + if self.source_root is not None: + encoded["sourceRoot"] = self.source_root + return encoded # type: ignore + + def __getitem__(self, idx: int | tuple[int, int]): + l: int + c: int + try: + l, c = idx # type: ignore # The exception handler deals with the int case + except TypeError: + l, c = idx, 0 # type: ignore # yes, idx is guaranteed to be an int + try: + return self.entries[l, c] + except KeyError: + # find the closest column + if not (cols := self.index[l]): + raise IndexError(idx) + cidx = bisect.bisect(cols, c) + return self.entries[l, cols[cidx and cidx - 1]] + + +# #### PyTeal Specific Classes below #### # + +_TEAL_LINE_NUMBER = "TL" +_TEAL_COLUMN = "TC" +_TEAL_COLUMN_END = "TCE" +_TEAL_LINE = "Teal Line" +_PROGRAM_COUNTERS = "PC" +_PYTEAL_HYBRID_UNPARSED = "PyTeal Hybrid Unparsed" +_PYTEAL_NODE_AST_UNPARSED = "PyTeal AST Unparsed" +_PYTEAL_NODE_AST_QUALNAME = "PyTeal Qualname" +_PYTEAL_COMPONENT = "PyTeal Qualname" +_PYTEAL_NODE_AST_SOURCE_BOUNDARIES = "PT Window" +_PYTEAL_FILENAME = "PT path" +_PYTEAL_LINE_NUMBER = "PTL" +_PYTEAL_LINE_NUMBER_END = "PTLE" +_PYTEAL_COLUMN = "PTC" +_PYTEAL_COLUMN_END = "PTCE" +_PYTEAL_LINE = "PyTeal" +_PYTEAL_NODE_AST = "PT AST" +_PYTEAL_NODE_AST_NONE = "FAILED" +_STATUS_CODE = "Sourcemap Status Code" +_STATUS = "Sourcemap Status" + + +class TealMapItem(PyTealFrame): + """ + TealMapItem extends PyTealFrame to add information about the teal code that + corresponds to the purported pyteal source. + It also encapsulates any program counter (PC) information, when given. + + The main consumers of TealMapItem are: + * _PyTealSourceMapper._build_r3sourcemap() which calls `source_mapping()` + and provides the result for constructing an R3SourceMap + * _PyTealSourceMapper.tabulate() which calls `asdict()` and is useful for + creating PyTeal (and PC) annotated *.teal files + """ + + def __init__( + self, + pt_frame: PyTealFrame, + teal_lineno: int, + teal_line: str, + teal_component: "pt.TealComponent", + pcs: list[int] | None = None, + ): + super().__init__( + frame_info=pt_frame.frame_info, + node=pt_frame.node, + creator=pt_frame.creator, + full_stack=pt_frame.full_stack, + rel_paths=pt_frame.rel_paths, + parent=pt_frame.parent, + ) + self.teal_lineno: Final[int] = teal_lineno + self.teal_line: Final[str] = teal_line + self.teal_component: Final[pt.TealComponent] = teal_component + self.pcs: Final[list[int] | None] = pcs if pcs else None + + def pcs_repr(self, prefix: str = "") -> str: + if not self.pcs: + return "" + return f"{prefix}({self.pcs[0]})" + + def __repr__(self) -> str: + P = " // " + return f"TealLine({self.teal_lineno}: {self.teal_line}{self.pcs_repr(prefix=P)} // PyTeal: {self._hybrid_w_offset()[0]}" + + def teal_column(self) -> int: + """Always returns 0 as the 0-index STARTING column offset""" + return 0 + + def teal_column_end(self) -> int: + """The 0-index ENDING column offset""" + return len(self.teal_line) + + _dict_lazy_attrs = { + _TEAL_LINE_NUMBER: lambda tmi: tmi.teal_lineno, + _TEAL_LINE: lambda tmi: tmi.teal_line, + _TEAL_COLUMN: lambda tmi: tmi.teal_column(), + _TEAL_COLUMN_END: lambda tmi: tmi.teal_column_end(), + _PROGRAM_COUNTERS: lambda tmi: tmi.pcs_repr(), + _PYTEAL_HYBRID_UNPARSED: lambda tmi: tmi.hybrid_unparsed(), + _PYTEAL_NODE_AST_UNPARSED: lambda tmi: tmi.node_source(), + _PYTEAL_NODE_AST_QUALNAME: lambda tmi: tmi.code_qualname(), + _PYTEAL_COMPONENT: lambda tmi: tmi.teal_component, + _PYTEAL_NODE_AST_SOURCE_BOUNDARIES: lambda tmi: tmi.node_source_window(), + _PYTEAL_FILENAME: lambda tmi: tmi.file(), + _PYTEAL_LINE_NUMBER: lambda tmi: tmi.lineno(), + _PYTEAL_LINE_NUMBER_END: lambda tmi: tmi.node_end_lineno(), + _PYTEAL_COLUMN: lambda tmi: tmi.column(), + _PYTEAL_COLUMN_END: lambda tmi: tmi.node_end_col_offset(), + _PYTEAL_LINE: lambda tmi: tmi.raw_code(), + _PYTEAL_NODE_AST: lambda tmi: tmi.node, + _PYTEAL_NODE_AST_NONE: lambda tmi: tmi.failed_ast(), + _STATUS_CODE: lambda tmi: tmi.status_code(), + _STATUS: lambda tmi: tmi.status(), + } + + def asdict(self, **kwargs) -> OrderedDict[str, Any]: + """kwargs serve as a rename mapping when present""" + assert ( + kwargs.keys() <= (attrs := self._dict_lazy_attrs).keys() + ), f"unrecognized parameters {kwargs.keys() - attrs.keys()}" + + return OrderedDict(((kwargs[k], attrs[k](self)) for k in kwargs)) + + def validate_for_export(self) -> None: + """ + Ensure providing necessary and unambiguous data before exporting. + """ + if self.teal_lineno is None: + raise ValueError("unable to export without valid target TEAL line number") + if self.lineno() is None: + raise ValueError("unable to export without valid target PyTEAL line number") + + def source_mapping(self) -> "R3SourceMapping": + self.validate_for_export() + return R3SourceMapping( + line=cast(int, self.teal_lineno) - 1, + column=self.teal_column(), + column_end=self.teal_column_end(), + source=self.file(), + source_line=cast(int, self.lineno()) - 1, + source_column=self.column(), + source_line_end=nel - 1 if (nel := self.node_end_lineno()) else None, + source_column_end=self.node_end_col_offset(), + source_extract=self.hybrid_unparsed(), + target_extract=self.teal_line, + ) + + +@dataclass(frozen=True) +class PyTealSourceMap: + """ + Encapsulate Expr-less source mapping data. + NOTE: type `PCSourceMap` is an alias for algosdk.source_map.SourceMap + """ + + teal_filename: str | None + r3_sourcemap: R3SourceMap | None + pc_sourcemap: PCSourceMap | None + annotated_teal: str | None + + +class _PyTealSourceMapper: + """ + _PyTealSourceMapper is the workhorse class that runs the sourcemapping algorithm. + + User-safe Expr-less artifacts are provided by methods: + * get_sourcemap() + * as_r3sourcemap() + * annotated_teal() + + Source Mapping Algorithm: + + INPUT: (teal_chunks, components) - these are present in the output object of + pyteal.compiler.compiler.Copmilation._compile_impl() + + OUTPUT: (self._cached_tmis, _cached_r3sourcemap, self._cached_pc_sourcemap) + + PASS I. Deduce the Best Frame Candidate (BFC) from each individual `NatalStackFrame` + + NOTE: this logic actually occurs at creation of the PyTeal Expr which is responsible + for each teal component. The `build()` method simply retrieves the result of this + pre-computation through NatalStackFrame.best() + + for each component in components: + # Deduce the "best" frame for the component's PyTeal source (@ Expr creation) + + # [1-7] Inside NatalStackFrame.__init__() + 1. call inspect.stack() to generate a full stack trace + 2. filter out "py crud" frames whose filename starts with "<" and don't have a code_context + 3. start searching at frame index i = 2 (right before Expr's NatalStackFrame was constructed) + 4. fast forward through PyTeal frames until the first non PyTeal frame is found + 5. in the case this was in import statement, back up until a known compiler generated line is discovered + 6. keep the last frame in the list + 7. convert this into a list[StackFrame] of size 1 + + # [8] Inside _PyTealSourceMapper.build() @ source-map creation: + 8. self._best_frames[i] = the singleton StackFrame in 7 converted to PyTealFrame # i == component's index of the component + + PASS II. Attempt to fill any "gaps" by inferring from adjacent BFC's + This logic is contained in _PyTealSourceMapper.infer(): + + NOTE: the mutations of self._best_framed described below are "sticky" in the sense + that when this `pyteal_frame` is modified and referenced by `prev_pyteal_frame` + in the next interation, it is the new _modified_ version that is used. + + for each pyteal_frame in self._best_frames: + status = pyteal_frame.status() + if status NOT in PyTealFrameStatus.{MISSING, MISSING_AST, MISSING_CODE, PYTEAL_GENERATED} + NO-OP # we can't do any better + + else: # call local method _PyTealSourceMapper.infer.infer_source() + prev_pyteal_frame, next_pyteal_frame <--- get these from self._best_frames (or None if doesn't exist) + if both exist: + if both are equal (have the equal `frame_info, node, rel_paths`): + replace this pyteal_frame by next_pyteal_frame + + if pyteal_frame is flagged as compiler generated: + if its reason is one of PT_GENERATED.{TYPE_ENUM_ONCOMPLETE, TYPE_ENUM_TXN, BRANCH_LABEL, BRANCH}: + # these occur when one of the # T2PT* comments is encountered + replace this pyteal_frame by next_pyteal_frame + if its reason is PT_GENERATED.FLAGGED_BY_DEV: + # this occurs when NatalStackFrame._compiler_gen is set True + replace this pyteal_frame by prev_pyteal_frame + + if ONLY prev_frame exists: + replace this pyteal_frame by prev_pyteal_frame + + # NOTE: Coverage shows we never get past here. + # I.e. the first frame never results in one of the PyTealFrameStatus'es listed above + if ONLY next_frame exists: + replace this pyteal_frame by next_pyteal_frame + + if NEITHER next_frame NOR pyteal_frame exist: + # this is basically impossible + NO-OP # we can't do any better + + """ + + UNEXPECTED_ERROR_SUFFIX = """This is an unexpected error. Please report this to the PyTeal team or create an issue at: + https://github.com/algorand/pyteal/issues""" + + def __init__( + self, + teal_chunks: list[str], + components: list["pt.TealComponent"], + *, + teal_filename: str | None = None, + include_pcs: bool = False, + algod: AlgodClient | None = None, + build: bool = True, + verbose: bool = False, + annotate_teal: bool = False, + annotate_teal_headers: bool = False, + annotate_teal_concise: bool = True, + ): + """ + Args: + teal_chunks: strings -each possibly multi-line- which represent a chunk + of TEAL code generated by the PyTeal compiler + components: TealComponent object in 1-to-1 correspondence with + `teal_chunks` and which generated the chunk in the PyTeal compiler + teal_filename (optional): filename of TEAL source to be used in source mapping. + This file isn't actually saved + include_pcs (optional): specifies whether program counters + should be included in the map + algod (optional): when `include_pcs == True`, an algod client is required + and calls the compile endpoint in order to retrieve the PC's. + In the case `include_pcs == True` but `algod` isn't provided, an algod + client will be bootstrapped via pyteal.util.algod_with_assertion + build (default=True): when True, building the sourcemap occurs at initialization + verbose (default=False): when True, more debugging information will be logged + annotate_teal (default=False): when True, a TEAL file will be provided with + annotated comments that give PyTeal (and optionally PC) source information + annotate_teal_headers (default=False): when True, an extra line will be added + to the top of the annotated TEAL file to indicate each annotation column + annotate_teal_concise (default=True): when False, additional columns will be added + to the annotated TEAL file + """ + if include_pcs: + # bootstrap an algod_client if not provided, and in either case, run a healthcheck + algod = algod_with_assertion( + algod, msg="Adding PC's to sourcemap requires live Algod" + ) + + if not teal_chunks: + raise TealInternalError("Please provide non-empty teal_chunks") + + if not components: + raise TealInternalError("Please provide non-empty components") + + self.teal_chunks: Final[list[str]] = teal_chunks + self.components: Final[list["pt.TealComponent"]] = components + + self.algod: AlgodClient | None = algod + + self.include_pcs: bool = include_pcs + + self.teal_filename: str | None = teal_filename + self.verbose: bool = verbose + + self._best_frames: list[PyTealFrame] = [] + self._cached_r3sourcemap: R3SourceMap | None = None + + self._cached_tmis: list[TealMapItem] = [] + self._cached_pc_sourcemap: PCSourceMap | None = None + + self._most_recent_omit_headers: bool | None = None + + # FOR DEBUGGING PURPOSES ONLY: + self._inferred_frames_at: list[int] = [] + + if annotate_teal or build: + self.build() + + self._annotated_teal: str | None = None + if annotate_teal: + self._annotated_teal = self.annotated_teal( + omit_headers=(oh := not annotate_teal_headers), + concise=annotate_teal_concise, + ) + self._most_recent_omit_headers = oh + + def get_sourcemap(self, teal_for_validation: str) -> PyTealSourceMap: + if not self._built(): + raise self._unexpected_error("source map not built yet") + + if self._annotated_teal: + if (oh := self._most_recent_omit_headers) is None: + raise self._unexpected_error( + "_most_recent_omit_headers is None unexpectedly after calculating annotated_teal" + ) + + self._validate_annotated( + oh, teal_for_validation.splitlines(), self._annotated_teal.splitlines() + ) + + return PyTealSourceMap( + self.teal_filename, + self._cached_r3sourcemap, + self._cached_pc_sourcemap, + self._annotated_teal, + ) + + def compiled_teal(self) -> str: + return "\n".join(self.teal_chunks) + + def _built(self) -> bool: + """ + If any portion of source map is missing, re-build it from scratch + """ + return all( + [ + not self.include_pcs or self._cached_pc_sourcemap, + self._cached_r3sourcemap, + self._cached_tmis, + ] + ) + + @classmethod + def _unexpected_error(cls, msg: str) -> TealInternalError: + return TealInternalError( + f"""{msg} + {cls.UNEXPECTED_ERROR_SUFFIX}""" + ) + + def build(self) -> None: + if self._built(): + return + + if self.include_pcs: + self._build_pc_sourcemap() + + # Validation + # sanity check teal_chunks, but discard them in the rest of the computation + # assert teal_chunks <--1-to-1--> components + + if (n := len(self.teal_chunks)) != len(self.components): + raise self._unexpected_error( + f"expected same number of teal chunks ({n}) and components ({len(self.components)})" + ) + + if n == 0: + raise self._unexpected_error( + "cannot generate empty source map: no components" + ) + + # PASS I. Deduce the Best Frame Candidate (BFC) from each individual `NatalStackFrame` + # See NatalStackFrame.__init__() for steps 1-7 which happen when an Expr is created + # 8. the list comprehension below converts each element + # FROM: the component's NatalStackFrame._frames : list[StackFrame] + # TO: a PyTealFrame + # overall resulting in a list[PyTealFrame] + self._best_frames = [ + tc.stack_frames().best().as_pyteal_frame() for tc in self.components + ] + + if not self._best_frames: + raise self._unexpected_error( + f"This shouldn't have happened as we already checked! Check again: {len(self.components)=}" + ) + + # PASS II. Attempt to fill any "gaps" by inferring from adjacent BFC's + self._best_frames, inferred = self._infer(self._best_frames) + if inferred: + self._inferred_frames_at = inferred + + lineno = 1 + for i, best_frame in enumerate(self._best_frames): + teal_chunk = self.teal_chunks[i] + for line in teal_chunk.splitlines(): + pcsm = cast(PCSourceMap, self._cached_pc_sourcemap) + pcs = None + if self.include_pcs: + pcs = pcsm.line_to_pc.get(lineno - 1, []) + self._cached_tmis.append( + TealMapItem( + pt_frame=best_frame, + teal_lineno=lineno, + teal_line=line, # type: ignore + teal_component=self.components[i], + pcs=pcs, + ) + ) + lineno += 1 + + self._build_r3sourcemap() + + if not NatalStackFrame._debug: + # cf. https://stackoverflow.com/questions/850795/different-ways-of-clearing-lists#answer-44349418 + self._best_frames *= 0 + self._inferred_frames_at *= 0 + + self._validate_build() + + def _validate_build(self): + dechunked = [line for chunk in self.teal_chunks for line in chunk.splitlines()] + + if (ld := len(dechunked)) != (ltmi := len(self._cached_tmis)): + raise self._unexpected_error( + f"teal chunks has {ld} teal lines which doesn't match the number of cached TealMapItem's ({ltmi})" + ) + + if (lr3 := len(r3_target_lines := self._cached_r3sourcemap.file_lines)) != ltmi: + raise self._unexpected_error( + f"there are {ltmi} TealMapItem's which doesn't match the number of file_lines in the cached R3SourceMap ({lr3})" + ) + + for i, line in enumerate(dechunked): + if line != (tmi_line := self._cached_tmis[i].teal_line): + raise self._unexpected_error( + f"teal chunk lines don't match TealMapItem's at index {i}. ('{line}' v. '{tmi_line}')" + ) + if tmi_line != (target_line := r3_target_lines[i]): + raise self._unexpected_error( + f"TealMapItem's don't match R3SourceMap.file_lines at index {i}. ('{tmi_line}' v. '{target_line}')" + ) + + def _build_r3sourcemap(self): + assert self._cached_tmis, "Unexpected error: no cached TealMapItems found" + + root = self._cached_tmis[0].root() + assert all( + root == tmi.root() for tmi in self._cached_tmis + ), "inconsistent sourceRoot - aborting" + + r3sms = [tmi.source_mapping() for tmi in self._cached_tmis] + entries = {(r3sm.line, r3sm.column): r3sm for r3sm in r3sms} + lines = [cast(str, r3sm.target_extract) for r3sm in r3sms] + + index_l: list[list[int]] = [[]] + + prev_line = 0 + for line, col in entries.keys(): + for _ in range(prev_line, line): + index_l.append([]) + curr = index_l[-1] + curr.append(col) + prev_line = line + + index: list[tuple[int, ...]] = [tuple(cs) for cs in index_l] + sources = [] + for tmi in self._cached_tmis: + if (f := tmi.file()) not in sources: + sources.append(f) + + self._cached_r3sourcemap = R3SourceMap( + filename=self.teal_filename, + source_root=root, + entries=entries, + index=index, + file_lines=lines, + source_files=sorted(sources), + ) + + def _build_pc_sourcemap(self): + """ + Prereq: self.teal_chunks - a Final member + """ + algod = algod_with_assertion( + self.algod, msg="Adding PC's to sourcemap requires live Algod" + ) + algod_compilation = algod.compile(self.compiled_teal(), source_map=True) + raw_sourcemap = algod_compilation.get("sourcemap") + if not raw_sourcemap: + raise TealInternalError( + f"algod compilation did not return 'sourcemap' as expected. {algod_compilation=}" + ) + self._cached_pc_sourcemap = PCSourceMap(raw_sourcemap) + + def as_list(self) -> list[TealMapItem]: + self.build() + return self._cached_tmis + + def as_r3sourcemap(self) -> R3SourceMap | None: + self.build() + return self._cached_r3sourcemap + + @classmethod + def _infer( + cls, best_frames: list[PyTealFrame] + ) -> tuple[list[PyTealFrame], list[int]]: + inferred = [] + frames = list(best_frames) + N = len(frames) + + def infer_source(i: int) -> PyTealFrame | None: + frame = frames[i] + if not frame: + return None + + prev_frame = None if i <= 0 else frames[i - 1] + next_frame = None if N <= i + 1 else frames[i + 1] + if prev_frame and next_frame: + if prev_frame == next_frame: + return prev_frame.clone(PyTealFrameStatus.PATCHED_BY_PREV_AND_NEXT) + + # PT Generated TypeEnum's presumably happened because of setting an transaction + # field in the next step: + reason = frame.compiler_generated_reason() + if reason in [ + PT_GENERATED.TYPE_ENUM_ONCOMPLETE, + PT_GENERATED.TYPE_ENUM_TXN, + PT_GENERATED.BRANCH_LABEL, + PT_GENERATED.BRANCH, + ]: + return next_frame.clone( + PyTealFrameStatus.PATCHED_BY_NEXT_OVERRIDE_PREV + ) + + if reason == PT_GENERATED.FLAGGED_BY_DEV: + return prev_frame.clone( + PyTealFrameStatus.PATCHED_BY_PREV_OVERRIDE_NEXT + ) + + # NO-OP otherwise: + return None + + if prev_frame and frame: + return prev_frame.clone(PyTealFrameStatus.PATCHED_BY_PREV) + + # TODO: We never get here because we have no trouble with the #pragma component + # Either remove or make it useful + if next_frame and frame: + return next_frame.clone(PyTealFrameStatus.PATCHED_BY_NEXT) + + return None + + for i in range(N): + f = frames[i] + if f and f.status_code() <= PyTealFrameStatus.PYTEAL_GENERATED: + ptf_or_none = infer_source(i) + if ptf_or_none: + inferred.append(i) + frames[i] = ptf_or_none + + return frames, inferred + + def pure_teal(self) -> str: + return "\n".join(tmi.teal_line for tmi in self.as_list()) + + _tabulate_param_defaults: Final[dict[str, str]] = dict( + teal=_TEAL_LINE, + pyteal_hybrid_unparsed=_PYTEAL_HYBRID_UNPARSED, + pyteal=_PYTEAL_NODE_AST_UNPARSED, + teal_line_number=_TEAL_LINE_NUMBER, + teal_column=_TEAL_COLUMN, + teal_column_end=_TEAL_COLUMN_END, + program_counters=_PROGRAM_COUNTERS, + pyteal_component=_PYTEAL_COMPONENT, + pyteal_node_ast_qualname=_PYTEAL_NODE_AST_QUALNAME, + pyteal_filename=_PYTEAL_FILENAME, + pyteal_line_number=_PYTEAL_LINE_NUMBER, + pyteal_line_number_end=_PYTEAL_LINE_NUMBER_END, + pyteal_column=_PYTEAL_COLUMN, + pyteal_column_end=_PYTEAL_COLUMN_END, + pyteal_line=_PYTEAL_LINE, + pyteal_node_ast_source_boundaries=_PYTEAL_NODE_AST_SOURCE_BOUNDARIES, + pyteal_node_ast_none=_PYTEAL_NODE_AST_NONE, + status_code=_STATUS_CODE, + status=_STATUS, + ) + + def tabulate( + self, + *, + tablefmt="fancy_grid", + numalign="right", + omit_headers: bool = False, + omit_repeating_col_except: list[str] | None = None, + post_process_delete_cols: list[str] | None = None, + **kwargs: dict[str, str], + ) -> str: + """ + Tabulate a sourcemap using Python's tabulate package: https://pypi.org/project/tabulate/ + + Columns are named and ordered by the arguments provided + + Args: + tablefmt (default 'fancy_grid'): format specifier used by tabulate. + For choices see: https://github.com/astanin/python-tabulate#table-format + + numalign (default 'right'): alignment of numbers. Choices are 'left', 'right', 'decimal', 'center' or None. + See: https://github.com/astanin/python-tabulate#column-alignment + + omit_headers (default `False`): Do not include the column headers when `True` + + omit_repeating_col_except (default None): specify columns for which repetitions should be printed out. + The Teal source column and constant columns such as the comment "//" column are always repeated regardless of this setting + + post_process_delete_cols (default None): Specify columns to delete after tabulation + + **kwargs: Additional keyword arguments are passed to tabulate to represent desired columns. + The order of these columns as arguments determines the column order. These MUST conform to the following parameters: + + teal (required): Teal target source code. This is the only mandatory column + + const_col_[.*] (optional): specify any number of columns to be treated as constant and always repeated + + pyteal_hybrid_unparsed (optional): PyTeal source via `executing.unparse` when available, or otherwise + via `FrameInfo.code_condext` + + pyteal (optional): PyTeal source via `executing.unparse` when available, or otherwise "" (empty string) + + teal_line_number (optional): Teal target's line number (1-based) + + teal_column (optional): Teal target's 0-indexed starting column (CURRENTLY THIS IS ALWAYS 0) + + teal_column_end (optional): Teal target's 0-indexed right boundary column (CURRENTLY THIS IS len(teal)) + + program_counters (optional): starting program counter as assembled by algod + + pyteal_component (optional): representation of the PyTeal source component mapping to target + + pyteal_node_ast_qualname (optional): the Python qualname of the PyTeal source + + pyteal_filename (optional): the filename of the PyTeal source + + pyteal_line_number (optional): the PyTeal source's beginning line number + + pyteal_line_number_end (optional): the PyTeal source's ending line number + + pyteal_column (optional): the PyTeal source's starting 0-indexed column + + pyteal_column_end (optional): the PyTeal source's ending 0-indexed boundary column + + pyteal_line (optional): the PyTeal source as provided by `FrameInfo.code_context` + + pyteal_node_ast_source_boundaries (optional): formatted representation of the PyTeal source's line and column boundaries. Eg "L17:5-L42:3" + + pyteal_node_ast_none (optional): boolean indicator of whether the AST node was successfully extracted for the PyTeal source + + status_code (optional): `PyTealFrameStatus` int value indicating confidence level for locating the PyTeal source responsible for generated Teal + + status (optional): simlar to `status_code` but with a human readable string representation + + Returns: + A ready to print string containing the table information. + """ + + assert ( + "teal" in kwargs + ), "teal column must be specified, but 'teal' is missing in kwargs" + + # 0. e.g. suppose: + # + # kwargs == dict( + # teal = "// TEAL", + # const_col_2 = "//", + # pyteal_filename = "PATH", + # pyteal_line_number = "LINE", + # const_col_5 = "|", + # pyteal_hybrid_unparsed = "PYTEAL", + # ) + + constant_columns = {} + new_kwargs = {} + for i, (k, v) in enumerate(kwargs.items()): + if k.startswith("const_col_"): + constant_columns[i] = v + else: + new_kwargs[k] = v + + # 1. now we have: + # + # new_kwargs == dict( + # teal = "// TEAL", + # pyteal_filename = "PATH", + # pyteal_line_number = "LINE", + # pyteal_hybrid_unparsed = "PYTEAL", + # ) + # + # and + # + # constant_columns == { + # 1: "//", + # 4: "|", + # } + + for k in new_kwargs: + assert k in self._tabulate_param_defaults, f"unrecognized parameter '{k}'" + + # 2. now we know that all the provided keys were valid + + renames = {self._tabulate_param_defaults[k]: v for k, v in new_kwargs.items()} + + # 3. now we have: + # + # renames == { + # _TEAL_LINE: "// TEAL", + # _PYTEAL_FILENAME: "PATH", + # _PYTEAL_LINE_NUMBER: "LINE", + # _PYTEAL_HYBRID_UNPARSED: "PYTEAL", + # } + + rows = [teal_item.asdict(**renames) for teal_item in self.as_list()] + + # 4. now we've populated the rows: + # + # rows == [ {_TEAL_LINE: 1, _PYTEAL_FILENAME: "foo.py", _PYTEAL_LINE_NUMBER: 79, _PYTEAL_HYBRID_UNPARSED: "Int(42)"}, ... ] + + if constant_columns: + + def add_const_cols(row): + i = 0 + new_row = {} + for k, v in row.items(): + if i in constant_columns: + new_row[f"_{i}"] = constant_columns[i] + i += 1 + new_row[k] = v + i += 1 + return new_row + + rows = list(map(add_const_cols, rows)) + # 5. now we've added the constant columns to the rows: + # + # rows == [ {_TEAL_LINE: 1, "_1": "//", _PYTEAL_FILENAME: "foo.py", _PYTEAL_LINE_NUMBER: 79, "_4": "|", _PYTEAL_HYBRID_UNPARSED: "Int(42)"}, ... ] + # ^^^^^^^^^^ ^^^^^^^^^ + + renames = add_const_cols(renames) + # 6. and we've added the constant columns at the required ordering to the renames as well: + # + # renames == { + # _TEAL_LINE: "// TEAL", + # "_1": "//", + # _PYTEAL_FILENAME: "PATH", + # _PYTEAL_LINE_NUMBER: "LINE", + # "_4": "|", + # _PYTEAL_HYBRID_UNPARSED: "PYTEAL", + # } + + teal_col_name = renames[_TEAL_LINE] + pt_simple_col_name = renames.get(_PYTEAL_COLUMN) + pt_hybrid_col_name = renames.get(_PYTEAL_HYBRID_UNPARSED) + pt_window_col_name = renames.get(_PYTEAL_NODE_AST_SOURCE_BOUNDARIES) + if omit_repeating_col_except: + # Assume the following column structure: + # * col 0 is the generated source with column name stored in `teal_col` + # * the source line number has column name stored in `pyteal_line_number` + # * the pyteal source has column name stored in `pyteal` OR `pyteal_hybrid_unparsed` + # + # Consequently, when `teal_col` is repeating we need to take extra care NOT + # to omit repeating source values, as these were likely coming from different portions of the source + + def reduction(row, next_row): + drop_2nd_pyteal = True + if pt_window_col_name and ( + row[pt_window_col_name] or next_row[pt_window_col_name] + ): + drop_2nd_pyteal = ( + row[pt_window_col_name] == next_row[pt_window_col_name] + ) + else: + drop_2nd_pyteal = row[teal_col_name] != next_row[teal_col_name] + return { + k: v2 + for k, v in row.items() + if any( + [ + (v2 := next_row[k]) != v, + k in omit_repeating_col_except, + k in (pt_hybrid_col_name, pt_simple_col_name) + and not drop_2nd_pyteal, + ] + ) + } + + rows = [rows[0]] + list( + map(lambda r_and_n: reduction(*r_and_n), zip(rows[:-1], rows[1:])) + ) + # 7. now we've removed repetitions of appropriate columns + + if post_process_delete_cols: + for col in post_process_delete_cols: + col_name = renames.pop(col) + for row in rows: + if col_name in row: + del row[col_name] # type: ignore + # 8. now we've removed any columns requested for deletion + + calling_kwargs: dict[str, Any] = {"tablefmt": tablefmt, "numalign": numalign} + if not omit_headers: + calling_kwargs["headers"] = renames + + return tabulate(rows, **calling_kwargs) + + def annotated_teal(self, omit_headers: bool = True, concise: bool = True) -> str: + """ + Helper function that hardcodes various tabulate parameters to produce a + reasonably formatted annotated teal output. + + In theory, the output can be compiled. + + In practice the output maybe be very large and therefore unacceptable to Algod. + + In such cases, you should use the original accompanying Teal compilation. + """ + if not self._built(): + raise ValueError( + "not ready for annotated_teal() because build() has yet to be called" + ) + + if not (r3sm := cast(R3SourceMap, self._cached_r3sourcemap)): + raise self._unexpected_error("R3SourceMap not available but should be") + + if not (file_lines := cast(list[str], r3sm.file_lines)): + raise self._unexpected_error( + "_cached_r3sourcemap.file_lines not available but should be" + ) + + teal_col = "// GENERATED TEAL" + comment_col = "_1" + kwargs = dict( + tablefmt="plain", + omit_headers=omit_headers, + omit_repeating_col_except=[teal_col, comment_col], + numalign="left", + teal=teal_col, + const_col_2="//", + ) + + if self.include_pcs: + kwargs["program_counters"] = "PC" + + if not concise: + kwargs["pyteal_filename"] = "PYTEAL PATH" + kwargs["pyteal_line_number"] = "LINE" + + kwargs["pyteal_hybrid_unparsed"] = "PYTEAL" + kwargs["pyteal_node_ast_source_boundaries"] = "PYTEAL RANGE" + + kwargs["post_process_delete_cols"] = [_PYTEAL_NODE_AST_SOURCE_BOUNDARIES] + annotated = self.tabulate(**kwargs) # type: ignore + + self._validate_annotated(omit_headers, file_lines, annotated.splitlines()) + + return annotated + + @classmethod + def _validate_annotated( + cls, omit_headers: bool, teal_lines: list[str], annotated_lines: list[str] + ): + header_delta = 1 - bool(omit_headers) + if (ltl := len(teal_lines)) + header_delta != (latl := len(annotated_lines)): + raise cls._unexpected_error( + f"mismatch between count of teal_lines ({ltl}) and annotated_lines ({latl}) for the case {omit_headers=}", + ) + + for i, (teal_line, annotated_line) in enumerate( + zip(teal_lines, annotated_lines[header_delta:]) + ): + if not annotated_line.startswith(teal_line): + raise cls._unexpected_error( + f"annotated teal ought to begin exactly with the teal line but line {i+1} [{annotated_line}] doesn't start with [{teal_line}]", + ) + pattern = r"^\s*($|//.*)" + if not re.match(pattern, annotated_line[len(teal_line) :]): + raise cls._unexpected_error( + f"annotated teal ought to begin exactly with the teal line followed by annotation in comments but line {i+1} [{annotated_line}] has non-commented out annotations" + ) + + @classmethod + def _validate_teal_identical( + cls, + original_teal: str, + new_teal: str, + msg: str, + ): + if original_teal == new_teal: + return + + diff = list(unified_diff(original_teal.splitlines(), new_teal.splitlines())) + raise cls._unexpected_error( + f"""{msg}. Original teal differs with new: + {''.join(diff)}""" + ) diff --git a/pyteal/errors.py b/pyteal/errors.py index 121df49e1..4c34ec87e 100644 --- a/pyteal/errors.py +++ b/pyteal/errors.py @@ -86,6 +86,31 @@ def __str__(self): TealPragmaError.__module__ = "pyteal" +class AlgodClientError(Exception): + def __init__(self, message: str) -> None: + self.message = message + + def __str__(self): + return self.message + + +AlgodClientError.__module__ = "pyteal" + + +class SourceMapDisabledError(RuntimeError): + msg = value = """ + Cannot calculate Teal to PyTeal source map because stack frame discovery is turned off. + + To enable source maps, set `enabled = True` in `pyteal.ini`'s [pyteal-source-mapper] section. + """ + + def __str__(self): + return self.msg + + +SourceMapDisabledError.__module__ = "pyteal" + + def verifyProgramVersion(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/__init__.py b/pyteal/ir/__init__.py index e7db57cf8..72348f627 100644 --- a/pyteal/ir/__init__.py +++ b/pyteal/ir/__init__.py @@ -1,22 +1,23 @@ from pyteal.ir.ops import Op, Mode - +from pyteal.ir.tealblock import TealBlock from pyteal.ir.tealcomponent import TealComponent -from pyteal.ir.tealop import TealOp +from pyteal.ir.tealconditionalblock import TealConditionalBlock from pyteal.ir.teallabel import TealLabel -from pyteal.ir.tealblock import TealBlock +from pyteal.ir.tealop import TealOp +from pyteal.ir.tealpragma import TealPragma from pyteal.ir.tealsimpleblock import TealSimpleBlock -from pyteal.ir.tealconditionalblock import TealConditionalBlock from pyteal.ir.labelref import LabelReference __all__ = [ - "Op", + "LabelReference", "Mode", + "Op", + "TealBlock", "TealComponent", - "TealOp", + "TealConditionalBlock", "TealLabel", - "TealBlock", + "TealOp", + "TealPragma", "TealSimpleBlock", - "TealConditionalBlock", - "LabelReference", ] diff --git a/pyteal/ir/tealblock.py b/pyteal/ir/tealblock.py index 8ceed472e..55f3c0a5c 100644 --- a/pyteal/ir/tealblock.py +++ b/pyteal/ir/tealblock.py @@ -14,9 +14,10 @@ class TealBlock(ABC): """Represents a basic block of TealComponents in a graph.""" - def __init__(self, ops: List[TealOp]) -> None: + def __init__(self, ops: List[TealOp], root_expr: "Expr | None" = None) -> None: self.ops = ops self.incoming: List[TealBlock] = [] + self._sframes_container = root_expr @abstractmethod def getOutgoing(self) -> List["TealBlock"]: diff --git a/pyteal/ir/tealcomponent.py b/pyteal/ir/tealcomponent.py index 0dc69cd9e..2704e0f87 100644 --- a/pyteal/ir/tealcomponent.py +++ b/pyteal/ir/tealcomponent.py @@ -1,14 +1,23 @@ from abc import ABC, abstractmethod -from typing import List, Optional, TYPE_CHECKING from contextlib import AbstractContextManager +from typing import TYPE_CHECKING, List, cast + +from pyteal.stack_frame import NatalStackFrame if TYPE_CHECKING: from pyteal.ast import Expr, ScratchSlot, SubroutineDefinition class TealComponent(ABC): - def __init__(self, expr: Optional["Expr"]): - self.expr = expr + def __init__(self, expr: "Expr | None"): + self.expr: Expr | None = expr + + # ALL BELOW: for source mapping only + self._stack_frames: NatalStackFrame | None = None + if not self.expr: # expr already has the frame info + self._stack_frames = NatalStackFrame() + + self._sframes_container: Expr | None = None def getSlots(self) -> List["ScratchSlot"]: return [] @@ -22,6 +31,17 @@ def getSubroutines(self) -> List["SubroutineDefinition"]: def resolveSubroutine(self, subroutine: "SubroutineDefinition", label: str) -> None: pass + def stack_frames(self) -> NatalStackFrame: + from pyteal.ast import Expr + + root_expr = self._sframes_container or self.expr + if root_expr: + if subroot := getattr(root_expr, "_sframes_container", None): + root_expr = cast(Expr, subroot) + return root_expr.stack_frames + + return cast(NatalStackFrame, self._stack_frames) + @abstractmethod def assemble(self) -> str: pass diff --git a/pyteal/ir/tealconditionalblock.py b/pyteal/ir/tealconditionalblock.py index d4056fef3..42e5ec2c7 100644 --- a/pyteal/ir/tealconditionalblock.py +++ b/pyteal/ir/tealconditionalblock.py @@ -7,8 +7,8 @@ class TealConditionalBlock(TealBlock): """Represents a basic block of TealComponents in a graph ending with a branch condition.""" - def __init__(self, ops: List[TealOp]) -> None: - super().__init__(ops) + def __init__(self, ops: List[TealOp], root_expr: "Expr | None" = None) -> None: # type: ignore + super().__init__(ops, root_expr=root_expr) self.trueBlock: TealBlock | None = None self.falseBlock: TealBlock | None = None @@ -35,10 +35,11 @@ def replaceOutgoing(self, oldBlock: TealBlock, newBlock: TealBlock) -> None: self.falseBlock = newBlock def __repr__(self) -> str: - return "TealConditionalBlock({}, true={}, false={})".format( + return "TealConditionalBlock({}, true={}, false={}, conditional={})".format( repr(self.ops), repr(self.trueBlock), repr(self.falseBlock), + repr(self._sframes_container), ) def __eq__(self, other: object) -> bool: diff --git a/pyteal/ir/tealpragma.py b/pyteal/ir/tealpragma.py new file mode 100644 index 000000000..4a0ff5d46 --- /dev/null +++ b/pyteal/ir/tealpragma.py @@ -0,0 +1,24 @@ +from pyteal.ir.tealcomponent import TealComponent + + +class TealPragma(TealComponent): + def __init__(self, version: int): + super().__init__(None) + self.version = version + + def assemble(self) -> str: + return f"#pragma version {self.version}" + + def __repr__(self) -> str: + return f"TealPragma({self.version})" + + def __hash__(self) -> int: + return hash(repr(self)) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, TealPragma): + return False + return self.version == other.version + + +TealPragma.__module__ = "pyteal" diff --git a/pyteal/ir/tealsimpleblock.py b/pyteal/ir/tealsimpleblock.py index e59c74377..18ef7e217 100644 --- a/pyteal/ir/tealsimpleblock.py +++ b/pyteal/ir/tealsimpleblock.py @@ -1,22 +1,20 @@ -from typing import Optional, List - -from pyteal.ir.tealop import TealOp from pyteal.ir.tealblock import TealBlock +from pyteal.ir.tealop import TealOp class TealSimpleBlock(TealBlock): """Represents a basic block of TealComponents in a graph that does not contain a branch condition.""" - def __init__(self, ops: List[TealOp]) -> None: + def __init__(self, ops: list[TealOp]) -> None: super().__init__(ops) - self.nextBlock: Optional[TealBlock] = None + self.nextBlock: TealBlock | None = None self.visited = False def setNextBlock(self, block: TealBlock) -> None: """Set the block that follows this one.""" self.nextBlock = block - def getOutgoing(self) -> List[TealBlock]: + def getOutgoing(self) -> list[TealBlock]: if self.nextBlock is None: return [] return [self.nextBlock] diff --git a/pyteal/stack_frame.py b/pyteal/stack_frame.py new file mode 100644 index 000000000..74fa8469a --- /dev/null +++ b/pyteal/stack_frame.py @@ -0,0 +1,726 @@ +from ast import AST, FunctionDef, unparse +from configparser import ConfigParser +from contextlib import contextmanager +from dataclasses import dataclass +from enum import IntEnum +from inspect import FrameInfo, stack +from typing import Callable, Final, cast +import os +import re + +from executing import Source # type: ignore + + +@dataclass(frozen=True) +class StackFrame: + """ + StackFrame is an _internal_ PyTeal class and is + the first ancestor in the following linear class hierarchy: + StackFrame + | + PyTealFrame + | + compiler.sourcemap.TealMapItem + + Somewhat confusingly, NatalStackFrame is _NOT_ in this hierarchy + so not "is-a" StackFrame, but it pretends to be and "has-a" list of StackFrame's. + + Its most important members are: + * frame_info of type inspect.FrameInfo + * node of type ast.AST + The first is a python representation of a stack frame, and the + second represents a python AST node. The imported package `executing` + features the method `Source.executing` which when run shortly after + the frame_info's creation, _usually_ succeeds in recovering the + associated AST node. + + In the current usage, there is a `creator` member which is a `NatalStackFrame` + object -usually belonging to a PyTeal Expr- and which is assumed to have called + the private constructor `_init_or_drop()`. + + It is not recommended that this class be accessed directly. + + Of special note, is the class variable `_internal_paths`. + This is a whitelist of file patterns which signal to the logic + of method `_frame_info_is_pyteal()` that a particular + frame was _NOT_ created by the user. + + QUIRKS: + * Unfortunately, this means that if a user created file satisfies the above pattern, + the performance of the source mapper will degrade. + * User generated code that includes the pattern `NatalStackFrame` may produce degraded results + """ + + frame_info: FrameInfo + node: AST | None + creator: "NatalStackFrame" + + # for debugging purposes: + full_stack: list[FrameInfo] | None = None + + @classmethod + def _init_or_drop( + cls, creator: "NatalStackFrame", f: FrameInfo, full_stack: list[FrameInfo] + ) -> "StackFrame | None": + """ + Attempt to create a StackFrame object. + However, if the resulting is considered "Python Crud" abandon and return None. + When debugging, also persist the full_stack that was provided. + """ + node = cast(AST | None, Source.executing(f.frame).node) + frame = StackFrame( + f, node, creator, full_stack if NatalStackFrame._debug else None + ) + return frame if frame._not_py_crud() else None + + # TODO: when a source mapper is instantiated, it ought to survey + # the user's project files and warn in the case that some file + # matches the _internal_paths pattern + _internal_paths = [ + "beaker/__init__.py", + "beaker/application.py", + "beaker/consts.py", + "beaker/decorators.py", + "beaker/state.py", + "pyteal/__init__.py", + "pyteal/ast", + "pyteal/compiler", + "pyteal/ir", + "pyteal/pragma", + "pyteal/stack_frame.py", + "tests/abi_roundtrip.py", + "tests/blackbox.py", + "tests/compile_asserts.py", + "tests/mock_version.py", + ] + _internal_paths_re = re.compile("|".join(_internal_paths)) + + def as_pyteal_frame( + self, + rel_paths: bool = True, + parent: "PyTealFrame | None" = None, + ) -> "PyTealFrame": + """ + Downcast one level in the class hierarchy + """ + return PyTealFrame( + frame_info=self.frame_info, + node=self.node, + creator=self.creator, + full_stack=self.full_stack, + rel_paths=rel_paths, + parent=parent, + ) + + @classmethod + def _frame_info_is_right_before_core(cls, f: FrameInfo) -> bool: + # We keep this method around to be used in a test . Originally, + # it was actually used in the __init__ method of StackFrame + # to calculate `last_drop_idx`. However, as -at the time of writing- + # last_drop_idx =ALWAYS= 1, the logic was simplified and + # this method was removed from the live calculation. + return bool(code := f.code_context or []) and "NatalStackFrame" in "".join(code) + + def _is_pyteal(self) -> bool: + return self._frame_info_is_pyteal(self.frame_info) + + @classmethod + def _frame_info_is_pyteal(cls, f: FrameInfo) -> bool: + return bool(cls._internal_paths_re.search(f.filename)) + + def _is_pyteal_import(self) -> bool: + return self._frame_info_is_pyteal_import(self.frame_info) + + @classmethod + def _frame_info_is_pyteal_import(cls, f: FrameInfo) -> bool: + cc = f.code_context + if not cc: + return False + + code = "".join(cc).split() + return "import" in code and "pyteal" in code + + def _not_py_crud(self) -> bool: + """Hackery that depends on C-Python. Not sure how reliable.""" + return self._frame_info_not_py_crud(self.frame_info) + + @classmethod + def _frame_info_not_py_crud(cls, f: FrameInfo) -> bool: + return bool(f.code_context) or not f.filename.startswith("<") + + def __repr__(self) -> str: + node = unparse(n) if (n := self.node) else None + context = "".join(cc) if (cc := (fi := self.frame_info).code_context) else None + return f"{node=}; {context=}; frame_info={fi}" + + def compiler_generated(self) -> bool | None: + if self.creator._compiler_gen: + return True + + return self._frame_info_compiler_generated(self.frame_info) + + @classmethod + def _frame_info_compiler_generated(cls, f: FrameInfo) -> bool | None: + if not (cc := f.code_context): + return None # we don't know / NA + + return "# T2PT" in "".join(cc) + + +def _sourcmapping_is_off(verbose=False) -> bool: + try: + config = ConfigParser() + config.read("pyteal.ini") + enabled = config.getboolean("pyteal-source-mapper", "enabled") + return not enabled + except Exception as e: + if verbose: + print( + f"""Turning off frame capture and disabling sourcemaps. +Could not read section (pyteal-source-mapper, enabled) of config "pyteal.ini": {e}""" + ) + return True + + +def _debug_frames(verbose=False) -> bool: + try: + config = ConfigParser() + config.read("pyteal.ini") + return config.getboolean("pyteal-source-mapper", "debug") + except Exception as e: + if verbose: + print( + f"""Disabling `debug` status for sourcemaps. +Could not read section (pyteal-source-mapper, debug) of config "pyteal.ini": {e}""" + ) + return False + + +@contextmanager +def sourcemapping_off_context(): + """Context manager that turns off sourcemapping for the duration of the context""" + _no_stackframes_before = NatalStackFrame._no_stackframes + _debug_before = NatalStackFrame._debug + NatalStackFrame._no_stackframes = True + NatalStackFrame._debug = False + + try: + yield + + finally: + NatalStackFrame._no_stackframes = _no_stackframes_before + NatalStackFrame._debug = _debug_before + + +class NatalStackFrame: + """ + PyTeal's source mapper deduces the code-location of a user's Expr + via a NatalStackFrame object that is associated with the Expr object. + + When source mapping is disabled (cf. `pyteal.ini`), NatalStackFrame' + constructor is a no-op. + + When source mapping is enabled, it wraps a list of StackFrame's. + + Under normal operations, only the "best" frame is kept in the list, so + the name is misleading. + """ + + _no_stackframes: bool = _sourcmapping_is_off() + _debug: bool = _debug_frames() + _keep_all_debugging = False + + @classmethod + def sourcemapping_is_off(cls, _force_refresh: bool = False) -> bool: + """ + The `_force_refresh` parameter, is mainly for test validation purposes. + It is discouraged for use in the wild because: + * Frames are useful in an "all or nothing" capacity. For example, in preparing + for a source mapping, it would be error prone to generate frames for + a subset of analyzed PyTeal + * Setting `_force_refresh = True` will cause a read from the file system every + time Frames are initialized and will result in significant performance degredation + """ + if _force_refresh: + cls._no_stackframes = _sourcmapping_is_off() + + return cls._no_stackframes + + def __init__( + self, + ): + self._compiler_gen: bool = False + self._frames: list[StackFrame] = [] + + if self.sourcemapping_is_off(): + return + + # 1. get the full stack trace + full_stack = stack() + + # 2. discard frames whose filename begins with "<" + frame_infos = list(filter(StackFrame._frame_info_not_py_crud, full_stack)) + + def _make_stack_frames(fis): + return [ + frame + for f in fis + if (frame := StackFrame._init_or_drop(self, f, full_stack)) + ] + + if self._keep_all_debugging or len(frame_infos) <= 1: + self._frames = _make_stack_frames(frame_infos) + return + + # 3. start the best frame search right after where NatalStackFrame() was constructed + # For more details see the unit test: + # tests/unit/sourcemap_monkey_unit_test.py::test_frame_info_is_right_before_core_last_drop_idx + i = 2 # formerly this was `last_drop_idx = 1; i = last_drop_idx + 1` + + # 4. fast forward the right bound until we're out of pyteal-library code + # This sets last_keep_idx to the first frame index which isn't pyteal + while i < len(frame_infos) and StackFrame._frame_info_is_pyteal(frame_infos[i]): + i += 1 + last_keep_idx = i + + # 5. if the pyteal-library exit point was an import, the expression was + # generated by pyteal itself. So let's back up and look for a "# T2PT*" comment + # which will give us a clue for what to do with this expression + if StackFrame._frame_info_is_pyteal_import(frame_infos[last_keep_idx]): + found = False + i = -1 + for i in range(last_keep_idx - 1, -1, -1): + if StackFrame._frame_info_compiler_generated(frame_infos[i]): + found = True + break + + if found and i >= 0: + last_keep_idx = i + + # 6. Keep only the last frame in the list. We maintain _as_ a list + # since in the case of `self._debug == True`, we'd like access to the full list. + # TODO: this is likely obsolete since full_stack is available on the PyTealFrame object when debugging + frame_infos = frame_infos[last_keep_idx : last_keep_idx + 1] + + # 7. we finish by constructing a list[StackFrame] from our one remaining frame_info + self._frames = _make_stack_frames(frame_infos) + + def __len__(self) -> int: + return len(self._frames) + + def best(self) -> StackFrame: + """ + Return the best guess as to the user-authored birthplace of the + associated StackFrame's + """ + assert ( + self._frames + ), f"expected to have some frames but currently {self._frames=}" + return self._frames[-1] + + def __repr__(self) -> str: + return f"{'C' if self._compiler_gen else 'U'}{self._frames}" + + def nodes(self) -> list[AST | None]: + return [f.node for f in self._frames] + + @classmethod + def _walk_asts(cls, func: Callable[["Expr"], None], *exprs: "Expr") -> None: # type: ignore + from pyteal.ast import ( + Assert, + BinaryExpr, + Cond, + Expr, + Return, + Seq, + SubroutineDeclaration, + ) + from pyteal.ast.frame import Proto + + for expr in exprs: + e = cast(Expr, expr) + func(e) + + match e: + case Assert(): + cls._walk_asts(func, *e.cond) + case BinaryExpr(): + cls._walk_asts(func, e.argLeft, e.argRight) + case Cond(): + cls._walk_asts(func, *(y for x in e.args for y in x)) + case Proto(): + cls._walk_asts(func, e.mem_layout) + case Seq(): + cls._walk_asts(func, *e.args) + case Return(): + cls._walk_asts(func, e.value) + case SubroutineDeclaration(): + cls._walk_asts(func, e.body) + case _: + # TODO: implement more cases, but no need to error as this isn't used for functionality's sake. + pass + + @classmethod + def _debug_asts(cls, *exprs: "Expr") -> None: # type: ignore + """ + For deubgging purposes only! + """ + from pyteal.ast import Expr + + if cls.sourcemapping_is_off(): + return + + def dbg(e: Expr): + print( + type(e), ": ", e.stack_frames.best().as_pyteal_frame().hybrid_unparsed() + ) + + cls._walk_asts(dbg, *exprs) + + @classmethod + def mark_asts_as_compiler_gen(cls, *exprs: "Expr") -> None: # type: ignore + from pyteal.ast import Expr + + if cls.sourcemapping_is_off(): + return + + def mark(e: Expr): + e.stack_frames._compiler_gen = True + + cls._walk_asts(mark, *exprs) + + @classmethod + def reframe_asts(cls, stack_frames: "NatalStackFrame", *exprs: "Expr") -> None: # type: ignore + from pyteal.ast import Expr + + if cls.sourcemapping_is_off(): + return + + def set_frames(e: Expr): + e.stack_frames = stack_frames + + cls._walk_asts(set_frames, *exprs) + + @classmethod + def reframe_ops_in_blocks(cls, root_expr: "Expr", start: "TealBlock") -> None: # type: ignore + start._sframes_container = root_expr + for op in start.ops: + op._sframes_container = root_expr + + if nxt := start.nextBlock: + cls.reframe_ops_in_blocks(root_expr, nxt) + + +class PT_GENERATED: + PRAGMA = "PyTeal generated pragma" + SUBR_LABEL = "PyTeal generated subroutine label" + RETURN_NONE = "PyTeal generated return for TealType.none" + RETURN_VALUE = "PyTeal generated return for non-null TealType" + SUBR_PARAM = "PyTeal generated subroutine parameter handler instruction" + BRANCH = "PyTeal generated branching" + BRANCH_LABEL = "PyTeal generated branching label" + TYPE_ENUM_TXN = "PyTeal generated transaction Type Enum" + TYPE_ENUM_ONCOMPLETE = "PyTeal generated OnComplete Type Enum" + FLAGGED_BY_DEV = "Developer has flagged expression as compiler generated" + + +_PT_GEN = { + "# T2PT0": PT_GENERATED.PRAGMA, + "# T2PT1": PT_GENERATED.SUBR_LABEL, + "# T2PT2": PT_GENERATED.RETURN_NONE, + "# T2PT3": PT_GENERATED.RETURN_VALUE, + "# T2PT4": PT_GENERATED.SUBR_PARAM, + "# T2PT5": PT_GENERATED.BRANCH, + "# T2PT6": PT_GENERATED.BRANCH_LABEL, + "# T2PT7": PT_GENERATED.TYPE_ENUM_TXN, + "# T2PT8": PT_GENERATED.TYPE_ENUM_ONCOMPLETE, +} + + +class PyTealFrameStatus(IntEnum): + """integer values indicate 'confidence' on a scale of 0 - 10""" + + MISSING = 0 + MISSING_AST = 1 + MISSING_CODE = 2 + PYTEAL_GENERATED = 3 + PATCHED_BY_PREV_OVERRIDE_NEXT = 4 + PATCHED_BY_NEXT_OVERRIDE_PREV = 5 + PATCHED_BY_PREV = 6 + PATCHED_BY_NEXT = 7 + PATCHED_BY_PREV_AND_NEXT = 8 + COPACETIC = 9 # currently, 90% confident is as good as it gets + + def human(self) -> str: + match self: + case self.MISSING: + return "sourcemapping line: total failure" + case self.MISSING_AST: + return "unreliable as missing AST" + case self.MISSING_CODE: + return "unreliable as couldn't retrieve source" + case self.PYTEAL_GENERATED: + return "INCOMPLETE" # "incomplete as source not user generated" + case self.PATCHED_BY_PREV_OVERRIDE_NEXT: + return "previously INCOMPLETE- patched to previous frame (ambiguous)" + case self.PATCHED_BY_NEXT_OVERRIDE_PREV: + return "previously INCOMPLETE- patched to next frame (ambiguous)" + case self.PATCHED_BY_PREV: + return "previously INCOMPLETE- patched to previous frame" + case self.PATCHED_BY_NEXT: + return "previously INCOMPLETE- patched to next frame" + case self.PATCHED_BY_PREV_AND_NEXT: + return "previously INCOMPLETE- patched" + case self.COPACETIC: + return "COPACETIC" + + raise Exception(f"unrecognized {type(self)} - THIS SHOULD NEVER HAPPEN!") + + +class PyTealFrame(StackFrame): + """ + PyTealFrame is the middle generation in the StackFrame class hierarchy. + + It adds a richer and more polished set of methods to learn about the PyTeal source. + """ + + def __init__( + self, + frame_info: FrameInfo, + node: AST | None, + creator: NatalStackFrame, + full_stack: list[FrameInfo] | None, + rel_paths: bool = True, + parent: "PyTealFrame | None" = None, + ): + super().__init__(frame_info, node, creator, full_stack) + self.rel_paths: Final[bool] = rel_paths + self.parent: "Final[PyTealFrame | None]" = parent + + self._raw_code: str | None = None + self._status: PyTealFrameStatus | None = None + + def __repr__(self) -> str: + """ + This repr isn't compliant, but keeping as it's useful for debugging in VSCode + """ + return self._str_impl(verbose=False) + + def __eq__(self, other: object) -> bool: + """We don't care about parents here. TODO: this comment is too rude""" + if not isinstance(other, PyTealFrame): + return False + + return all( + [ + self.frame_info == other.frame_info, + self.node == other.node, + self.rel_paths == other.rel_paths, + ] + ) + + def clone(self, status: PyTealFrameStatus) -> "PyTealFrame": + ptf = PyTealFrame( + frame_info=self.frame_info, + node=self.node, + creator=self.creator, + full_stack=self.full_stack, + rel_paths=self.rel_paths, + ) + ptf._status = status + + return ptf + + def location(self) -> str: + return f"{self.file()}:{self.lineno()}" if self.frame_info else "" + + def file(self) -> str: + if not self.frame_info: + return "" + + path = self.frame_info.filename + return os.path.relpath(path) if self.rel_paths else path + + def root(self) -> str: + if not self.frame_info: + return "" + + path = self.frame_info.filename + return path[: -len(self.file())] + + def code_qualname(self) -> str: + return ( + Source.executing(self.frame_info.frame).code_qualname() + if self.frame_info + else "" + ) + + def lineno(self) -> int | None: + naive_lineno = self.frame_info.lineno if self.frame_info else None + if naive_lineno is None: + return naive_lineno + + hybrid_line, offset = self._hybrid_w_offset() + if hybrid_line.startswith(self.raw_code()): + return naive_lineno + + return naive_lineno + offset + + def column(self) -> int: + """Provide accurate 0-indexed column offset when available. Or 0 when not.""" + return self.node_col_offset() or 0 + + def compiler_generated_reason(self) -> str | None: + """ + None indicates either "unkown" or "not compiler generated". + To distinguish between these two usages, call `compiler_generated()` first. + """ + if not self.compiler_generated(): + return None + + if self.creator._compiler_gen: + return PT_GENERATED.FLAGGED_BY_DEV + + for k, v in _PT_GEN.items(): + if k in self.raw_code(): + return v + + raise AssertionError( + "This should never happen as the call to self.compiler_generated() should have prevented this case." + ) + + # VARIATIONS ON THE THEME OF SOURCE CODE: + + def raw_code(self) -> str: + if self._raw_code is None: + self._raw_code = ( + ("".join(self.frame_info.code_context)).strip() + if self.frame_info and self.frame_info.code_context + else "" + ) + + return self._raw_code + + def hybrid_unparsed(self) -> str: + """ + Attempt to unparse the node and return the most apropos line + """ + return self._hybrid_w_offset()[0] + + def _hybrid_w_offset(self) -> tuple[str, int]: + """ + Attempt to unparse the node and return the most apropos line, + together with its offset in comparison with its raw code. + """ + raw_code = self.raw_code() + node = self.node + pt_chunk = self.node_source() + return self._hybrid_impl(raw_code, node, pt_chunk) + + @classmethod + def _hybrid_impl( + cls, code: str, node: AST | None, pt_chunk: str + ) -> tuple[str, int]: + """ + Given a chunk of PyTeal `pt_chunk` represending a node's source, + and `code` representing a FrameInfo's code_context, + return information about the most appropriate code to use in + the source map. + When the node source isn't available, return `code` + and an offset of 0. + When the node source is a single line, return `pt_chunk` + and an offset of 0. + When the node source is a multi-line chunk, in the case of + a non-FunctionDef node, we assume that the offset is 0. + Finally, in the case of a FunctionDef node, find the offset + by finding where the prefix `def` appears in the chunk. + """ + if pt_chunk: + pt_lines = pt_chunk.splitlines() + if len(pt_lines) == 1: + return pt_chunk, 0 + + offset = i = 0 + if pt_lines and isinstance(node, FunctionDef): + code_idx = -1 + for i, line in enumerate(pt_lines): + if line.startswith(code): + code_idx = i + if line.startswith("def"): + if code_idx >= 0: + offset = i - code_idx + return pt_lines[i], offset + if code_idx >= 0: + return pt_lines[code_idx], 0 + + return code, 0 + + # END OF VARIATIONS ON A THEME OF CODE + def failed_ast(self) -> bool: + return not self.node + + def status_code(self) -> PyTealFrameStatus: + if self._status is not None: + return self._status + + if self.frame_info is None: + return PyTealFrameStatus.MISSING + + if self.node is None: + return PyTealFrameStatus.MISSING_AST + + if self.compiler_generated(): + return PyTealFrameStatus.PYTEAL_GENERATED + + if not self.raw_code(): + return PyTealFrameStatus.MISSING_CODE + + return PyTealFrameStatus.COPACETIC + + def status(self) -> str: + return self.status_code().human() + + def node_source(self) -> str: + return unparse(self.node) if self.node else "" + + def node_lineno(self) -> int | None: + return getattr(self.node, "lineno", None) if self.node else None + + def node_col_offset(self) -> int | None: + """0-indexed BEGINNING column offset""" + return getattr(self.node, "col_offset", None) if self.node else None + + def node_end_lineno(self) -> int | None: + return getattr(self.node, "end_lineno", None) if self.node else None + + def node_end_col_offset(self) -> int | None: + """0-indexed ENDING column offset""" + return getattr(self.node, "end_col_offset", None) if self.node else None + + def node_source_window(self) -> str: + boundaries = ( + self.node_lineno(), + self.node_col_offset(), + self.node_end_lineno(), + self.node_end_col_offset(), + ) + if not all(b is not None for b in boundaries): + return "" + return "L{}:{}-L{}:{}".format(*boundaries) + + def __str__(self) -> str: + return self._str_impl(verbose=True) + + def _str_impl(self, verbose: bool = True) -> str: + if not self.frame_info: + return "None" + + spaces = "\n\t\t\t" + short = f"<{self.raw_code()}>{spaces}@{self.location()}" + if not verbose: + return short + + return f"""{short} +{self.frame_info.index=} +{self.frame_info.function=} +{self.frame_info.frame=}""" diff --git a/pyteal/stack_frame_test.py b/pyteal/stack_frame_test.py new file mode 100644 index 000000000..78fcb889b --- /dev/null +++ b/pyteal/stack_frame_test.py @@ -0,0 +1,20 @@ +from unittest.mock import Mock + +from pyteal.stack_frame import NatalStackFrame, StackFrame + + +def test_is_pyteal(): + FrameInfo = Mock() + FrameInfo.return_value = Mock() + + FrameInfo.return_value.filename = "not_pyteal.py" + sf = StackFrame(FrameInfo(), None, NatalStackFrame()) + assert not sf._is_pyteal() + + FrameInfo.return_value.filename = "blahblah/pyteal/ir/blah" + sf = StackFrame(FrameInfo(), None, NatalStackFrame()) + assert sf._is_pyteal() + + FrameInfo.return_value.filename = "blahblah/pyteal/not_really..." + sf = StackFrame(FrameInfo(), None, NatalStackFrame()) + assert not sf._is_pyteal() diff --git a/pyteal/util.py b/pyteal/util.py index 614919fc6..e4c0eec5c 100644 --- a/pyteal/util.py +++ b/pyteal/util.py @@ -1,4 +1,36 @@ -from pyteal.errors import TealInternalError +from algosdk.v2client import algod + +from pyteal.errors import AlgodClientError, TealInternalError + + +def algod_with_assertion( + client: algod.AlgodClient | None = None, msg: str = "" +) -> algod.AlgodClient: + def wrap(e, msg): + raise AlgodClientError(f"{msg}: {e}" if msg else str(e)) + + if not client: + try: + client = _algod_client() + except Exception as e: + wrap(e, msg) + + assert client + try: + if not client.status(): + wrap("algod.status() did not produce any results", msg) + + except Exception as e: + wrap(e, msg) + + return client + + +def _algod_client( + algod_address="http://localhost:4001", algod_token="a" * 64 +) -> algod.AlgodClient: + """Instantiate and return Algod client object.""" + return algod.AlgodClient(algod_token, algod_address) def escapeStr(s: str) -> str: diff --git a/pytest.ini b/pytest.ini index c443cbdee..81d9bbf83 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,3 +1,4 @@ [pytest] markers = serial: marks tests requiring serial execution + slow: marks tests which are slow diff --git a/requirements.txt b/requirements.txt index 5811b9870..fe3b92066 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,6 +5,7 @@ graviton@git+https://github.com/algorand/graviton@v0.9.0 mypy==0.991 pytest==7.2.0 pytest-cov==3.0.0 +pytest-custom-exit-code==0.3.0 pytest-timeout==2.1.0 pytest-xdist==3.0.2 types-setuptools==57.4.18 diff --git a/setup.py b/setup.py index 6f342f8d8..60b0446bf 100644 --- a/setup.py +++ b/setup.py @@ -22,9 +22,11 @@ ), install_requires=[ # when changing this list, also update docs/requirements.txt + "docstring-parser==0.14.1", + "executing==1.2.0", "py-algorand-sdk>=2.0.0,<3.0.0", "semantic-version>=2.9.0,<3.0.0", - "docstring-parser==0.14.1", + "tabulate>=0.9.0,<0.10.0", ], classifiers=[ "Programming Language :: Python :: 3", diff --git a/tests/integration/algod_test.py b/tests/integration/algod_test.py new file mode 100644 index 000000000..84e1deaa5 --- /dev/null +++ b/tests/integration/algod_test.py @@ -0,0 +1,76 @@ +from unittest import mock + +import pytest + +from pyteal.errors import AlgodClientError +from pyteal.util import algod_with_assertion + + +def test_algod_with_assertion_copacetic(): + """ + In C.I. integration tests we expect the happy path for the Algod client + """ + client = algod_with_assertion() + + assert client + + reclient = algod_with_assertion(client) + assert client is reclient + + +def test_algod_errors(): + from algosdk.v2client.algod import AlgodClient + + from pyteal import util + + with mock.patch.object(util, "_algod_client", side_effect=Exception("1337")): + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion() + assert "1337" in str(ace) + + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion(msg="okey dokey") + assert "1337" in str(ace) + assert "okey dokey" in str(ace) + + with mock.patch.object(AlgodClient, "status", side_effect=Exception("42")): + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion() + assert "42" in str(ace) + + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion(msg="yoyo ma") + assert "42" in str(ace) + assert "yoyo ma" in str(ace) + + with mock.patch.object(AlgodClient, "status", lambda _: None): + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion() + assert "did not produce any results" in str(ace) + + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion(msg="mellow yellow") + assert "did not produce any results" in str(ace) + assert "mellow yellow" in str(ace) + + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion("foo") # type: ignore + assert "has no attribute 'status'" in str(ace) + + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion("foo", msg="blarney") # type: ignore + assert "has no attribute 'status'" in str(ace) + assert "blarney" in str(ace) + + client = algod_with_assertion() + assert client + + with mock.patch.object(client, "status", side_effect=Exception("42")): + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion(client) + assert "42" in str(ace) + + with pytest.raises(AlgodClientError) as ace: + algod_with_assertion(client, msg="hokus pokus") + assert "42" in str(ace) + assert "hokus pokus" in str(ace) diff --git a/tests/integration/sourcemap_monkey_integ_test.py b/tests/integration/sourcemap_monkey_integ_test.py new file mode 100644 index 000000000..bf4ee848f --- /dev/null +++ b/tests/integration/sourcemap_monkey_integ_test.py @@ -0,0 +1,245 @@ +from configparser import ConfigParser +from importlib import import_module +from itertools import product +from pathlib import Path +from unittest import mock + +import pytest + +""" +This file monkey-patches ConfigParser in order to enable source mapping +and test the results of source mapping various PyTeal dapps. +""" + + +BRUTE_FORCE_TERRIBLE_SKIPPING = ( + """The second router is flaky due to issue 199, so skipping for now""" +) +FIXTURES = Path.cwd() / "tests" / "integration" / "teal" / "annotated" + +ROUTERS = [ + ("examples.application.abi.algobank", "router"), + ("pyteal.compiler.compiler_test", "FIRST_ROUTER"), +] + +if BRUTE_FORCE_TERRIBLE_SKIPPING: + del ROUTERS[1] + + +@pytest.fixture +def mock_ConfigParser(): + patcher = mock.patch.object(ConfigParser, "getboolean", return_value=True) + patcher.start() + yield + patcher.stop() + + +@pytest.mark.serial +@pytest.mark.parametrize("path, obj", ROUTERS) +@pytest.mark.parametrize("annotate_teal_headers", [True, False]) +@pytest.mark.parametrize("annotate_teal_concise", [True, False]) +def test_sourcemap_annotate( + mock_ConfigParser, path, obj, annotate_teal_headers, annotate_teal_concise +): + from pyteal import OptimizeOptions + + router = getattr(import_module(path), obj) + + a_fname, c_fname = "A.teal", "C.teal" + compile_bundle = router.compile( + version=6, + optimize=OptimizeOptions(scratch_slots=True), + assemble_constants=False, + with_sourcemaps=True, + approval_filename=a_fname, + clear_filename=c_fname, + pcs_in_sourcemap=True, + annotate_teal=True, + annotate_teal_headers=annotate_teal_headers, + annotate_teal_concise=annotate_teal_concise, + ) + + CL = 49 # COMPILATION LINE right before this + CFILE = "tests/integration/sourcemap_monkey_integ_test.py" # this file + COMPILE = "router.compile(version=6, optimize=OptimizeOptions(scratch_slots=True), assemble_constants=False, with_sourcemaps=True, approval_filename=a_fname, clear_filename=c_fname, pcs_in_sourcemap=True, annotate_teal=True, annotate_teal_headers=annotate_teal_headers, annotate_teal_concise=annotate_teal_concise)" + INNERTXN = "InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: recipient.address(), TxnField.amount: amount.get(), TxnField.fee: Int(0)})" + + with_headers_int = int(annotate_teal_headers) + concise_int = int(annotate_teal_concise) + with open( + FIXTURES / f"{router.name}_h{with_headers_int}_c{concise_int}.tealf" + ) as f: + tealf = f.read() + + # less confident that this annotated teal will remain identical in 310, but for now it's working: + EXPECTED_ANNOTATED_TEAL_311 = tealf.format( + CFILE=CFILE, + CL=CL, + COMPILE=COMPILE, + INNERTXN=INNERTXN, + ).strip() + + annotated_approval, annotated_clear = ( + compile_bundle.approval_sourcemap.annotated_teal, + compile_bundle.clear_sourcemap.annotated_teal, + ) + assert annotated_approval + assert annotated_clear + + the_same = EXPECTED_ANNOTATED_TEAL_311 == annotated_approval + print(f"{annotated_approval.splitlines()=}") + assert the_same, first_diff(EXPECTED_ANNOTATED_TEAL_311, annotated_approval) + + raw_approval_lines, raw_clear_lines = ( + compile_bundle.approval_teal.splitlines(), + compile_bundle.clear_teal.splitlines(), + ) + + ann_approval_lines, ann_clear_lines = ( + annotated_approval.splitlines(), + annotated_clear.splitlines(), + ) + + assert len(raw_approval_lines) + with_headers_int == len(ann_approval_lines) + assert len(raw_clear_lines) + with_headers_int == len(ann_clear_lines) + + for i, raw_line in enumerate(raw_approval_lines): + assert ann_approval_lines[i + with_headers_int].startswith(raw_line) + + for i, raw_line in enumerate(raw_clear_lines): + assert ann_clear_lines[i + with_headers_int].startswith(raw_line) + + +def first_diff(expected, actual): + alines = actual.splitlines() + elines = expected.splitlines() + for i, e in enumerate(elines): + if i >= len(alines): + return f"""LINE[{i+1}] missing from actual: +{e}""" + if e != (a := alines[i]): + return f"""LINE[{i+1}] +{e} +VS. +{a} +""" + if len(alines) > len(elines): + return f"""LINE[{len(elines) + 1}] missing from expected: +{alines[len(elines)]}""" + + return "" + + +def assert_lines_start_with(prefixes, lines): + assert len(prefixes) == len(lines) + for prefix, line in zip(prefixes, lines): + assert line.startswith(prefix) + + +@pytest.mark.serial +def test_no_regressions_via_fixtures_algobank(mock_ConfigParser): + import pyteal as pt + + module_path, obj = ROUTERS[0] + algobank_router = getattr(import_module(module_path), obj) + assert algobank_router.name == "AlgoBank" + + actual_approval, actual_clear, _ = algobank_router.compile_program( + version=6, optimize=pt.OptimizeOptions(scratch_slots=True) + ) + + algobank_path = Path.cwd() / "examples" / "application" / "abi" + + with open(algobank_path / "algobank_approval.teal") as f: + expected_approval = f.read() + + with open(algobank_path / "algobank_clear_state.teal") as f: + expected_clear = f.read() + + assert expected_approval == actual_approval + assert expected_clear == actual_clear + + bundle = algobank_router.compile( + version=6, + optimize=pt.OptimizeOptions(scratch_slots=True), + ) + assert expected_approval == bundle.approval_teal + assert expected_clear == bundle.clear_teal + + assert bundle.approval_sourcemap is None + assert bundle.clear_sourcemap is None + + approval_prefixes = expected_approval.splitlines() + clear_prefixes = expected_clear.splitlines() + + def assert_didnt_regress(pcs, headers, concise): + bundle = algobank_router.compile( + version=6, + optimize=pt.OptimizeOptions(scratch_slots=True), + with_sourcemaps=True, + pcs_in_sourcemap=pcs, + annotate_teal=True, + annotate_teal_headers=headers, + annotate_teal_concise=concise, + ) + assert expected_approval == bundle.approval_teal + assert expected_clear == bundle.clear_teal + + actual_approval_lines = bundle.approval_sourcemap.annotated_teal.splitlines() + actual_clear_lines = bundle.clear_sourcemap.annotated_teal.splitlines() + + if headers: + del actual_approval_lines[0] + del actual_clear_lines[0] + + assert_lines_start_with(approval_prefixes, actual_approval_lines) + assert_lines_start_with(clear_prefixes, actual_clear_lines) + + for pcs, headers, concise in product([True, False], repeat=3): + assert_didnt_regress(pcs, headers, concise) + + +RPS = Path.cwd() / "tests" / "teal" + + +@pytest.mark.serial +def test_no_regressions_via_fixtures_rps(mock_ConfigParser): + import pyteal as pt + from tests.teal.rps import approval_program + + actual_approval = pt.compileTeal(approval_program(), pt.Mode.Application, version=6) + + with open(RPS / "rps.teal") as f: + expected_approval = f.read() + + assert expected_approval == actual_approval + + compilation = pt.Compilation(approval_program(), pt.Mode.Application, version=6) + + bundle = compilation.compile() + + assert expected_approval == bundle.teal + + assert bundle.sourcemap is None + + approval_prefixes = expected_approval.splitlines() + + def assert_didnt_regress(pcs, headers, concise): + bundle = compilation.compile( + with_sourcemap=True, + pcs_in_sourcemap=pcs, + annotate_teal=True, + annotate_teal_headers=headers, + annotate_teal_concise=concise, + ) + assert expected_approval == bundle.teal + + actual_approval_lines = bundle.sourcemap.annotated_teal.splitlines() + + if headers: + del actual_approval_lines[0] + + assert_lines_start_with(approval_prefixes, actual_approval_lines) + + for pcs, headers, concise in product([True, False], repeat=3): + assert_didnt_regress(pcs, headers, concise) diff --git a/tests/integration/teal/annotated/AlgoBank_h0_c0.tealf b/tests/integration/teal/annotated/AlgoBank_h0_c0.tealf new file mode 100644 index 000000000..a587c7032 --- /dev/null +++ b/tests/integration/teal/annotated/AlgoBank_h0_c0.tealf @@ -0,0 +1,226 @@ +#pragma version 6 // (0) {CFILE} {CL} {COMPILE} +txn NumAppArgs // (20) +int 0 // (22) +== // (23) +bnz main_l8 // (24) +txna ApplicationArgs 0 // (27) +method "deposit(pay,account)void" // (30) +== // (36) +bnz main_l7 // (37) +txna ApplicationArgs 0 // (40) +method "getBalance(account)uint64" // (43) +== // (49) +bnz main_l6 // (50) +txna ApplicationArgs 0 // (53) +method "withdraw(uint64,account)void" // (56) +== // (62) +bnz main_l5 // (63) +err // (66) +main_l5: // +txn OnCompletion // (67) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +int NoOp // (69) +== // (70) +txn ApplicationID // (71) +int 0 // (73) +!= // (74) +&& // (75) +assert // (76) +txna ApplicationArgs 1 // (77) {CFILE} {CL} {COMPILE} +btoi // (80) +store 3 // (81) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +txna ApplicationArgs 2 // (83) {CFILE} {CL} {COMPILE} +int 0 // (86) +getbyte // (87) +store 4 // (88) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +load 3 // (90) +load 4 // (92) +callsub withdraw_3 // (94) +int 1 // (97) {CFILE} {CL} {COMPILE} +return // (98) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +main_l6: // {CFILE} {CL} {COMPILE} +txn OnCompletion // (99) examples/application/abi/algobank.py 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +int NoOp // (101) +== // (102) +txn ApplicationID // (103) +int 0 // (105) +!= // (106) +&& // (107) +assert // (108) +txna ApplicationArgs 1 // (109) {CFILE} {CL} {COMPILE} +int 0 // (112) +getbyte // (113) +callsub getBalance_2 // (114) +store 2 // (117) examples/application/abi/algobank.py 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +byte 0x151f7c75 // (119) +load 2 // (125) +itob // (127) +concat // (128) +log // (129) +int 1 // (130) {CFILE} {CL} {COMPILE} +return // (131) examples/application/abi/algobank.py 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +main_l7: // {CFILE} {CL} {COMPILE} +txn OnCompletion // (132) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int NoOp // (134) +== // (135) +txn ApplicationID // (136) +int 0 // (138) +!= // (139) +&& // (140) +txn OnCompletion // (141) +int OptIn // (143) +== // (144) +txn ApplicationID // (145) +int 0 // (147) +!= // (148) +&& // (149) +|| // (150) +assert // (151) +txna ApplicationArgs 1 // (152) {CFILE} {CL} {COMPILE} +int 0 // (155) +getbyte // (156) +store 1 // (157) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +txn GroupIndex // (159) {CFILE} {CL} {COMPILE} +int 1 // (161) +- // (162) +store 0 // (163) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +load 0 // (165) {CFILE} {CL} {COMPILE} +gtxns TypeEnum // (167) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int pay // (169) +== // (170) +assert // (171) +load 0 // (172) +load 1 // (174) +callsub deposit_1 // (176) +int 1 // (179) {CFILE} {CL} {COMPILE} +return // (180) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +main_l8: // {CFILE} {CL} {COMPILE} +txn OnCompletion // (181) +int NoOp // (183) +== // (184) +bnz main_l18 // (185) +txn OnCompletion // (188) +int OptIn // (190) +== // (191) +bnz main_l17 // (192) +txn OnCompletion // (195) +int CloseOut // (197) +== // (199) +bnz main_l16 // (200) +txn OnCompletion // (203) +int UpdateApplication // (205) +== // (207) +bnz main_l15 // (208) +txn OnCompletion // (211) +int DeleteApplication // (213) +== // (215) +bnz main_l14 // (216) +err // (219) +main_l14: // +txn ApplicationID // (220) +int 0 // (222) +!= // (223) +assert // (224) +callsub assertsenderiscreator_0 // (225) +int 1 // (228) +return // (229) +main_l15: // +txn ApplicationID // (230) +int 0 // (232) +!= // (233) +assert // (234) +callsub assertsenderiscreator_0 // (235) +int 1 // (238) +return // (239) +main_l16: // +txn ApplicationID // (240) +int 0 // (242) +!= // (243) +assert // (244) +byte "lost" // (245) examples/application/abi/algobank.py 13 Bytes('lost') +byte "lost" // (246) 14 Bytes('lost') +app_global_get // (247) App.globalGet(Bytes('lost')) +txn Sender // (248) Txn.sender() +byte "balance" // (250) Bytes('balance') +app_local_get // (251) App.localGet(Txn.sender(), Bytes('balance')) ++ // (252) App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance')) +app_global_put // (253) 12 App.globalPut(Bytes('lost'), App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance'))) +int 1 // (254) {CFILE} {CL} {COMPILE} +return // (255) +main_l17: // +int 1 // (256) examples/application/abi/algobank.py 23 Approve() +return // (257) +main_l18: // {CFILE} {CL} {COMPILE} +txn ApplicationID // (258) +int 0 // (260) +== // (261) +assert // (262) +int 1 // (263) examples/application/abi/algobank.py 21 Approve() +return // (264) + // 7 def assert_sender_is_creator() -> Expr: +// assert_sender_is_creator // +assertsenderiscreator_0: // +txn Sender // (265) 8 Txn.sender() +global CreatorAddress // (267) Global.creator_address() +== // (269) Txn.sender() == Global.creator_address() +assert // (270) Assert(Txn.sender() == Global.creator_address()) +retsub // (271) 7 def assert_sender_is_creator() -> Expr: + // 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +// deposit // +deposit_1: // +store 6 // (272) {CFILE} {CL} {COMPILE} +store 5 // (274) +load 5 // (276) examples/application/abi/algobank.py 54 payment.get() +gtxns Sender // (278) payment.get().sender() +load 6 // (280) sender.address() +txnas Accounts // (282) +== // (284) payment.get().sender() == sender.address() +assert // (285) Assert(payment.get().sender() == sender.address()) +load 5 // (286) 55 payment.get() +gtxns Receiver // (288) payment.get().receiver() +global CurrentApplicationAddress // (290) Global.current_application_address() +== // (292) payment.get().receiver() == Global.current_application_address() +assert // (293) Assert(payment.get().receiver() == Global.current_application_address()) +load 6 // (294) 57 sender.address() +txnas Accounts // (296) +byte "balance" // (298) 58 Bytes('balance') +load 6 // (299) 59 sender.address() +txnas Accounts // (301) +byte "balance" // (303) Bytes('balance') +app_local_get // (304) App.localGet(sender.address(), Bytes('balance')) +load 5 // (305) payment.get() +gtxns Amount // (307) payment.get().amount() ++ // (309) App.localGet(sender.address(), Bytes('balance')) + payment.get().amount() +app_local_put // (310) 56 App.localPut(sender.address(), Bytes('balance'), App.localGet(sender.address(), Bytes('balance')) + payment.get().amount()) +retsub // (311) 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + // 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +// getBalance // +getBalance_2: // +txnas Accounts // (312) 74 user.address() +byte "balance" // (314) Bytes('balance') +app_local_get // (315) App.localGet(user.address(), Bytes('balance')) +retsub // (316) 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: + // 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +// withdraw // +withdraw_3: // +store 8 // (317) {CFILE} {CL} {COMPILE} +store 7 // (319) +txn Sender // (321) examples/application/abi/algobank.py 98 Txn.sender() +byte "balance" // (323) 99 Bytes('balance') +txn Sender // (324) 100 Txn.sender() +byte "balance" // (326) Bytes('balance') +app_local_get // (327) App.localGet(Txn.sender(), Bytes('balance')) +load 7 // (328) amount.get() +- // (330) App.localGet(Txn.sender(), Bytes('balance')) - amount.get() +app_local_put // (331) 97 App.localPut(Txn.sender(), Bytes('balance'), App.localGet(Txn.sender(), Bytes('balance')) - amount.get()) +itxn_begin // (332) 102 InnerTxnBuilder.Begin() +int pay // (333) 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +itxn_field TypeEnum // (334) 103 {INNERTXN} +load 8 // (336) 106 recipient.address() +txnas Accounts // (338) +itxn_field Receiver // (340) 103 {INNERTXN} +load 7 // (342) 107 amount.get() +itxn_field Amount // (344) 103 {INNERTXN} +int 0 // (346) 108 Int(0) +itxn_field Fee // (347) 103 {INNERTXN} +itxn_submit // (349) 111 InnerTxnBuilder.Submit() +retsub // (350) 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: \ No newline at end of file diff --git a/tests/integration/teal/annotated/AlgoBank_h0_c1.tealf b/tests/integration/teal/annotated/AlgoBank_h0_c1.tealf new file mode 100644 index 000000000..bb5bd4942 --- /dev/null +++ b/tests/integration/teal/annotated/AlgoBank_h0_c1.tealf @@ -0,0 +1,226 @@ +#pragma version 6 // (0) {COMPILE} +txn NumAppArgs // (20) +int 0 // (22) +== // (23) +bnz main_l8 // (24) +txna ApplicationArgs 0 // (27) +method "deposit(pay,account)void" // (30) +== // (36) +bnz main_l7 // (37) +txna ApplicationArgs 0 // (40) +method "getBalance(account)uint64" // (43) +== // (49) +bnz main_l6 // (50) +txna ApplicationArgs 0 // (53) +method "withdraw(uint64,account)void" // (56) +== // (62) +bnz main_l5 // (63) +err // (66) +main_l5: // +txn OnCompletion // (67) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +int NoOp // (69) +== // (70) +txn ApplicationID // (71) +int 0 // (73) +!= // (74) +&& // (75) +assert // (76) +txna ApplicationArgs 1 // (77) {COMPILE} +btoi // (80) +store 3 // (81) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +txna ApplicationArgs 2 // (83) {COMPILE} +int 0 // (86) +getbyte // (87) +store 4 // (88) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +load 3 // (90) +load 4 // (92) +callsub withdraw_3 // (94) +int 1 // (97) {COMPILE} +return // (98) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +main_l6: // {COMPILE} +txn OnCompletion // (99) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +int NoOp // (101) +== // (102) +txn ApplicationID // (103) +int 0 // (105) +!= // (106) +&& // (107) +assert // (108) +txna ApplicationArgs 1 // (109) {COMPILE} +int 0 // (112) +getbyte // (113) +callsub getBalance_2 // (114) +store 2 // (117) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +byte 0x151f7c75 // (119) +load 2 // (125) +itob // (127) +concat // (128) +log // (129) +int 1 // (130) {COMPILE} +return // (131) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +main_l7: // {COMPILE} +txn OnCompletion // (132) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int NoOp // (134) +== // (135) +txn ApplicationID // (136) +int 0 // (138) +!= // (139) +&& // (140) +txn OnCompletion // (141) +int OptIn // (143) +== // (144) +txn ApplicationID // (145) +int 0 // (147) +!= // (148) +&& // (149) +|| // (150) +assert // (151) +txna ApplicationArgs 1 // (152) {COMPILE} +int 0 // (155) +getbyte // (156) +store 1 // (157) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +txn GroupIndex // (159) {COMPILE} +int 1 // (161) +- // (162) +store 0 // (163) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +load 0 // (165) {COMPILE} +gtxns TypeEnum // (167) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int pay // (169) +== // (170) +assert // (171) +load 0 // (172) +load 1 // (174) +callsub deposit_1 // (176) +int 1 // (179) {COMPILE} +return // (180) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +main_l8: // {COMPILE} +txn OnCompletion // (181) +int NoOp // (183) +== // (184) +bnz main_l18 // (185) +txn OnCompletion // (188) +int OptIn // (190) +== // (191) +bnz main_l17 // (192) +txn OnCompletion // (195) +int CloseOut // (197) +== // (199) +bnz main_l16 // (200) +txn OnCompletion // (203) +int UpdateApplication // (205) +== // (207) +bnz main_l15 // (208) +txn OnCompletion // (211) +int DeleteApplication // (213) +== // (215) +bnz main_l14 // (216) +err // (219) +main_l14: // +txn ApplicationID // (220) +int 0 // (222) +!= // (223) +assert // (224) +callsub assertsenderiscreator_0 // (225) +int 1 // (228) +return // (229) +main_l15: // +txn ApplicationID // (230) +int 0 // (232) +!= // (233) +assert // (234) +callsub assertsenderiscreator_0 // (235) +int 1 // (238) +return // (239) +main_l16: // +txn ApplicationID // (240) +int 0 // (242) +!= // (243) +assert // (244) +byte "lost" // (245) Bytes('lost') +byte "lost" // (246) Bytes('lost') +app_global_get // (247) App.globalGet(Bytes('lost')) +txn Sender // (248) Txn.sender() +byte "balance" // (250) Bytes('balance') +app_local_get // (251) App.localGet(Txn.sender(), Bytes('balance')) ++ // (252) App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance')) +app_global_put // (253) App.globalPut(Bytes('lost'), App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance'))) +int 1 // (254) {COMPILE} +return // (255) +main_l17: // +int 1 // (256) Approve() +return // (257) +main_l18: // {COMPILE} +txn ApplicationID // (258) +int 0 // (260) +== // (261) +assert // (262) +int 1 // (263) Approve() +return // (264) + // def assert_sender_is_creator() -> Expr: +// assert_sender_is_creator // +assertsenderiscreator_0: // +txn Sender // (265) Txn.sender() +global CreatorAddress // (267) Global.creator_address() +== // (269) Txn.sender() == Global.creator_address() +assert // (270) Assert(Txn.sender() == Global.creator_address()) +retsub // (271) def assert_sender_is_creator() -> Expr: + // def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +// deposit // +deposit_1: // +store 6 // (272) {COMPILE} +store 5 // (274) +load 5 // (276) payment.get() +gtxns Sender // (278) payment.get().sender() +load 6 // (280) sender.address() +txnas Accounts // (282) +== // (284) payment.get().sender() == sender.address() +assert // (285) Assert(payment.get().sender() == sender.address()) +load 5 // (286) payment.get() +gtxns Receiver // (288) payment.get().receiver() +global CurrentApplicationAddress // (290) Global.current_application_address() +== // (292) payment.get().receiver() == Global.current_application_address() +assert // (293) Assert(payment.get().receiver() == Global.current_application_address()) +load 6 // (294) sender.address() +txnas Accounts // (296) +byte "balance" // (298) Bytes('balance') +load 6 // (299) sender.address() +txnas Accounts // (301) +byte "balance" // (303) Bytes('balance') +app_local_get // (304) App.localGet(sender.address(), Bytes('balance')) +load 5 // (305) payment.get() +gtxns Amount // (307) payment.get().amount() ++ // (309) App.localGet(sender.address(), Bytes('balance')) + payment.get().amount() +app_local_put // (310) App.localPut(sender.address(), Bytes('balance'), App.localGet(sender.address(), Bytes('balance')) + payment.get().amount()) +retsub // (311) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + // def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +// getBalance // +getBalance_2: // +txnas Accounts // (312) user.address() +byte "balance" // (314) Bytes('balance') +app_local_get // (315) App.localGet(user.address(), Bytes('balance')) +retsub // (316) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: + // def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +// withdraw // +withdraw_3: // +store 8 // (317) {COMPILE} +store 7 // (319) +txn Sender // (321) Txn.sender() +byte "balance" // (323) Bytes('balance') +txn Sender // (324) Txn.sender() +byte "balance" // (326) Bytes('balance') +app_local_get // (327) App.localGet(Txn.sender(), Bytes('balance')) +load 7 // (328) amount.get() +- // (330) App.localGet(Txn.sender(), Bytes('balance')) - amount.get() +app_local_put // (331) App.localPut(Txn.sender(), Bytes('balance'), App.localGet(Txn.sender(), Bytes('balance')) - amount.get()) +itxn_begin // (332) InnerTxnBuilder.Begin() +int pay // (333) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +itxn_field TypeEnum // (334) {INNERTXN} +load 8 // (336) recipient.address() +txnas Accounts // (338) +itxn_field Receiver // (340) {INNERTXN} +load 7 // (342) amount.get() +itxn_field Amount // (344) {INNERTXN} +int 0 // (346) Int(0) +itxn_field Fee // (347) {INNERTXN} +itxn_submit // (349) InnerTxnBuilder.Submit() +retsub // (350) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: diff --git a/tests/integration/teal/annotated/AlgoBank_h1_c0.tealf b/tests/integration/teal/annotated/AlgoBank_h1_c0.tealf new file mode 100644 index 000000000..159e10131 --- /dev/null +++ b/tests/integration/teal/annotated/AlgoBank_h1_c0.tealf @@ -0,0 +1,227 @@ +// GENERATED TEAL // PC PYTEAL PATH LINE PYTEAL +#pragma version 6 // (0) {CFILE} {CL} {COMPILE} +txn NumAppArgs // (20) +int 0 // (22) +== // (23) +bnz main_l8 // (24) +txna ApplicationArgs 0 // (27) +method "deposit(pay,account)void" // (30) +== // (36) +bnz main_l7 // (37) +txna ApplicationArgs 0 // (40) +method "getBalance(account)uint64" // (43) +== // (49) +bnz main_l6 // (50) +txna ApplicationArgs 0 // (53) +method "withdraw(uint64,account)void" // (56) +== // (62) +bnz main_l5 // (63) +err // (66) +main_l5: // +txn OnCompletion // (67) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +int NoOp // (69) +== // (70) +txn ApplicationID // (71) +int 0 // (73) +!= // (74) +&& // (75) +assert // (76) +txna ApplicationArgs 1 // (77) {CFILE} {CL} {COMPILE} +btoi // (80) +store 3 // (81) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +txna ApplicationArgs 2 // (83) {CFILE} {CL} {COMPILE} +int 0 // (86) +getbyte // (87) +store 4 // (88) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +load 3 // (90) +load 4 // (92) +callsub withdraw_3 // (94) +int 1 // (97) {CFILE} {CL} {COMPILE} +return // (98) examples/application/abi/algobank.py 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +main_l6: // {CFILE} {CL} {COMPILE} +txn OnCompletion // (99) examples/application/abi/algobank.py 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +int NoOp // (101) +== // (102) +txn ApplicationID // (103) +int 0 // (105) +!= // (106) +&& // (107) +assert // (108) +txna ApplicationArgs 1 // (109) {CFILE} {CL} {COMPILE} +int 0 // (112) +getbyte // (113) +callsub getBalance_2 // (114) +store 2 // (117) examples/application/abi/algobank.py 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +byte 0x151f7c75 // (119) +load 2 // (125) +itob // (127) +concat // (128) +log // (129) +int 1 // (130) {CFILE} {CL} {COMPILE} +return // (131) examples/application/abi/algobank.py 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +main_l7: // {CFILE} {CL} {COMPILE} +txn OnCompletion // (132) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int NoOp // (134) +== // (135) +txn ApplicationID // (136) +int 0 // (138) +!= // (139) +&& // (140) +txn OnCompletion // (141) +int OptIn // (143) +== // (144) +txn ApplicationID // (145) +int 0 // (147) +!= // (148) +&& // (149) +|| // (150) +assert // (151) +txna ApplicationArgs 1 // (152) {CFILE} {CL} {COMPILE} +int 0 // (155) +getbyte // (156) +store 1 // (157) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +txn GroupIndex // (159) {CFILE} {CL} {COMPILE} +int 1 // (161) +- // (162) +store 0 // (163) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +load 0 // (165) {CFILE} {CL} {COMPILE} +gtxns TypeEnum // (167) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int pay // (169) +== // (170) +assert // (171) +load 0 // (172) +load 1 // (174) +callsub deposit_1 // (176) +int 1 // (179) {CFILE} {CL} {COMPILE} +return // (180) examples/application/abi/algobank.py 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +main_l8: // {CFILE} {CL} {COMPILE} +txn OnCompletion // (181) +int NoOp // (183) +== // (184) +bnz main_l18 // (185) +txn OnCompletion // (188) +int OptIn // (190) +== // (191) +bnz main_l17 // (192) +txn OnCompletion // (195) +int CloseOut // (197) +== // (199) +bnz main_l16 // (200) +txn OnCompletion // (203) +int UpdateApplication // (205) +== // (207) +bnz main_l15 // (208) +txn OnCompletion // (211) +int DeleteApplication // (213) +== // (215) +bnz main_l14 // (216) +err // (219) +main_l14: // +txn ApplicationID // (220) +int 0 // (222) +!= // (223) +assert // (224) +callsub assertsenderiscreator_0 // (225) +int 1 // (228) +return // (229) +main_l15: // +txn ApplicationID // (230) +int 0 // (232) +!= // (233) +assert // (234) +callsub assertsenderiscreator_0 // (235) +int 1 // (238) +return // (239) +main_l16: // +txn ApplicationID // (240) +int 0 // (242) +!= // (243) +assert // (244) +byte "lost" // (245) examples/application/abi/algobank.py 13 Bytes('lost') +byte "lost" // (246) 14 Bytes('lost') +app_global_get // (247) App.globalGet(Bytes('lost')) +txn Sender // (248) Txn.sender() +byte "balance" // (250) Bytes('balance') +app_local_get // (251) App.localGet(Txn.sender(), Bytes('balance')) ++ // (252) App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance')) +app_global_put // (253) 12 App.globalPut(Bytes('lost'), App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance'))) +int 1 // (254) {CFILE} {CL} {COMPILE} +return // (255) +main_l17: // +int 1 // (256) examples/application/abi/algobank.py 23 Approve() +return // (257) +main_l18: // {CFILE} {CL} {COMPILE} +txn ApplicationID // (258) +int 0 // (260) +== // (261) +assert // (262) +int 1 // (263) examples/application/abi/algobank.py 21 Approve() +return // (264) + // 7 def assert_sender_is_creator() -> Expr: +// assert_sender_is_creator // +assertsenderiscreator_0: // +txn Sender // (265) 8 Txn.sender() +global CreatorAddress // (267) Global.creator_address() +== // (269) Txn.sender() == Global.creator_address() +assert // (270) Assert(Txn.sender() == Global.creator_address()) +retsub // (271) 7 def assert_sender_is_creator() -> Expr: + // 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +// deposit // +deposit_1: // +store 6 // (272) {CFILE} {CL} {COMPILE} +store 5 // (274) +load 5 // (276) examples/application/abi/algobank.py 54 payment.get() +gtxns Sender // (278) payment.get().sender() +load 6 // (280) sender.address() +txnas Accounts // (282) +== // (284) payment.get().sender() == sender.address() +assert // (285) Assert(payment.get().sender() == sender.address()) +load 5 // (286) 55 payment.get() +gtxns Receiver // (288) payment.get().receiver() +global CurrentApplicationAddress // (290) Global.current_application_address() +== // (292) payment.get().receiver() == Global.current_application_address() +assert // (293) Assert(payment.get().receiver() == Global.current_application_address()) +load 6 // (294) 57 sender.address() +txnas Accounts // (296) +byte "balance" // (298) 58 Bytes('balance') +load 6 // (299) 59 sender.address() +txnas Accounts // (301) +byte "balance" // (303) Bytes('balance') +app_local_get // (304) App.localGet(sender.address(), Bytes('balance')) +load 5 // (305) payment.get() +gtxns Amount // (307) payment.get().amount() ++ // (309) App.localGet(sender.address(), Bytes('balance')) + payment.get().amount() +app_local_put // (310) 56 App.localPut(sender.address(), Bytes('balance'), App.localGet(sender.address(), Bytes('balance')) + payment.get().amount()) +retsub // (311) 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + // 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +// getBalance // +getBalance_2: // +txnas Accounts // (312) 74 user.address() +byte "balance" // (314) Bytes('balance') +app_local_get // (315) App.localGet(user.address(), Bytes('balance')) +retsub // (316) 65 def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: + // 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +// withdraw // +withdraw_3: // +store 8 // (317) {CFILE} {CL} {COMPILE} +store 7 // (319) +txn Sender // (321) examples/application/abi/algobank.py 98 Txn.sender() +byte "balance" // (323) 99 Bytes('balance') +txn Sender // (324) 100 Txn.sender() +byte "balance" // (326) Bytes('balance') +app_local_get // (327) App.localGet(Txn.sender(), Bytes('balance')) +load 7 // (328) amount.get() +- // (330) App.localGet(Txn.sender(), Bytes('balance')) - amount.get() +app_local_put // (331) 97 App.localPut(Txn.sender(), Bytes('balance'), App.localGet(Txn.sender(), Bytes('balance')) - amount.get()) +itxn_begin // (332) 102 InnerTxnBuilder.Begin() +int pay // (333) 41 def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +itxn_field TypeEnum // (334) 103 {INNERTXN} +load 8 // (336) 106 recipient.address() +txnas Accounts // (338) +itxn_field Receiver // (340) 103 {INNERTXN} +load 7 // (342) 107 amount.get() +itxn_field Amount // (344) 103 {INNERTXN} +int 0 // (346) 108 Int(0) +itxn_field Fee // (347) 103 {INNERTXN} +itxn_submit // (349) 111 InnerTxnBuilder.Submit() +retsub // (350) 78 def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: \ No newline at end of file diff --git a/tests/integration/teal/annotated/AlgoBank_h1_c1.tealf b/tests/integration/teal/annotated/AlgoBank_h1_c1.tealf new file mode 100644 index 000000000..66ebfbd61 --- /dev/null +++ b/tests/integration/teal/annotated/AlgoBank_h1_c1.tealf @@ -0,0 +1,227 @@ +// GENERATED TEAL // PC PYTEAL +#pragma version 6 // (0) {COMPILE} +txn NumAppArgs // (20) +int 0 // (22) +== // (23) +bnz main_l8 // (24) +txna ApplicationArgs 0 // (27) +method "deposit(pay,account)void" // (30) +== // (36) +bnz main_l7 // (37) +txna ApplicationArgs 0 // (40) +method "getBalance(account)uint64" // (43) +== // (49) +bnz main_l6 // (50) +txna ApplicationArgs 0 // (53) +method "withdraw(uint64,account)void" // (56) +== // (62) +bnz main_l5 // (63) +err // (66) +main_l5: // +txn OnCompletion // (67) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +int NoOp // (69) +== // (70) +txn ApplicationID // (71) +int 0 // (73) +!= // (74) +&& // (75) +assert // (76) +txna ApplicationArgs 1 // (77) {COMPILE} +btoi // (80) +store 3 // (81) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +txna ApplicationArgs 2 // (83) {COMPILE} +int 0 // (86) +getbyte // (87) +store 4 // (88) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +load 3 // (90) +load 4 // (92) +callsub withdraw_3 // (94) +int 1 // (97) {COMPILE} +return // (98) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +main_l6: // {COMPILE} +txn OnCompletion // (99) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +int NoOp // (101) +== // (102) +txn ApplicationID // (103) +int 0 // (105) +!= // (106) +&& // (107) +assert // (108) +txna ApplicationArgs 1 // (109) {COMPILE} +int 0 // (112) +getbyte // (113) +callsub getBalance_2 // (114) +store 2 // (117) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +byte 0x151f7c75 // (119) +load 2 // (125) +itob // (127) +concat // (128) +log // (129) +int 1 // (130) {COMPILE} +return // (131) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +main_l7: // {COMPILE} +txn OnCompletion // (132) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int NoOp // (134) +== // (135) +txn ApplicationID // (136) +int 0 // (138) +!= // (139) +&& // (140) +txn OnCompletion // (141) +int OptIn // (143) +== // (144) +txn ApplicationID // (145) +int 0 // (147) +!= // (148) +&& // (149) +|| // (150) +assert // (151) +txna ApplicationArgs 1 // (152) {COMPILE} +int 0 // (155) +getbyte // (156) +store 1 // (157) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +txn GroupIndex // (159) {COMPILE} +int 1 // (161) +- // (162) +store 0 // (163) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +load 0 // (165) {COMPILE} +gtxns TypeEnum // (167) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +int pay // (169) +== // (170) +assert // (171) +load 0 // (172) +load 1 // (174) +callsub deposit_1 // (176) +int 1 // (179) {COMPILE} +return // (180) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +main_l8: // {COMPILE} +txn OnCompletion // (181) +int NoOp // (183) +== // (184) +bnz main_l18 // (185) +txn OnCompletion // (188) +int OptIn // (190) +== // (191) +bnz main_l17 // (192) +txn OnCompletion // (195) +int CloseOut // (197) +== // (199) +bnz main_l16 // (200) +txn OnCompletion // (203) +int UpdateApplication // (205) +== // (207) +bnz main_l15 // (208) +txn OnCompletion // (211) +int DeleteApplication // (213) +== // (215) +bnz main_l14 // (216) +err // (219) +main_l14: // +txn ApplicationID // (220) +int 0 // (222) +!= // (223) +assert // (224) +callsub assertsenderiscreator_0 // (225) +int 1 // (228) +return // (229) +main_l15: // +txn ApplicationID // (230) +int 0 // (232) +!= // (233) +assert // (234) +callsub assertsenderiscreator_0 // (235) +int 1 // (238) +return // (239) +main_l16: // +txn ApplicationID // (240) +int 0 // (242) +!= // (243) +assert // (244) +byte "lost" // (245) Bytes('lost') +byte "lost" // (246) Bytes('lost') +app_global_get // (247) App.globalGet(Bytes('lost')) +txn Sender // (248) Txn.sender() +byte "balance" // (250) Bytes('balance') +app_local_get // (251) App.localGet(Txn.sender(), Bytes('balance')) ++ // (252) App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance')) +app_global_put // (253) App.globalPut(Bytes('lost'), App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance'))) +int 1 // (254) {COMPILE} +return // (255) +main_l17: // +int 1 // (256) Approve() +return // (257) +main_l18: // {COMPILE} +txn ApplicationID // (258) +int 0 // (260) +== // (261) +assert // (262) +int 1 // (263) Approve() +return // (264) + // def assert_sender_is_creator() -> Expr: +// assert_sender_is_creator // +assertsenderiscreator_0: // +txn Sender // (265) Txn.sender() +global CreatorAddress // (267) Global.creator_address() +== // (269) Txn.sender() == Global.creator_address() +assert // (270) Assert(Txn.sender() == Global.creator_address()) +retsub // (271) def assert_sender_is_creator() -> Expr: + // def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +// deposit // +deposit_1: // +store 6 // (272) {COMPILE} +store 5 // (274) +load 5 // (276) payment.get() +gtxns Sender // (278) payment.get().sender() +load 6 // (280) sender.address() +txnas Accounts // (282) +== // (284) payment.get().sender() == sender.address() +assert // (285) Assert(payment.get().sender() == sender.address()) +load 5 // (286) payment.get() +gtxns Receiver // (288) payment.get().receiver() +global CurrentApplicationAddress // (290) Global.current_application_address() +== // (292) payment.get().receiver() == Global.current_application_address() +assert // (293) Assert(payment.get().receiver() == Global.current_application_address()) +load 6 // (294) sender.address() +txnas Accounts // (296) +byte "balance" // (298) Bytes('balance') +load 6 // (299) sender.address() +txnas Accounts // (301) +byte "balance" // (303) Bytes('balance') +app_local_get // (304) App.localGet(sender.address(), Bytes('balance')) +load 5 // (305) payment.get() +gtxns Amount // (307) payment.get().amount() ++ // (309) App.localGet(sender.address(), Bytes('balance')) + payment.get().amount() +app_local_put // (310) App.localPut(sender.address(), Bytes('balance'), App.localGet(sender.address(), Bytes('balance')) + payment.get().amount()) +retsub // (311) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: + // def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: +// getBalance // +getBalance_2: // +txnas Accounts // (312) user.address() +byte "balance" // (314) Bytes('balance') +app_local_get // (315) App.localGet(user.address(), Bytes('balance')) +retsub // (316) def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr: + // def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: +// withdraw // +withdraw_3: // +store 8 // (317) {COMPILE} +store 7 // (319) +txn Sender // (321) Txn.sender() +byte "balance" // (323) Bytes('balance') +txn Sender // (324) Txn.sender() +byte "balance" // (326) Bytes('balance') +app_local_get // (327) App.localGet(Txn.sender(), Bytes('balance')) +load 7 // (328) amount.get() +- // (330) App.localGet(Txn.sender(), Bytes('balance')) - amount.get() +app_local_put // (331) App.localPut(Txn.sender(), Bytes('balance'), App.localGet(Txn.sender(), Bytes('balance')) - amount.get()) +itxn_begin // (332) InnerTxnBuilder.Begin() +int pay // (333) def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr: +itxn_field TypeEnum // (334) {INNERTXN} +load 8 // (336) recipient.address() +txnas Accounts // (338) +itxn_field Receiver // (340) {INNERTXN} +load 7 // (342) amount.get() +itxn_field Amount // (344) {INNERTXN} +int 0 // (346) Int(0) +itxn_field Fee // (347) {INNERTXN} +itxn_submit // (349) InnerTxnBuilder.Submit() +retsub // (350) def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr: \ No newline at end of file diff --git a/tests/teal/__init__.py b/tests/teal/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/teal/rps.py b/tests/teal/rps.py new file mode 100644 index 000000000..8b4756de4 --- /dev/null +++ b/tests/teal/rps.py @@ -0,0 +1,249 @@ +""" Rock Paper Scissors example taken from https://github.com/gr8h/project_rps (15 Feb 2023) +""" + +from pyteal import * +from pyteal.ast.bytes import Bytes +from .rps_helpers import program + + +def approval_program(): + + # locals + local_opponent = Bytes("opponent") # byteslice + local_bet = Bytes("bet") # uint64 + local_private_play = Bytes("private_play") # byteslice + local_play = Bytes("play") # byteslice + + op_challenge = Bytes("challenge") + op_accept = Bytes("accept") + op_reveal = Bytes("reveal") + + @Subroutine(TealType.none) + def reset(account: Expr): + return Seq( + App.localPut(account, local_opponent, Bytes("")), + App.localPut(account, local_bet, Int(0)), + App.localPut(account, local_private_play, Bytes("")), + App.localPut(account, local_play, Bytes("")), + ) + + @Subroutine(TealType.uint64) + def is_account_empty(account: Expr): + return Return( + And( + App.localGet(account, local_opponent) == Bytes(""), + App.localGet(account, local_bet) == Int(0), + App.localGet(account, local_private_play) == Bytes(""), + App.localGet(account, local_play) == Bytes(""), + ) + ) + + @Subroutine(TealType.uint64) + def is_valid_play(play: Expr): + first_ch = ScratchVar(TealType.bytes) + return Seq( + first_ch.store(Substring(play, Int(0), Int(1))), + Return( + Or( + first_ch.load() == Bytes("r"), + first_ch.load() == Bytes("p"), + first_ch.load() == Bytes("s"), + ) + ), + ) + + @Subroutine(TealType.uint64) + def play_value(p: Expr): + first_letter = ScratchVar() + return Seq( + first_letter.store(Substring(p, Int(0), Int(1))), + Return( + Cond( + [first_letter.load() == Bytes("r"), Int(0)], + [first_letter.load() == Bytes("p"), Int(1)], + [first_letter.load() == Bytes("s"), Int(2)], + ) + ), + ) + + @Subroutine(TealType.uint64) + def get_winner_account_index(challanger_play: Expr, opponent_play: Expr): + return Return( + Cond( + [challanger_play == opponent_play, Int(2)], # a tie + [ + (challanger_play + Int(1)) % Int(3) == opponent_play, + Int(1), + ], # opponent wins + [ + (opponent_play + Int(1)) % Int(3) == challanger_play, + Int(0), + ], # challanger wins + ) + ) + + @Subroutine(TealType.none) + def send_reward(account_index: Expr, amount: Expr): + return Seq( + InnerTxnBuilder.Begin(), + InnerTxnBuilder.SetFields( + { + TxnField.type_enum: TxnType.Payment, + TxnField.receiver: Txn.accounts[account_index], + TxnField.amount: amount, + } + ), + InnerTxnBuilder.Submit(), + ) + + @Subroutine(TealType.none) + def create_challenge(): + return Seq( + Assert( + And( + # check that we have two txn, one for the payment + Global.group_size() == Int(2), + Txn.group_index() == Int(0), + # security checks + Gtxn[1].close_remainder_to() == Global.zero_address(), + Gtxn[0].rekey_to() == Global.zero_address(), + Gtxn[1].rekey_to() == Global.zero_address(), + # check 2nd txn is a payment for the bet + Gtxn[1].type_enum() == TxnType.Payment, + # check the reciver is the application + Gtxn[1].receiver() == Global.current_application_address(), + # check the amount == the amount of the challanger + Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), + # check that the challanger is opted in + App.optedIn(Txn.accounts[1], Global.current_application_id()), + # check that challanger account is the correct account + Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), + # check hashed/private play + Txn.application_args.length() == Int(2), + is_valid_play(Txn.application_args[1]), + ) + ), + App.localPut(Txn.sender(), local_opponent, Txn.accounts[1]), + App.localPut(Txn.sender(), local_bet, Gtxn[1].amount()), + App.localPut(Txn.sender(), local_play, Txn.application_args[1]), + Approve(), + ) + + @Subroutine(TealType.none) + def accept_challenge(): + return Seq( + Assert( + And( + # check that we have two txn, one for the payment + Global.group_size() == Int(2), + Txn.group_index() == Int(0), + # security checks + Gtxn[1].close_remainder_to() == Global.zero_address(), + Gtxn[0].rekey_to() == Global.zero_address(), + Gtxn[1].rekey_to() == Global.zero_address(), + # check 2nd txn is a payment for the bet + Gtxn[1].type_enum() == TxnType.Payment, + # check the reciver is the application + Gtxn[1].receiver() == Global.current_application_address(), + # check that the opponent is opted in + App.optedIn(Txn.accounts[1], Global.current_application_id()), + # check accounts avilability to play + is_account_empty(Txn.sender()), + is_account_empty(Txn.accounts[1]), + # check hashed/private play + Txn.application_args.length() == Int(2), + ) + ), + App.localPut(Txn.sender(), local_opponent, Txn.accounts[1]), + App.localPut(Txn.sender(), local_bet, Gtxn[1].amount()), + App.localPut(Txn.sender(), local_private_play, Txn.application_args[1]), + Approve(), + ) + + @Subroutine(TealType.none) + def reveal(): + challenger_play = ScratchVar(TealType.uint64) + opponent_play = ScratchVar(TealType.uint64) + winner_index = ScratchVar(TealType.uint64) + bet = ScratchVar(TealType.uint64) + + return Seq( + Assert( + And( + # check that we have one txn + Global.group_size() == Int(1), + Txn.group_index() == Int(0), + # security checks + Gtxn[0].rekey_to() == Global.zero_address(), + # check accounts are opponents to each other + App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], + App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), + # check accounts have the same bet + App.localGet(Txn.sender(), local_bet) + == App.localGet(Txn.accounts[1], local_bet), + # check account has submit a play + App.localGet(Txn.sender(), local_private_play) != Bytes(""), + App.localGet(Txn.accounts[1], local_play) != Bytes(""), + # the private_play is valid + Txn.application_args.length() == Int(2), + Sha256(Txn.application_args[1]) + == App.localGet(Int(0), local_private_play), + ) + ), + challenger_play.store(play_value(Txn.application_args[1])), + opponent_play.store(play_value(App.localGet(Txn.accounts[1], local_play))), + bet.store(App.localGet(Txn.sender(), local_bet)), + winner_index.store( + get_winner_account_index(challenger_play.load(), opponent_play.load()) + ), + If(winner_index.load() == Int(2)) + .Then( + Seq( + send_reward(Txn.sender(), bet.load()), + send_reward(Txn.accounts[1], bet.load()), + ) + ) + .Else(Seq(send_reward(winner_index.load(), bet.load() * Int(2)))), + reset(Txn.sender()), + reset(Txn.accounts[1]), + Approve(), + ) + + return program.event( + init=Approve(), + opt_in=Seq( + reset(Int(0)), + Approve(), + ), + no_op=Seq( + Cond( + [ + Txn.application_args[0] == op_challenge, + create_challenge(), + ], + [ + Txn.application_args[0] == op_accept, + accept_challenge(), + ], + [ + Txn.application_args[0] == op_reveal, + reveal(), + ], + ), + Reject(), + ), + ) + + +def clear_state_program(): + return Int(0) + + +if __name__ == "__main__": + with open("vote_approval.teal", "w") as f: + compiled = compileTeal(approval_program(), Mode.Application, version=6) + f.write(compiled) + + with open("vote_clear_state.teal", "w") as f: + compiled = compileTeal(clear_state_program(), Mode.Application, version=6) + f.write(compiled) diff --git a/tests/teal/rps.teal b/tests/teal/rps.teal new file mode 100644 index 000000000..327d93b68 --- /dev/null +++ b/tests/teal/rps.teal @@ -0,0 +1,442 @@ +#pragma version 6 +txn ApplicationID +int 0 +== +bnz main_l19 +txn OnCompletion +int DeleteApplication +== +bnz main_l18 +txn OnCompletion +int UpdateApplication +== +bnz main_l17 +txn OnCompletion +int OptIn +== +bnz main_l16 +txn OnCompletion +int CloseOut +== +bnz main_l15 +txn OnCompletion +int NoOp +== +bnz main_l7 +err +main_l7: +txna ApplicationArgs 0 +byte "challenge" +== +bnz main_l14 +txna ApplicationArgs 0 +byte "accept" +== +bnz main_l13 +txna ApplicationArgs 0 +byte "reveal" +== +bnz main_l11 +err +main_l11: +callsub reveal_8 +main_l12: +int 0 +return +main_l13: +callsub acceptchallenge_7 +b main_l12 +main_l14: +callsub createchallenge_6 +b main_l12 +main_l15: +int 0 +return +main_l16: +int 0 +callsub reset_0 +int 1 +return +main_l17: +int 0 +return +main_l18: +int 0 +return +main_l19: +int 1 +return + +// reset +reset_0: +store 0 +load 0 +byte "opponent" +byte "" +app_local_put +load 0 +byte "bet" +int 0 +app_local_put +load 0 +byte "private_play" +byte "" +app_local_put +load 0 +byte "play" +byte "" +app_local_put +retsub + +// is_account_empty +isaccountempty_1: +store 3 +load 3 +byte "opponent" +app_local_get +byte "" +== +load 3 +byte "bet" +app_local_get +int 0 +== +&& +load 3 +byte "private_play" +app_local_get +byte "" +== +&& +load 3 +byte "play" +app_local_get +byte "" +== +&& +retsub + +// is_valid_play +isvalidplay_2: +store 1 +load 1 +extract 0 1 +store 2 +load 2 +byte "r" +== +load 2 +byte "p" +== +|| +load 2 +byte "s" +== +|| +retsub + +// play_value +playvalue_3: +store 8 +load 8 +extract 0 1 +store 9 +load 9 +byte "r" +== +bnz playvalue_3_l6 +load 9 +byte "p" +== +bnz playvalue_3_l5 +load 9 +byte "s" +== +bnz playvalue_3_l4 +err +playvalue_3_l4: +int 2 +b playvalue_3_l7 +playvalue_3_l5: +int 1 +b playvalue_3_l7 +playvalue_3_l6: +int 0 +playvalue_3_l7: +retsub + +// get_winner_account_index +getwinneraccountindex_4: +store 11 +store 10 +load 10 +load 11 +== +bnz getwinneraccountindex_4_l6 +load 10 +int 1 ++ +int 3 +% +load 11 +== +bnz getwinneraccountindex_4_l5 +load 11 +int 1 ++ +int 3 +% +load 10 +== +bnz getwinneraccountindex_4_l4 +err +getwinneraccountindex_4_l4: +int 0 +b getwinneraccountindex_4_l7 +getwinneraccountindex_4_l5: +int 1 +b getwinneraccountindex_4_l7 +getwinneraccountindex_4_l6: +int 2 +getwinneraccountindex_4_l7: +retsub + +// send_reward +sendreward_5: +store 13 +store 12 +itxn_begin +int pay +itxn_field TypeEnum +load 12 +txnas Accounts +itxn_field Receiver +load 13 +itxn_field Amount +itxn_submit +retsub + +// create_challenge +createchallenge_6: +global GroupSize +int 2 +== +txn GroupIndex +int 0 +== +&& +gtxn 1 CloseRemainderTo +global ZeroAddress +== +&& +gtxn 0 RekeyTo +global ZeroAddress +== +&& +gtxn 1 RekeyTo +global ZeroAddress +== +&& +gtxn 1 TypeEnum +int pay +== +&& +gtxn 1 Receiver +global CurrentApplicationAddress +== +&& +gtxn 1 Amount +txna Accounts 1 +byte "bet" +app_local_get +== +&& +txna Accounts 1 +global CurrentApplicationID +app_opted_in +&& +txn Sender +txna Accounts 1 +byte "opponent" +app_local_get +== +&& +txn NumAppArgs +int 2 +== +&& +txna ApplicationArgs 1 +callsub isvalidplay_2 +&& +assert +txn Sender +byte "opponent" +txna Accounts 1 +app_local_put +txn Sender +byte "bet" +gtxn 1 Amount +app_local_put +txn Sender +byte "play" +txna ApplicationArgs 1 +app_local_put +int 1 +return + +// accept_challenge +acceptchallenge_7: +global GroupSize +int 2 +== +txn GroupIndex +int 0 +== +&& +gtxn 1 CloseRemainderTo +global ZeroAddress +== +&& +gtxn 0 RekeyTo +global ZeroAddress +== +&& +gtxn 1 RekeyTo +global ZeroAddress +== +&& +gtxn 1 TypeEnum +int pay +== +&& +gtxn 1 Receiver +global CurrentApplicationAddress +== +&& +txna Accounts 1 +global CurrentApplicationID +app_opted_in +&& +txn Sender +callsub isaccountempty_1 +&& +txna Accounts 1 +callsub isaccountempty_1 +&& +txn NumAppArgs +int 2 +== +&& +assert +txn Sender +byte "opponent" +txna Accounts 1 +app_local_put +txn Sender +byte "bet" +gtxn 1 Amount +app_local_put +txn Sender +byte "private_play" +txna ApplicationArgs 1 +app_local_put +int 1 +return + +// reveal +reveal_8: +global GroupSize +int 1 +== +txn GroupIndex +int 0 +== +&& +gtxn 0 RekeyTo +global ZeroAddress +== +&& +txn Sender +byte "opponent" +app_local_get +txna Accounts 1 +== +&& +txna Accounts 1 +byte "opponent" +app_local_get +txn Sender +== +&& +txn Sender +byte "bet" +app_local_get +txna Accounts 1 +byte "bet" +app_local_get +== +&& +txn Sender +byte "private_play" +app_local_get +byte "" +!= +&& +txna Accounts 1 +byte "play" +app_local_get +byte "" +!= +&& +txn NumAppArgs +int 2 +== +&& +txna ApplicationArgs 1 +sha256 +int 0 +byte "private_play" +app_local_get +== +&& +assert +txna ApplicationArgs 1 +callsub playvalue_3 +store 4 +txna Accounts 1 +byte "play" +app_local_get +callsub playvalue_3 +store 5 +txn Sender +byte "bet" +app_local_get +store 7 +load 4 +load 5 +callsub getwinneraccountindex_4 +store 6 +load 6 +int 2 +== +bnz reveal_8_l2 +load 6 +load 7 +int 2 +* +callsub sendreward_5 +b reveal_8_l3 +reveal_8_l2: +txn Sender +load 7 +callsub sendreward_5 +txna Accounts 1 +load 7 +callsub sendreward_5 +reveal_8_l3: +txn Sender +callsub reset_0 +txna Accounts 1 +callsub reset_0 +int 1 +return \ No newline at end of file diff --git a/tests/teal/rps_annotated.teal b/tests/teal/rps_annotated.teal new file mode 100644 index 000000000..711f16cda --- /dev/null +++ b/tests/teal/rps_annotated.teal @@ -0,0 +1,443 @@ +// GENERATED TEAL // PYTEAL PATH LINE PYTEAL +#pragma version 6 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +txn ApplicationID // tests/teal/rps_helpers/program.py 22 Txn.application_id() +int 0 // Int(0) +== // Txn.application_id() == Int(0) +bnz main_l19 // +txn OnCompletion // 23 Txn.on_completion() +int DeleteApplication // Txn.on_completion() == OnComplete.DeleteApplication +== // +bnz main_l18 // +txn OnCompletion // 24 Txn.on_completion() +int UpdateApplication // Txn.on_completion() == OnComplete.UpdateApplication +== // +bnz main_l17 // +txn OnCompletion // 25 Txn.on_completion() +int OptIn // Txn.on_completion() == OnComplete.OptIn +== // +bnz main_l16 // +txn OnCompletion // 26 Txn.on_completion() +int CloseOut // Txn.on_completion() == OnComplete.CloseOut +== // +bnz main_l15 // +txn OnCompletion // 27 Txn.on_completion() +int NoOp // Txn.on_completion() == OnComplete.NoOp +== // +bnz main_l7 // +err // 21 Cond([Txn.application_id() == Int(0), init], [Txn.on_completion() == OnComplete.DeleteApplication, delete], [Txn.on_completion() == OnComplete.UpdateApplication, update], [Txn.on_completion() == OnComplete.OptIn, opt_in], [Txn.on_completion() == OnComplete.CloseOut, close_out], [Txn.on_completion() == OnComplete.NoOp, no_op]) +main_l7: // tests/teal/rps.py 221 Txn.application_args[0] == op_challenge +txna ApplicationArgs 0 // Txn.application_args[0] +byte "challenge" // 17 Bytes('challenge') +== // 221 Txn.application_args[0] == op_challenge +bnz main_l14 // +txna ApplicationArgs 0 // 225 Txn.application_args[0] +byte "accept" // 18 Bytes('accept') +== // 225 Txn.application_args[0] == op_accept +bnz main_l13 // +txna ApplicationArgs 0 // 229 Txn.application_args[0] +byte "reveal" // 19 Bytes('reveal') +== // 229 Txn.application_args[0] == op_reveal +bnz main_l11 // +err // 219 Cond([Txn.application_args[0] == op_challenge, create_challenge()], [Txn.application_args[0] == op_accept, accept_challenge()], [Txn.application_args[0] == op_reveal, reveal()]) +main_l11: // 229 Txn.application_args[0] == op_reveal +callsub reveal_8 // 230 reveal() +main_l12: // 229 Txn.application_args[0] == op_reveal +int 0 // 233 Reject() +return // +main_l13: // 225 Txn.application_args[0] == op_accept +callsub acceptchallenge_7 // 226 accept_challenge() +b main_l12 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +main_l14: // tests/teal/rps.py 221 Txn.application_args[0] == op_challenge +callsub createchallenge_6 // 222 create_challenge() +b main_l12 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +main_l15: // tests/teal/rps_helpers/program.py 26 Txn.on_completion() == OnComplete.CloseOut +int 0 // 18 Reject() +return // +main_l16: // 25 Txn.on_completion() == OnComplete.OptIn +int 0 // tests/teal/rps.py 215 Int(0) +callsub reset_0 // reset(Int(0)) +int 1 // 216 Approve() +return // +main_l17: // tests/teal/rps_helpers/program.py 24 Txn.on_completion() == OnComplete.UpdateApplication +int 0 // 16 Reject() +return // +main_l18: // 23 Txn.on_completion() == OnComplete.DeleteApplication +int 0 // 15 Reject() +return // +main_l19: // 22 Txn.application_id() == Int(0) +int 1 // tests/teal/rps.py 213 Approve() +return // + // 22 def reset(account: Expr): +// reset // +reset_0: // +store 0 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +load 0 // +byte "opponent" // tests/teal/rps.py 12 Bytes('opponent') +byte "" // 24 Bytes('') +app_local_put // App.localPut(account, local_opponent, Bytes('')) +load 0 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +byte "bet" // tests/teal/rps.py 13 Bytes('bet') +int 0 // 25 Int(0) +app_local_put // App.localPut(account, local_bet, Int(0)) +load 0 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +byte "private_play" // tests/teal/rps.py 14 Bytes('private_play') +byte "" // 26 Bytes('') +app_local_put // App.localPut(account, local_private_play, Bytes('')) +load 0 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +byte "play" // tests/teal/rps.py 15 Bytes('play') +byte "" // 27 Bytes('') +app_local_put // App.localPut(account, local_play, Bytes('')) +retsub // 22 def reset(account: Expr): + // 31 def is_account_empty(account: Expr): +// is_account_empty // +isaccountempty_1: // +store 3 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +load 3 // +byte "opponent" // tests/teal/rps.py 12 Bytes('opponent') +app_local_get // 34 App.localGet(account, local_opponent) +byte "" // Bytes('') +== // App.localGet(account, local_opponent) == Bytes('') +load 3 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +byte "bet" // tests/teal/rps.py 13 Bytes('bet') +app_local_get // 35 App.localGet(account, local_bet) +int 0 // Int(0) +== // App.localGet(account, local_bet) == Int(0) +&& // 33 And(App.localGet(account, local_opponent) == Bytes(''), App.localGet(account, local_bet) == Int(0), App.localGet(account, local_private_play) == Bytes(''), App.localGet(account, local_play) == Bytes('')) +load 3 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +byte "private_play" // tests/teal/rps.py 14 Bytes('private_play') +app_local_get // 36 App.localGet(account, local_private_play) +byte "" // Bytes('') +== // App.localGet(account, local_private_play) == Bytes('') +&& // 33 And(App.localGet(account, local_opponent) == Bytes(''), App.localGet(account, local_bet) == Int(0), App.localGet(account, local_private_play) == Bytes(''), App.localGet(account, local_play) == Bytes('')) +load 3 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +byte "play" // tests/teal/rps.py 15 Bytes('play') +app_local_get // 37 App.localGet(account, local_play) +byte "" // Bytes('') +== // App.localGet(account, local_play) == Bytes('') +&& // 33 And(App.localGet(account, local_opponent) == Bytes(''), App.localGet(account, local_bet) == Int(0), App.localGet(account, local_private_play) == Bytes(''), App.localGet(account, local_play) == Bytes('')) +retsub // 31 def is_account_empty(account: Expr): + // 42 def is_valid_play(play: Expr): +// is_valid_play // +isvalidplay_2: // +store 1 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +load 1 // +extract 0 1 // tests/teal/rps.py 45 Substring(play, Int(0), Int(1)) +store 2 // first_ch.store(Substring(play, Int(0), Int(1))) +load 2 // 48 first_ch.load() +byte "r" // Bytes('r') +== // first_ch.load() == Bytes('r') +load 2 // 49 first_ch.load() +byte "p" // Bytes('p') +== // first_ch.load() == Bytes('p') +|| // 47 Or(first_ch.load() == Bytes('r'), first_ch.load() == Bytes('p'), first_ch.load() == Bytes('s')) +load 2 // 50 first_ch.load() +byte "s" // Bytes('s') +== // first_ch.load() == Bytes('s') +|| // 47 Or(first_ch.load() == Bytes('r'), first_ch.load() == Bytes('p'), first_ch.load() == Bytes('s')) +retsub // 42 def is_valid_play(play: Expr): + // 56 def play_value(p: Expr): +// play_value // +playvalue_3: // +store 8 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +load 8 // +extract 0 1 // tests/teal/rps.py 59 Substring(p, Int(0), Int(1)) +store 9 // first_letter.store(Substring(p, Int(0), Int(1))) +load 9 // 62 first_letter.load() +byte "r" // Bytes('r') +== // first_letter.load() == Bytes('r') +bnz playvalue_3_l6 // +load 9 // 63 first_letter.load() +byte "p" // Bytes('p') +== // first_letter.load() == Bytes('p') +bnz playvalue_3_l5 // +load 9 // 64 first_letter.load() +byte "s" // Bytes('s') +== // first_letter.load() == Bytes('s') +bnz playvalue_3_l4 // +err // 61 Cond([first_letter.load() == Bytes('r'), Int(0)], [first_letter.load() == Bytes('p'), Int(1)], [first_letter.load() == Bytes('s'), Int(2)]) +playvalue_3_l4: // 64 first_letter.load() == Bytes('s') +int 2 // Int(2) +b playvalue_3_l7 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +playvalue_3_l5: // tests/teal/rps.py 63 first_letter.load() == Bytes('p') +int 1 // Int(1) +b playvalue_3_l7 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +playvalue_3_l6: // tests/teal/rps.py 62 first_letter.load() == Bytes('r') +int 0 // Int(0) +playvalue_3_l7: // first_letter.load() == Bytes('r') +retsub // 56 def play_value(p: Expr): + // 70 def get_winner_account_index(challanger_play: Expr, opponent_play: Expr): +// get_winner_account_index // +getwinneraccountindex_4: // +store 11 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +store 10 // +load 10 // +load 11 // +== // tests/teal/rps.py 73 challanger_play == opponent_play +bnz getwinneraccountindex_4_l6 // +load 10 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +int 1 // tests/teal/rps.py 75 Int(1) ++ // challanger_play + Int(1) +int 3 // Int(3) +% // (challanger_play + Int(1)) % Int(3) +load 11 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +== // tests/teal/rps.py 75 (challanger_play + Int(1)) % Int(3) == opponent_play +bnz getwinneraccountindex_4_l5 // +load 11 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +int 1 // tests/teal/rps.py 79 Int(1) ++ // opponent_play + Int(1) +int 3 // Int(3) +% // (opponent_play + Int(1)) % Int(3) +load 10 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +== // tests/teal/rps.py 79 (opponent_play + Int(1)) % Int(3) == challanger_play +bnz getwinneraccountindex_4_l4 // +err // 72 Cond([challanger_play == opponent_play, Int(2)], [(challanger_play + Int(1)) % Int(3) == opponent_play, Int(1)], [(opponent_play + Int(1)) % Int(3) == challanger_play, Int(0)]) +getwinneraccountindex_4_l4: // 79 (opponent_play + Int(1)) % Int(3) == challanger_play +int 0 // 80 Int(0) +b getwinneraccountindex_4_l7 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +getwinneraccountindex_4_l5: // tests/teal/rps.py 75 (challanger_play + Int(1)) % Int(3) == opponent_play +int 1 // 76 Int(1) +b getwinneraccountindex_4_l7 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +getwinneraccountindex_4_l6: // tests/teal/rps.py 73 challanger_play == opponent_play +int 2 // Int(2) +getwinneraccountindex_4_l7: // challanger_play == opponent_play +retsub // 70 def get_winner_account_index(challanger_play: Expr, opponent_play: Expr): + // 86 def send_reward(account_index: Expr, amount: Expr): +// send_reward // +sendreward_5: // +store 13 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +store 12 // +itxn_begin // tests/teal/rps.py 88 InnerTxnBuilder.Begin() +int pay // 89 InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: Txn.accounts[account_index], TxnField.amount: amount}) +itxn_field TypeEnum // +load 12 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +txnas Accounts // tests/teal/rps.py 92 Txn.accounts[account_index] +itxn_field Receiver // 89 InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: Txn.accounts[account_index], TxnField.amount: amount}) +load 13 // tests/unit/sourcemap_rps_test.py 46 Compilation(approval_program(), Mode.Application, version=6).compile(with_sourcemap=True, annotate_teal=True, annotate_teal_headers=True, annotate_teal_concise=False) +itxn_field Amount // tests/teal/rps.py 89 InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: Txn.accounts[account_index], TxnField.amount: amount}) +itxn_submit // 96 InnerTxnBuilder.Submit() +retsub // 86 def send_reward(account_index: Expr, amount: Expr): + // 100 def create_challenge(): +// create_challenge // +createchallenge_6: // +global GroupSize // 105 Global.group_size() +int 2 // Int(2) +== // Global.group_size() == Int(2) +txn GroupIndex // 106 Txn.group_index() +int 0 // Int(0) +== // Txn.group_index() == Int(0) +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +gtxn 1 CloseRemainderTo // 108 Gtxn[1].close_remainder_to() +global ZeroAddress // Global.zero_address() +== // Gtxn[1].close_remainder_to() == Global.zero_address() +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +gtxn 0 RekeyTo // 109 Gtxn[0].rekey_to() +global ZeroAddress // Global.zero_address() +== // Gtxn[0].rekey_to() == Global.zero_address() +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +gtxn 1 RekeyTo // 110 Gtxn[1].rekey_to() +global ZeroAddress // Global.zero_address() +== // Gtxn[1].rekey_to() == Global.zero_address() +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +gtxn 1 TypeEnum // 112 Gtxn[1].type_enum() +int pay // Gtxn[1].type_enum() == TxnType.Payment +== // +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +gtxn 1 Receiver // 114 Gtxn[1].receiver() +global CurrentApplicationAddress // Global.current_application_address() +== // Gtxn[1].receiver() == Global.current_application_address() +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +gtxn 1 Amount // 116 Gtxn[1].amount() +txna Accounts 1 // Txn.accounts[1] +byte "bet" // 13 Bytes('bet') +app_local_get // 116 App.localGet(Txn.accounts[1], local_bet) +== // Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet) +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +txna Accounts 1 // 118 Txn.accounts[1] +global CurrentApplicationID // Global.current_application_id() +app_opted_in // App.optedIn(Txn.accounts[1], Global.current_application_id()) +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +txn Sender // 120 Txn.sender() +txna Accounts 1 // Txn.accounts[1] +byte "opponent" // 12 Bytes('opponent') +app_local_get // 120 App.localGet(Txn.accounts[1], local_opponent) +== // Txn.sender() == App.localGet(Txn.accounts[1], local_opponent) +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +txn NumAppArgs // 122 Txn.application_args.length() +int 2 // Int(2) +== // Txn.application_args.length() == Int(2) +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +txna ApplicationArgs 1 // 123 Txn.application_args[1] +callsub isvalidplay_2 // is_valid_play(Txn.application_args[1]) +&& // 103 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1])) +assert // 102 Assert(And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), Gtxn[1].amount() == App.localGet(Txn.accounts[1], local_bet), App.optedIn(Txn.accounts[1], Global.current_application_id()), Txn.sender() == App.localGet(Txn.accounts[1], local_opponent), Txn.application_args.length() == Int(2), is_valid_play(Txn.application_args[1]))) +txn Sender // 126 Txn.sender() +byte "opponent" // 12 Bytes('opponent') +txna Accounts 1 // 126 Txn.accounts[1] +app_local_put // App.localPut(Txn.sender(), local_opponent, Txn.accounts[1]) +txn Sender // 127 Txn.sender() +byte "bet" // 13 Bytes('bet') +gtxn 1 Amount // 127 Gtxn[1].amount() +app_local_put // App.localPut(Txn.sender(), local_bet, Gtxn[1].amount()) +txn Sender // 128 Txn.sender() +byte "play" // 15 Bytes('play') +txna ApplicationArgs 1 // 128 Txn.application_args[1] +app_local_put // App.localPut(Txn.sender(), local_play, Txn.application_args[1]) +int 1 // 129 Approve() +return // 100 def create_challenge(): + // 133 def accept_challenge(): +// accept_challenge // +acceptchallenge_7: // +global GroupSize // 138 Global.group_size() +int 2 // Int(2) +== // Global.group_size() == Int(2) +txn GroupIndex // 139 Txn.group_index() +int 0 // Int(0) +== // Txn.group_index() == Int(0) +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +gtxn 1 CloseRemainderTo // 141 Gtxn[1].close_remainder_to() +global ZeroAddress // Global.zero_address() +== // Gtxn[1].close_remainder_to() == Global.zero_address() +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +gtxn 0 RekeyTo // 142 Gtxn[0].rekey_to() +global ZeroAddress // Global.zero_address() +== // Gtxn[0].rekey_to() == Global.zero_address() +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +gtxn 1 RekeyTo // 143 Gtxn[1].rekey_to() +global ZeroAddress // Global.zero_address() +== // Gtxn[1].rekey_to() == Global.zero_address() +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +gtxn 1 TypeEnum // 145 Gtxn[1].type_enum() +int pay // Gtxn[1].type_enum() == TxnType.Payment +== // +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +gtxn 1 Receiver // 147 Gtxn[1].receiver() +global CurrentApplicationAddress // Global.current_application_address() +== // Gtxn[1].receiver() == Global.current_application_address() +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +txna Accounts 1 // 149 Txn.accounts[1] +global CurrentApplicationID // Global.current_application_id() +app_opted_in // App.optedIn(Txn.accounts[1], Global.current_application_id()) +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +txn Sender // 151 Txn.sender() +callsub isaccountempty_1 // is_account_empty(Txn.sender()) +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +txna Accounts 1 // 152 Txn.accounts[1] +callsub isaccountempty_1 // is_account_empty(Txn.accounts[1]) +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +txn NumAppArgs // 154 Txn.application_args.length() +int 2 // Int(2) +== // Txn.application_args.length() == Int(2) +&& // 136 And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2)) +assert // 135 Assert(And(Global.group_size() == Int(2), Txn.group_index() == Int(0), Gtxn[1].close_remainder_to() == Global.zero_address(), Gtxn[0].rekey_to() == Global.zero_address(), Gtxn[1].rekey_to() == Global.zero_address(), Gtxn[1].type_enum() == TxnType.Payment, Gtxn[1].receiver() == Global.current_application_address(), App.optedIn(Txn.accounts[1], Global.current_application_id()), is_account_empty(Txn.sender()), is_account_empty(Txn.accounts[1]), Txn.application_args.length() == Int(2))) +txn Sender // 157 Txn.sender() +byte "opponent" // 12 Bytes('opponent') +txna Accounts 1 // 157 Txn.accounts[1] +app_local_put // App.localPut(Txn.sender(), local_opponent, Txn.accounts[1]) +txn Sender // 158 Txn.sender() +byte "bet" // 13 Bytes('bet') +gtxn 1 Amount // 158 Gtxn[1].amount() +app_local_put // App.localPut(Txn.sender(), local_bet, Gtxn[1].amount()) +txn Sender // 159 Txn.sender() +byte "private_play" // 14 Bytes('private_play') +txna ApplicationArgs 1 // 159 Txn.application_args[1] +app_local_put // App.localPut(Txn.sender(), local_private_play, Txn.application_args[1]) +int 1 // 160 Approve() +return // 133 def accept_challenge(): + // 164 def reveal(): +// reveal // +reveal_8: // +global GroupSize // 174 Global.group_size() +int 1 // Int(1) +== // Global.group_size() == Int(1) +txn GroupIndex // 175 Txn.group_index() +int 0 // Int(0) +== // Txn.group_index() == Int(0) +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +gtxn 0 RekeyTo // 177 Gtxn[0].rekey_to() +global ZeroAddress // Global.zero_address() +== // Gtxn[0].rekey_to() == Global.zero_address() +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +txn Sender // 179 Txn.sender() +byte "opponent" // 12 Bytes('opponent') +app_local_get // 179 App.localGet(Txn.sender(), local_opponent) +txna Accounts 1 // Txn.accounts[1] +== // App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1] +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +txna Accounts 1 // 180 Txn.accounts[1] +byte "opponent" // 12 Bytes('opponent') +app_local_get // 180 App.localGet(Txn.accounts[1], local_opponent) +txn Sender // Txn.sender() +== // App.localGet(Txn.accounts[1], local_opponent) == Txn.sender() +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +txn Sender // 182 Txn.sender() +byte "bet" // 13 Bytes('bet') +app_local_get // 182 App.localGet(Txn.sender(), local_bet) +txna Accounts 1 // 183 Txn.accounts[1] +byte "bet" // 13 Bytes('bet') +app_local_get // 183 App.localGet(Txn.accounts[1], local_bet) +== // 182 App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet) +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +txn Sender // 185 Txn.sender() +byte "private_play" // 14 Bytes('private_play') +app_local_get // 185 App.localGet(Txn.sender(), local_private_play) +byte "" // Bytes('') +!= // App.localGet(Txn.sender(), local_private_play) != Bytes('') +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +txna Accounts 1 // 186 Txn.accounts[1] +byte "play" // 15 Bytes('play') +app_local_get // 186 App.localGet(Txn.accounts[1], local_play) +byte "" // Bytes('') +!= // App.localGet(Txn.accounts[1], local_play) != Bytes('') +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +txn NumAppArgs // 188 Txn.application_args.length() +int 2 // Int(2) +== // Txn.application_args.length() == Int(2) +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +txna ApplicationArgs 1 // 189 Txn.application_args[1] +sha256 // Sha256(Txn.application_args[1]) +int 0 // 190 Int(0) +byte "private_play" // 14 Bytes('private_play') +app_local_get // 190 App.localGet(Int(0), local_private_play) +== // 189 Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play) +&& // 172 And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play)) +assert // 171 Assert(And(Global.group_size() == Int(1), Txn.group_index() == Int(0), Gtxn[0].rekey_to() == Global.zero_address(), App.localGet(Txn.sender(), local_opponent) == Txn.accounts[1], App.localGet(Txn.accounts[1], local_opponent) == Txn.sender(), App.localGet(Txn.sender(), local_bet) == App.localGet(Txn.accounts[1], local_bet), App.localGet(Txn.sender(), local_private_play) != Bytes(''), App.localGet(Txn.accounts[1], local_play) != Bytes(''), Txn.application_args.length() == Int(2), Sha256(Txn.application_args[1]) == App.localGet(Int(0), local_private_play))) +txna ApplicationArgs 1 // 193 Txn.application_args[1] +callsub playvalue_3 // play_value(Txn.application_args[1]) +store 4 // challenger_play.store(play_value(Txn.application_args[1])) +txna Accounts 1 // 194 Txn.accounts[1] +byte "play" // 15 Bytes('play') +app_local_get // 194 App.localGet(Txn.accounts[1], local_play) +callsub playvalue_3 // play_value(App.localGet(Txn.accounts[1], local_play)) +store 5 // opponent_play.store(play_value(App.localGet(Txn.accounts[1], local_play))) +txn Sender // 195 Txn.sender() +byte "bet" // 13 Bytes('bet') +app_local_get // 195 App.localGet(Txn.sender(), local_bet) +store 7 // bet.store(App.localGet(Txn.sender(), local_bet)) +load 4 // 197 challenger_play.load() +load 5 // opponent_play.load() +callsub getwinneraccountindex_4 // get_winner_account_index(challenger_play.load(), opponent_play.load()) +store 6 // 196 winner_index.store(get_winner_account_index(challenger_play.load(), opponent_play.load())) +load 6 // 199 winner_index.load() +int 2 // Int(2) +== // winner_index.load() == Int(2) +bnz reveal_8_l2 // If(winner_index.load() == Int(2)) +load 6 // 206 winner_index.load() +load 7 // bet.load() +int 2 // Int(2) +* // bet.load() * Int(2) +callsub sendreward_5 // send_reward(winner_index.load(), bet.load() * Int(2)) +b reveal_8_l3 // 199 If(winner_index.load() == Int(2)) +reveal_8_l2: // +txn Sender // 202 Txn.sender() +load 7 // bet.load() +callsub sendreward_5 // send_reward(Txn.sender(), bet.load()) +txna Accounts 1 // 203 Txn.accounts[1] +load 7 // bet.load() +callsub sendreward_5 // send_reward(Txn.accounts[1], bet.load()) +reveal_8_l3: // 199 If(winner_index.load() == Int(2)) +txn Sender // 207 Txn.sender() +callsub reset_0 // reset(Txn.sender()) +txna Accounts 1 // 208 Txn.accounts[1] +callsub reset_0 // reset(Txn.accounts[1]) +int 1 // 209 Approve() +return // 164 def reveal(): \ No newline at end of file diff --git a/tests/teal/rps_helpers/__init__.py b/tests/teal/rps_helpers/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/teal/rps_helpers/program.py b/tests/teal/rps_helpers/program.py new file mode 100644 index 000000000..a6d137e86 --- /dev/null +++ b/tests/teal/rps_helpers/program.py @@ -0,0 +1,74 @@ +""" Rock Paper Scissors example taken from https://github.com/gr8h/project_rps (15 Feb 2023) +""" + +from base64 import b64decode +from dataclasses import dataclass +from typing import Dict + +from algosdk.v2client.algod import AlgodClient +from pyteal import * +from pyteal.ast import * + + +def event( + init: Expr = Reject(), + delete: Expr = Reject(), + update: Expr = Reject(), + opt_in: Expr = Reject(), + close_out: Expr = Reject(), + no_op: Expr = Reject(), +) -> Expr: + return Cond( + [Txn.application_id() == Int(0), init], + [Txn.on_completion() == OnComplete.DeleteApplication, delete], + [Txn.on_completion() == OnComplete.UpdateApplication, update], + [Txn.on_completion() == OnComplete.OptIn, opt_in], + [Txn.on_completion() == OnComplete.CloseOut, close_out], + [Txn.on_completion() == OnComplete.NoOp, no_op], + ) + + +def check_rekey_zero( + num_transactions: int, +): + return Assert( + And( + *[ + Gtxn[i].rekey_to() == Global.zero_address() + for i in range(num_transactions) + ] + ) + ) + + +def check_self( + group_size: Expr = Int(1), + group_index: Expr = Int(0), +): + return Assert( + And( + Global.group_size() == group_size, + Txn.group_index() == group_index, + ) + ) + + +def application(pyteal: Expr) -> str: + return compileTeal(pyteal, mode=Mode.Application, version=MAX_TEAL_VERSION) + + +@dataclass +class CompiledSignature: + address: str + bytecode_b64: str + teal: str + + +def signature(algod_client: AlgodClient, pyteal: Expr) -> CompiledSignature: + teal = compileTeal(pyteal, mode=Mode.Signature, version=MAX_TEAL_VERSION) + compilation_result = algod_client.compile(teal) + return CompiledSignature( + address=compilation_result["hash"], + bytecode_b64=compilation_result["result"], + teal=teal, + ) diff --git a/tests/unit/blackbox_test.py b/tests/unit/blackbox_test.py index b12b63e5f..5a5a15e65 100644 --- a/tests/unit/blackbox_test.py +++ b/tests/unit/blackbox_test.py @@ -1,3 +1,7 @@ +# TODO: add blackbox_test.py to multithreaded tests when following issue has been fixed: +# https://github.com/algorand/pyteal/issues/199 + + from itertools import product from pathlib import Path from typing import Literal, Optional, Tuple @@ -213,6 +217,7 @@ def test_abi_blackbox_pyteal( @pytest.mark.parametrize( "fn, expected_is_abi", ((utest_noop, False), (fn_0arg_uint64_ret, True)) ) +@pytest.mark.serial def test_PyTealBlackboxExecutor_is_abi( mode: pt.Mode, fn: BlackboxWrapper, expected_is_abi: bool ): @@ -237,6 +242,7 @@ def test_PyTealBlackboxExecutor_is_abi( (fn_2mixed_arg_1ret, 2), ), ) +@pytest.mark.serial def test_PyTealBlackboxExecutor_abi_argument_types( mode: pt.Mode, fn: BlackboxWrapper, expected_arg_count: int ): @@ -256,6 +262,7 @@ def test_PyTealBlackboxExecutor_abi_argument_types( (fn_2mixed_arg_1ret, True), ), ) +@pytest.mark.serial def test_PyTealBlackboxExecutor_abi_return_type( mode: pt.Mode, fn: BlackboxWrapper, expected_does_produce_type: bool ): diff --git a/tests/unit/compile_test.py b/tests/unit/compile_test.py index 427c7e4c6..aaafab772 100644 --- a/tests/unit/compile_test.py +++ b/tests/unit/compile_test.py @@ -6,12 +6,13 @@ def test_abi_algobank(): - from examples.application.abi.algobank import ( - approval_program, - clear_state_program, - contract, - ) + from pyteal.compiler.optimizer import OptimizeOptions + from examples.application.abi.algobank import router + + approval_program, clear_state_program, contract = router.compile_program( + version=6, optimize=OptimizeOptions(scratch_slots=True) + ) target_dir = Path.cwd() / "examples" / "application" / "abi" with open( diff --git a/tests/unit/sourcemap_constructs311_test.py b/tests/unit/sourcemap_constructs311_test.py new file mode 100644 index 000000000..9d4468a09 --- /dev/null +++ b/tests/unit/sourcemap_constructs311_test.py @@ -0,0 +1,2129 @@ +from configparser import ConfigParser +from copy import deepcopy +import sys +from typing import Literal +from unittest import mock + +import pytest + +from tests.unit.sourcemap_monkey_unit_test import router_static_abisubroutine + + +@pytest.fixture +def mock_ConfigParser(): + patcher = mock.patch.object(ConfigParser, "getboolean", return_value=True) + patcher.start() + yield + patcher.stop() + + +P = "#pragma version {v}" # fill the template at runtime + +C = "comp._compile_impl(with_sourcemap=True)" +R = "expr._build_impl(rci)" + +BIG_A = "pt.And(pt.Gtxn[0].rekey_to() == pt.Global.zero_address(), pt.Gtxn[1].rekey_to() == pt.Global.zero_address(), pt.Gtxn[2].rekey_to() == pt.Global.zero_address(), pt.Gtxn[3].rekey_to() == pt.Global.zero_address(), pt.Gtxn[4].rekey_to() == pt.Global.zero_address(), pt.Gtxn[0].last_valid() == pt.Gtxn[1].last_valid(), pt.Gtxn[1].last_valid() == pt.Gtxn[2].last_valid(), pt.Gtxn[2].last_valid() == pt.Gtxn[3].last_valid(), pt.Gtxn[3].last_valid() == pt.Gtxn[4].last_valid(), pt.Gtxn[0].type_enum() == pt.TxnType.AssetTransfer, pt.Gtxn[0].xfer_asset() == asset_c, pt.Gtxn[0].receiver() == receiver)" +BIG_OR = "pt.Or(pt.App.globalGet(pt.Bytes('paused')), pt.App.localGet(pt.Int(0), pt.Bytes('frozen')), pt.App.localGet(pt.Int(1), pt.Bytes('frozen')), pt.App.localGet(pt.Int(0), pt.Bytes('lock until')) >= pt.Global.latest_timestamp(), pt.App.localGet(pt.Int(1), pt.Bytes('lock until')) >= pt.Global.latest_timestamp(), pt.App.globalGet(pt.Concat(pt.Bytes('rule'), pt.Itob(pt.App.localGet(pt.Int(0), pt.Bytes('transfer group'))), pt.Itob(pt.App.localGet(pt.Int(1), pt.Bytes('transfer group'))))))" +BIG_C = "pt.Cond([pt.Txn.application_id() == pt.Int(0), foo], [pt.Txn.on_completion() == pt.OnComplete.DeleteApplication, pt.Return(is_admin)], [pt.Txn.on_completion() == pt.OnComplete.UpdateApplication, pt.Return(is_admin)], [pt.Txn.on_completion() == pt.OnComplete.CloseOut, foo], [pt.Txn.on_completion() == pt.OnComplete.OptIn, foo], [pt.Txn.application_args[0] == pt.Bytes('set admin'), foo], [pt.Txn.application_args[0] == pt.Bytes('mint'), foo], [pt.Txn.application_args[0] == pt.Bytes('transfer'), foo], [pt.Txn.accounts[4] == pt.Bytes('foo'), foo])" +BIG_W = "pt.While(i.load() < pt.Global.group_size())" +BIG_F = "pt.For(i.store(pt.Int(0)), i.load() < pt.Global.group_size(), i.store(i.load() + pt.Int(1)))" +BIG_A2 = "pt.And(pt.Int(1) - pt.Int(2), pt.Not(pt.Int(3)), pt.Int(4) ^ pt.Int(5), ~pt.Int(6), pt.BytesEq(pt.Bytes('7'), pt.Bytes('8')), pt.GetBit(pt.Int(9), pt.Int(10)), pt.SetBit(pt.Int(11), pt.Int(12), pt.Int(13)), pt.GetByte(pt.Bytes('14'), pt.Int(15)), pt.Btoi(pt.Concat(pt.BytesDiv(pt.Bytes('101'), pt.Bytes('102')), pt.BytesNot(pt.Bytes('103')), pt.BytesZero(pt.Int(10)), pt.SetBit(pt.Bytes('105'), pt.Int(106), pt.Int(107)), pt.SetByte(pt.Bytes('108'), pt.Int(109), pt.Int(110)))))" +BIG_C2 = "pt.Concat(pt.BytesDiv(pt.Bytes('101'), pt.Bytes('102')), pt.BytesNot(pt.Bytes('103')), pt.BytesZero(pt.Int(10)), pt.SetBit(pt.Bytes('105'), pt.Int(106), pt.Int(107)), pt.SetByte(pt.Bytes('108'), pt.Int(109), pt.Int(110)))" +A = "def add(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr:" +RA = "router.add_method_handler(Foo.set_foo, 'set_foo', pt.MethodConfig(no_op=pt.CallConfig.CALL), 'Foo the foo')" + + +def abi_named_tuple_example(pt): + NUM_OPTIONS = 0 + + class PollStatus(pt.abi.NamedTuple): + question: pt.abi.Field[pt.abi.String] + can_resubmit: pt.abi.Field[pt.abi.Bool] + is_open: pt.abi.Field[pt.abi.Bool] + results: pt.abi.Field[ + pt.abi.StaticArray[ + pt.abi.Tuple2[pt.abi.String, pt.abi.Uint64], Literal[NUM_OPTIONS] # type: ignore + ] + ] + + @pt.ABIReturnSubroutine + def status(*, output: PollStatus) -> pt.Expr: # type: ignore + """Get the status of this poll. + + Returns: + A tuple containing the following information, in order: the question is poll is asking, + whether the poll allows resubmission, whether the poll is open, and an array of the poll's + current results. This array contains one entry per option, and each entry is a tuple of that + option's value and the number of accounts who have voted for it. + """ + question = pt.abi.make(pt.abi.String) + can_resubmit = pt.abi.make(pt.abi.Bool) + is_open = pt.abi.make(pt.abi.Bool) + results = pt.abi.make(pt.abi.StaticArray[pt.abi.Tuple2[pt.abi.String, pt.abi.Uint64], Literal[NUM_OPTIONS]]) # type: ignore + return pt.Seq( + question.set(pt.App.globalGet(pt.Bytes("1"))), + can_resubmit.set(pt.App.globalGet(pt.Bytes("2"))), + is_open.set(pt.App.globalGet(pt.Bytes("3"))), + results.set([]), + output.set(question, can_resubmit, is_open, results), + ) + + output = PollStatus() + return pt.Seq(status().store_into(output), pt.Int(1)) # type: ignore + + +def abi_method_return_example(pt): + @pt.ABIReturnSubroutine + def abi_sum( + toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64 # type: ignore + ) -> pt.Expr: # type: ignore + 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()), + ) + ), + ) + + return 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)), # type: ignore + pt.abi.MethodReturn(res), + pt.Approve(), + ) + + +def router_method_example(pt): + on_completion_actions = pt.BareCallActions( + opt_in=pt.OnCompleteAction.call_only(pt.Log(pt.Bytes("optin call"))), + ) + router = pt.Router("questionable", on_completion_actions, clear_state=pt.Approve()) + + @router.method + def add(a: pt.abi.Uint64, b: pt.abi.Uint64, *, output: pt.abi.Uint64) -> pt.Expr: # type: ignore + return output.set(a.get() + b.get()) + + return router + + +def get_fib_example(pt): + @pt.Subroutine(pt.TealType.uint64) + def fib(n) -> pt.Expr: # type: ignore + return pt.If( + n <= pt.Int(1), + n, + fib(n - pt.Int(1)) + fib(n - pt.Int(2)), + ) + + return fib + + +def recursive_subroutine_example(pt): + fib = get_fib_example(pt) + return fib(pt.Int(10)) + + +CONSTRUCTS_LATEST_VERSION = 8 + + +def test_constructs_handles_latest_pyteal(): + import pyteal as pt + + assert CONSTRUCTS_LATEST_VERSION == pt.MAX_PROGRAM_VERSION + + +CONSTRUCTS = [ + ( # 0: Int + lambda pt: pt.Return(pt.Int(42)), + [[P, C], ["int 42", "pt.Int(42)"], ["return", "pt.Return(pt.Int(42))"]], + ), + (lambda pt: pt.Int(42), [[P, C], ["int 42", "pt.Int(42)"], ["return", C]]), + ( # 2: Bytes + lambda pt: pt.Seq(pt.Pop(pt.Bytes("hello world")), pt.Int(1)), + [ + [P, C], + ['byte "hello world"', "pt.Bytes('hello world')"], + ["pop", "pt.Pop(pt.Bytes('hello world'))"], + ["int 1", "pt.Int(1)"], + ["return", C], + ], + ), + ( # 3: * + lambda pt: pt.Int(2) * pt.Int(3), + [ + [P, C], + ["int 2", "pt.Int(2)"], + ["int 3", "pt.Int(3)"], + ["*", "pt.Int(2) * pt.Int(3)"], + ["return", C], + ], + ), + ( # 4: ^ + lambda pt: pt.Int(2) ^ pt.Int(3), + [ + [P, C], + ["int 2", "pt.Int(2)"], + ["int 3", "pt.Int(3)"], + ["^", "pt.Int(2) ^ pt.Int(3)"], + ["return", C], + ], + ), + ( # 5: +* + lambda pt: pt.Int(1) + pt.Int(2) * pt.Int(3), + [ + [P, C], + ["int 1", "pt.Int(1)"], + ["int 2", "pt.Int(2)"], + ["int 3", "pt.Int(3)"], + ["*", "pt.Int(2) * pt.Int(3)"], + ["+", "pt.Int(1) + pt.Int(2) * pt.Int(3)"], + ["return", C], + ], + ), + ( # 6: ~ + lambda pt: ~pt.Int(1), + [[P, C], ["int 1", "pt.Int(1)"], ["~", "~pt.Int(1)"], ["return", C]], + ), + ( # 7: And Or + lambda pt: pt.And( + pt.Int(1), + pt.Int(2), + pt.Or(pt.Int(3), pt.Int(4), pt.Or(pt.And(pt.Int(5), pt.Int(6)))), + ), + [ + [P, C], + ["int 1", "pt.Int(1)"], + ["int 2", "pt.Int(2)"], + [ + "&&", + "pt.And(pt.Int(1), pt.Int(2), pt.Or(pt.Int(3), pt.Int(4), pt.Or(pt.And(pt.Int(5), pt.Int(6)))))", + ], + ["int 3", "pt.Int(3)"], + ["int 4", "pt.Int(4)"], + [ + "||", + "pt.Or(pt.Int(3), pt.Int(4), pt.Or(pt.And(pt.Int(5), pt.Int(6))))", + ], + ["int 5", "pt.Int(5)"], + ["int 6", "pt.Int(6)"], + ["&&", "pt.And(pt.Int(5), pt.Int(6))"], + [ + "||", + "pt.Or(pt.Int(3), pt.Int(4), pt.Or(pt.And(pt.Int(5), pt.Int(6))))", + ], + [ + "&&", + "pt.And(pt.Int(1), pt.Int(2), pt.Or(pt.Int(3), pt.Int(4), pt.Or(pt.And(pt.Int(5), pt.Int(6)))))", + ], + ["return", C], + ], + ), + ( # 8: Btoi BytesAnd + lambda pt: pt.Btoi( + pt.BytesAnd(pt.Bytes("base16", "0xBEEF"), pt.Bytes("base16", "0x1337")) + ), + [ + [P, C], + ["byte 0xBEEF", "pt.Bytes('base16', '0xBEEF')"], + ["byte 0x1337", "pt.Bytes('base16', '0x1337')"], + [ + "b&", + "pt.BytesAnd(pt.Bytes('base16', '0xBEEF'), pt.Bytes('base16', '0x1337'))", + ], + [ + "btoi", + "pt.Btoi(pt.BytesAnd(pt.Bytes('base16', '0xBEEF'), pt.Bytes('base16', '0x1337')))", + ], + ["return", C], + ], + 4, + ), + ( # 9: Btoi BytesZero + lambda pt: pt.Btoi(pt.BytesZero(pt.Int(4))), + [ + [P, C], + ["int 4", "pt.Int(4)"], + ["bzero", "pt.BytesZero(pt.Int(4))"], + ["btoi", "pt.Btoi(pt.BytesZero(pt.Int(4)))"], + ["return", C], + ], + 4, + ), + ( # 10: BytesNot + lambda pt: pt.Btoi(pt.BytesNot(pt.Bytes("base16", "0xFF00"))), + [ + [P, C], + ["byte 0xFF00", "pt.Bytes('base16', '0xFF00')"], + ["b~", "pt.BytesNot(pt.Bytes('base16', '0xFF00'))"], + ["btoi", "pt.Btoi(pt.BytesNot(pt.Bytes('base16', '0xFF00')))"], + ["return", C], + ], + 4, + ), + ( # 11: Get/SetBit + lambda pt: pt.Seq( + pt.Pop(pt.SetBit(pt.Bytes("base16", "0x00"), pt.Int(3), pt.Int(1))), + pt.GetBit(pt.Int(16), pt.Int(64)), + ), + [ + [P, C], + ("byte 0x00", "pt.Bytes('base16', '0x00')"), + ("int 3", "pt.Int(3)"), + ("int 1", "pt.Int(1)"), + ( + "setbit", + "pt.SetBit(pt.Bytes('base16', '0x00'), pt.Int(3), pt.Int(1))", + ), + ( + "pop", + "pt.Pop(pt.SetBit(pt.Bytes('base16', '0x00'), pt.Int(3), pt.Int(1)))", + ), + ("int 16", "pt.Int(16)"), + ("int 64", "pt.Int(64)"), + ("getbit", "pt.GetBit(pt.Int(16), pt.Int(64))"), + ["return", C], + ], + 3, + ), + ( # 12: Get/SetByte + lambda pt: pt.Seq( + pt.Pop(pt.SetByte(pt.Bytes("base16", "0xff00"), pt.Int(0), pt.Int(0))), + pt.GetByte(pt.Bytes("abc"), pt.Int(2)), + ), + [ + [P, C], + ("byte 0xff00", "pt.Bytes('base16', '0xff00')"), + ("int 0", "pt.Int(0)"), + ("int 0", "pt.Int(0)"), + ( + "setbyte", + "pt.SetByte(pt.Bytes('base16', '0xff00'), pt.Int(0), pt.Int(0))", + ), + ( + "pop", + "pt.Pop(pt.SetByte(pt.Bytes('base16', '0xff00'), pt.Int(0), pt.Int(0)))", + ), + ('byte "abc"', "pt.Bytes('abc')"), + ("int 2", "pt.Int(2)"), + ("getbyte", "pt.GetByte(pt.Bytes('abc'), pt.Int(2))"), + ["return", C], + ], + 3, + ), + ( # 13: Concat + lambda pt: pt.Btoi(pt.Concat(pt.Bytes("a"), pt.Bytes("b"), pt.Bytes("c"))), + [ + [P, C], + ('byte "a"', "pt.Bytes('a')"), + ('byte "b"', "pt.Bytes('b')"), + ("concat", "pt.Concat(pt.Bytes('a'), pt.Bytes('b'), pt.Bytes('c'))"), + ('byte "c"', "pt.Bytes('c')"), + ("concat", "pt.Concat(pt.Bytes('a'), pt.Bytes('b'), pt.Bytes('c'))"), + ( + "btoi", + "pt.Btoi(pt.Concat(pt.Bytes('a'), pt.Bytes('b'), pt.Bytes('c')))", + ), + ["return", C], + ], + ), + ( # 14: Substring + lambda pt: pt.Btoi(pt.Substring(pt.Bytes("algorand"), pt.Int(2), pt.Int(8))), + [ + [P, C], + ('byte "algorand"', "pt.Bytes('algorand')"), + ( + "extract 2 6", + "pt.Substring(pt.Bytes('algorand'), pt.Int(2), pt.Int(8))", + ), + ( + "btoi", + "pt.Btoi(pt.Substring(pt.Bytes('algorand'), pt.Int(2), pt.Int(8)))", + ), + ( + "return", + C, + ), + ], + 5, + ), + ( # 15: Extract + lambda pt: pt.Btoi(pt.Extract(pt.Bytes("algorand"), pt.Int(2), pt.Int(6))), + [ + [P, C], + ('byte "algorand"', "pt.Bytes('algorand')"), + ( + "extract 2 6", + "pt.Extract(pt.Bytes('algorand'), pt.Int(2), pt.Int(6))", + ), + ( + "btoi", + "pt.Btoi(pt.Extract(pt.Bytes('algorand'), pt.Int(2), pt.Int(6)))", + ), + ["return", C], + ], + 5, + ), + ( # 16: Txn + lambda pt: pt.And( + pt.Txn.type_enum() == pt.TxnType.Payment, + pt.Txn.fee() < pt.Int(100), + pt.Txn.first_valid() % pt.Int(50) == pt.Int(0), + pt.Txn.last_valid() == pt.Int(5000) + pt.Txn.first_valid(), + pt.Txn.lease() == pt.Bytes("base64", "023sdDE2"), + ), + [ + [P, C], + ("txn TypeEnum", "pt.Txn.type_enum()"), + ("int pay", "pt.Txn.type_enum() == pt.TxnType.Payment"), + ("==", "pt.Txn.type_enum() == pt.TxnType.Payment"), + ("txn Fee", "pt.Txn.fee()"), + ("int 100", "pt.Int(100)"), + ("<", "pt.Txn.fee() < pt.Int(100)"), + ( + "&&", + "pt.And(pt.Txn.type_enum() == pt.TxnType.Payment, pt.Txn.fee() < pt.Int(100), pt.Txn.first_valid() % pt.Int(50) == pt.Int(0), pt.Txn.last_valid() == pt.Int(5000) + pt.Txn.first_valid(), pt.Txn.lease() == pt.Bytes('base64', '023sdDE2'))", + ), + ("txn FirstValid", "pt.Txn.first_valid()"), + ("int 50", "pt.Int(50)"), + ("%", "pt.Txn.first_valid() % pt.Int(50)"), + ("int 0", "pt.Int(0)"), + ("==", "pt.Txn.first_valid() % pt.Int(50) == pt.Int(0)"), + ( + "&&", + "pt.And(pt.Txn.type_enum() == pt.TxnType.Payment, pt.Txn.fee() < pt.Int(100), pt.Txn.first_valid() % pt.Int(50) == pt.Int(0), pt.Txn.last_valid() == pt.Int(5000) + pt.Txn.first_valid(), pt.Txn.lease() == pt.Bytes('base64', '023sdDE2'))", + ), + ("txn LastValid", "pt.Txn.last_valid()"), + ("int 5000", "pt.Int(5000)"), + ("txn FirstValid", "pt.Txn.first_valid()"), + ("+", "pt.Int(5000) + pt.Txn.first_valid()"), + ("==", "pt.Txn.last_valid() == pt.Int(5000) + pt.Txn.first_valid()"), + ( + "&&", + "pt.And(pt.Txn.type_enum() == pt.TxnType.Payment, pt.Txn.fee() < pt.Int(100), pt.Txn.first_valid() % pt.Int(50) == pt.Int(0), pt.Txn.last_valid() == pt.Int(5000) + pt.Txn.first_valid(), pt.Txn.lease() == pt.Bytes('base64', '023sdDE2'))", + ), + ("txn Lease", "pt.Txn.lease()"), + ("byte base64(023sdDE2)", "pt.Bytes('base64', '023sdDE2')"), + ("==", "pt.Txn.lease() == pt.Bytes('base64', '023sdDE2')"), + ( + "&&", + "pt.And(pt.Txn.type_enum() == pt.TxnType.Payment, pt.Txn.fee() < pt.Int(100), pt.Txn.first_valid() % pt.Int(50) == pt.Int(0), pt.Txn.last_valid() == pt.Int(5000) + pt.Txn.first_valid(), pt.Txn.lease() == pt.Bytes('base64', '023sdDE2'))", + ), + ["return", C], + ], + ), + ( # 17: Cond + lambda pt: [ + is_admin := pt.App.localGet(pt.Int(0), pt.Bytes("admin")), + foo := pt.Return(pt.Int(1)), + pt.Cond( + [pt.Txn.application_id() == pt.Int(0), foo], + [ + pt.Txn.on_completion() == pt.OnComplete.DeleteApplication, + pt.Return(is_admin), + ], + [ + pt.Txn.on_completion() == pt.OnComplete.UpdateApplication, + pt.Return(is_admin), + ], + [pt.Txn.on_completion() == pt.OnComplete.CloseOut, foo], + [pt.Txn.on_completion() == pt.OnComplete.OptIn, foo], + [pt.Txn.application_args[0] == pt.Bytes("set admin"), foo], + [pt.Txn.application_args[0] == pt.Bytes("mint"), foo], + [pt.Txn.application_args[0] == pt.Bytes("transfer"), foo], + [pt.Txn.accounts[4] == pt.Bytes("foo"), foo], + ), + ][-1], + [ + [P, C], + ("txn ApplicationID", "pt.Txn.application_id()"), + ("int 0", "pt.Int(0)"), + ("==", "pt.Txn.application_id() == pt.Int(0)"), + ("bnz main_l18", "pt.Txn.application_id() == pt.Int(0)"), + ("txn OnCompletion", "pt.Txn.on_completion()"), + ( + "int DeleteApplication", + "pt.Txn.on_completion() == pt.OnComplete.DeleteApplication", + ), # source inferencing at work here!!!! + ("==", "pt.Txn.on_completion() == pt.OnComplete.DeleteApplication"), + ( + "bnz main_l17", + "pt.Txn.on_completion() == pt.OnComplete.DeleteApplication", + ), + ("txn OnCompletion", "pt.Txn.on_completion()"), + ( + "int UpdateApplication", + "pt.Txn.on_completion() == pt.OnComplete.UpdateApplication", + ), # source inferencing + ("==", "pt.Txn.on_completion() == pt.OnComplete.UpdateApplication"), + ( + "bnz main_l16", + "pt.Txn.on_completion() == pt.OnComplete.UpdateApplication", + ), # yep + ("txn OnCompletion", "pt.Txn.on_completion()"), + ("int CloseOut", "pt.Txn.on_completion() == pt.OnComplete.CloseOut"), + ("==", "pt.Txn.on_completion() == pt.OnComplete.CloseOut"), + ("bnz main_l15", "pt.Txn.on_completion() == pt.OnComplete.CloseOut"), + ("txn OnCompletion", "pt.Txn.on_completion()"), + ("int OptIn", "pt.Txn.on_completion() == pt.OnComplete.OptIn"), + ("==", "pt.Txn.on_completion() == pt.OnComplete.OptIn"), + ("bnz main_l14", "pt.Txn.on_completion() == pt.OnComplete.OptIn"), + ("txna ApplicationArgs 0", "pt.Txn.application_args[0]"), + ('byte "set admin"', "pt.Bytes('set admin')"), + ("==", "pt.Txn.application_args[0] == pt.Bytes('set admin')"), + ("bnz main_l13", "pt.Txn.application_args[0] == pt.Bytes('set admin')"), + ("txna ApplicationArgs 0", "pt.Txn.application_args[0]"), + ('byte "mint"', "pt.Bytes('mint')"), + ("==", "pt.Txn.application_args[0] == pt.Bytes('mint')"), + ("bnz main_l12", "pt.Txn.application_args[0] == pt.Bytes('mint')"), + ("txna ApplicationArgs 0", "pt.Txn.application_args[0]"), + ('byte "transfer"', "pt.Bytes('transfer')"), + ("==", "pt.Txn.application_args[0] == pt.Bytes('transfer')"), + ("bnz main_l11", "pt.Txn.application_args[0] == pt.Bytes('transfer')"), + ("txna Accounts 4", "pt.Txn.accounts[4]"), + ('byte "foo"', "pt.Bytes('foo')"), + ("==", "pt.Txn.accounts[4] == pt.Bytes('foo')"), + ("bnz main_l10", "pt.Txn.accounts[4] == pt.Bytes('foo')"), + ("err", BIG_C), + ("main_l10:", "pt.Txn.accounts[4] == pt.Bytes('foo')"), + ("int 1", "pt.Int(1)"), + ("return", "pt.Return(pt.Int(1))"), + ("main_l11:", "pt.Txn.application_args[0] == pt.Bytes('transfer')"), + ("int 1", "pt.Int(1)"), + ("return", "pt.Return(pt.Int(1))"), + ("main_l12:", "pt.Txn.application_args[0] == pt.Bytes('mint')"), + ("int 1", "pt.Int(1)"), + ("return", "pt.Return(pt.Int(1))"), + ("main_l13:", "pt.Txn.application_args[0] == pt.Bytes('set admin')"), + ("int 1", "pt.Int(1)"), + ("return", "pt.Return(pt.Int(1))"), + ("main_l14:", "pt.Txn.on_completion() == pt.OnComplete.OptIn"), + ("int 1", "pt.Int(1)"), + ("return", "pt.Return(pt.Int(1))"), + ("main_l15:", "pt.Txn.on_completion() == pt.OnComplete.CloseOut"), + ("int 1", "pt.Int(1)"), + ("return", "pt.Return(pt.Int(1))"), + ("main_l16:", "pt.Txn.on_completion() == pt.OnComplete.UpdateApplication"), + ("int 0", "pt.Int(0)"), + ('byte "admin"', "pt.Bytes('admin')"), + ("app_local_get", "pt.App.localGet(pt.Int(0), pt.Bytes('admin'))"), + ("return", "pt.Return(is_admin)"), + ("main_l17:", "pt.Txn.on_completion() == pt.OnComplete.DeleteApplication"), + ("int 0", "pt.Int(0)"), + ('byte "admin"', "pt.Bytes('admin')"), + ("app_local_get", "pt.App.localGet(pt.Int(0), pt.Bytes('admin'))"), + ("return", "pt.Return(is_admin)"), + ("main_l18:", "pt.Txn.application_id() == pt.Int(0)"), + ("int 1", "pt.Int(1)"), + ("return", "pt.Return(pt.Int(1))"), + ], + 2, + "Application", + ), + ( # 18: Tmpl Gtxn TxnType + lambda pt: [ + asset_c := pt.Tmpl.Int("TMPL_ASSET_C"), + receiver := pt.Tmpl.Addr("TMPL_RECEIVER"), + pt.And( + pt.Gtxn[0].rekey_to() == pt.Global.zero_address(), + pt.Gtxn[1].rekey_to() == pt.Global.zero_address(), + pt.Gtxn[2].rekey_to() == pt.Global.zero_address(), + pt.Gtxn[3].rekey_to() == pt.Global.zero_address(), + pt.Gtxn[4].rekey_to() == pt.Global.zero_address(), + pt.Gtxn[0].last_valid() == pt.Gtxn[1].last_valid(), + pt.Gtxn[1].last_valid() == pt.Gtxn[2].last_valid(), + pt.Gtxn[2].last_valid() == pt.Gtxn[3].last_valid(), + pt.Gtxn[3].last_valid() == pt.Gtxn[4].last_valid(), + pt.Gtxn[0].type_enum() == pt.TxnType.AssetTransfer, + pt.Gtxn[0].xfer_asset() == asset_c, + pt.Gtxn[0].receiver() == receiver, + ), + ][-1], + [ + [P, C], + ("gtxn 0 RekeyTo", "pt.Gtxn[0].rekey_to()"), + ("global ZeroAddress", "pt.Global.zero_address()"), + ("==", "pt.Gtxn[0].rekey_to() == pt.Global.zero_address()"), + ("gtxn 1 RekeyTo", "pt.Gtxn[1].rekey_to()"), + ("global ZeroAddress", "pt.Global.zero_address()"), + ("==", "pt.Gtxn[1].rekey_to() == pt.Global.zero_address()"), + ("&&", BIG_A), + ("gtxn 2 RekeyTo", "pt.Gtxn[2].rekey_to()"), + ("global ZeroAddress", "pt.Global.zero_address()"), + ("==", "pt.Gtxn[2].rekey_to() == pt.Global.zero_address()"), + ("&&", BIG_A), + ("gtxn 3 RekeyTo", "pt.Gtxn[3].rekey_to()"), + ("global ZeroAddress", "pt.Global.zero_address()"), + ("==", "pt.Gtxn[3].rekey_to() == pt.Global.zero_address()"), + ("&&", BIG_A), + ("gtxn 4 RekeyTo", "pt.Gtxn[4].rekey_to()"), + ("global ZeroAddress", "pt.Global.zero_address()"), + ("==", "pt.Gtxn[4].rekey_to() == pt.Global.zero_address()"), + ("&&", BIG_A), + ("gtxn 0 LastValid", "pt.Gtxn[0].last_valid()"), + ("gtxn 1 LastValid", "pt.Gtxn[1].last_valid()"), + ("==", "pt.Gtxn[0].last_valid() == pt.Gtxn[1].last_valid()"), + ("&&", BIG_A), + ("gtxn 1 LastValid", "pt.Gtxn[1].last_valid()"), + ("gtxn 2 LastValid", "pt.Gtxn[2].last_valid()"), + ("==", "pt.Gtxn[1].last_valid() == pt.Gtxn[2].last_valid()"), + ("&&", BIG_A), + ("gtxn 2 LastValid", "pt.Gtxn[2].last_valid()"), + ("gtxn 3 LastValid", "pt.Gtxn[3].last_valid()"), + ("==", "pt.Gtxn[2].last_valid() == pt.Gtxn[3].last_valid()"), + ("&&", BIG_A), + ("gtxn 3 LastValid", "pt.Gtxn[3].last_valid()"), + ("gtxn 4 LastValid", "pt.Gtxn[4].last_valid()"), + ("==", "pt.Gtxn[3].last_valid() == pt.Gtxn[4].last_valid()"), + ("&&", BIG_A), + ("gtxn 0 TypeEnum", "pt.Gtxn[0].type_enum()"), + ( + "int axfer", + "pt.Gtxn[0].type_enum() == pt.TxnType.AssetTransfer", + ), + ("==", "pt.Gtxn[0].type_enum() == pt.TxnType.AssetTransfer"), + ("&&", BIG_A), + ("gtxn 0 XferAsset", "pt.Gtxn[0].xfer_asset()"), + ("int TMPL_ASSET_C", "pt.Tmpl.Int('TMPL_ASSET_C')"), + ("==", "pt.Gtxn[0].xfer_asset() == asset_c"), + ("&&", BIG_A), + ("gtxn 0 Receiver", "pt.Gtxn[0].receiver()"), + ("addr TMPL_RECEIVER", "pt.Tmpl.Addr('TMPL_RECEIVER')"), + ("==", "pt.Gtxn[0].receiver() == receiver"), + ("&&", BIG_A), + ["return", C], + ], + ), + ( # 19: Txn.application_args + lambda pt: pt.And( + pt.Txn.application_args.length(), # get the number of application arguments in the transaction + # as of AVM v5, PyTeal expressions can be used to dynamically index into array properties as well + pt.Btoi( + pt.Txn.application_args[pt.Txn.application_args.length() - pt.Int(1)] + ), + ), + [ + [P, C], + ("txn NumAppArgs", "pt.Txn.application_args.length()"), + ("txn NumAppArgs", "pt.Txn.application_args.length()"), + ("int 1", "pt.Int(1)"), + ("-", "pt.Txn.application_args.length() - pt.Int(1)"), + ( + "txnas ApplicationArgs", + "pt.Txn.application_args[pt.Txn.application_args.length() - pt.Int(1)]", + ), + ( + "btoi", + "pt.Btoi(pt.Txn.application_args[pt.Txn.application_args.length() - pt.Int(1)])", + ), + ( + "&&", + "pt.And(pt.Txn.application_args.length(), pt.Btoi(pt.Txn.application_args[pt.Txn.application_args.length() - pt.Int(1)]))", + ), + ["return", C], + ], + 5, + ), + ( # 20: App + lambda pt: pt.Seq( + receiver_max_balance := pt.App.localGetEx( # noqa: F841 + pt.Int(1), pt.App.id(), pt.Bytes("max balance") + ), + pt.Or( + pt.App.globalGet(pt.Bytes("paused")), + pt.App.localGet(pt.Int(0), pt.Bytes("frozen")), + pt.App.localGet(pt.Int(1), pt.Bytes("frozen")), + pt.App.localGet(pt.Int(0), pt.Bytes("lock until")) + >= pt.Global.latest_timestamp(), + pt.App.localGet(pt.Int(1), pt.Bytes("lock until")) + >= pt.Global.latest_timestamp(), + pt.App.globalGet( + pt.Concat( + pt.Bytes("rule"), + pt.Itob(pt.App.localGet(pt.Int(0), pt.Bytes("transfer group"))), + pt.Itob(pt.App.localGet(pt.Int(1), pt.Bytes("transfer group"))), + ) + ), + ), + ), + [ + [P, C], + ("int 1", "pt.Int(1)"), + ("global CurrentApplicationID", "pt.App.id()"), + ('byte "max balance"', "pt.Bytes('max balance')"), + ( + "app_local_get_ex", + "pt.App.localGetEx(pt.Int(1), pt.App.id(), pt.Bytes('max balance'))", + ), + ( + "store 1", + "pt.App.localGetEx(pt.Int(1), pt.App.id(), pt.Bytes('max balance'))", + ), + ( + "store 0", + "pt.App.localGetEx(pt.Int(1), pt.App.id(), pt.Bytes('max balance'))", + ), + ('byte "paused"', "pt.Bytes('paused')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('paused'))"), + ("int 0", "pt.Int(0)"), + ('byte "frozen"', "pt.Bytes('frozen')"), + ("app_local_get", "pt.App.localGet(pt.Int(0), pt.Bytes('frozen'))"), + ("||", BIG_OR), + ("int 1", "pt.Int(1)"), + ('byte "frozen"', "pt.Bytes('frozen')"), + ("app_local_get", "pt.App.localGet(pt.Int(1), pt.Bytes('frozen'))"), + ("||", BIG_OR), + ("int 0", "pt.Int(0)"), + ('byte "lock until"', "pt.Bytes('lock until')"), + ("app_local_get", "pt.App.localGet(pt.Int(0), pt.Bytes('lock until'))"), + ("global LatestTimestamp", "pt.Global.latest_timestamp()"), + ( + ">=", + "pt.App.localGet(pt.Int(0), pt.Bytes('lock until')) >= pt.Global.latest_timestamp()", + ), + ("||", BIG_OR), + ("int 1", "pt.Int(1)"), + ('byte "lock until"', "pt.Bytes('lock until')"), + ("app_local_get", "pt.App.localGet(pt.Int(1), pt.Bytes('lock until'))"), + ("global LatestTimestamp", "pt.Global.latest_timestamp()"), + ( + ">=", + "pt.App.localGet(pt.Int(1), pt.Bytes('lock until')) >= pt.Global.latest_timestamp()", + ), + ("||", BIG_OR), + ('byte "rule"', "pt.Bytes('rule')"), + ("int 0", "pt.Int(0)"), + ('byte "transfer group"', "pt.Bytes('transfer group')"), + ( + "app_local_get", + "pt.App.localGet(pt.Int(0), pt.Bytes('transfer group'))", + ), + ( + "itob", + "pt.Itob(pt.App.localGet(pt.Int(0), pt.Bytes('transfer group')))", + ), + ( + "concat", + "pt.Concat(pt.Bytes('rule'), pt.Itob(pt.App.localGet(pt.Int(0), pt.Bytes('transfer group'))), pt.Itob(pt.App.localGet(pt.Int(1), pt.Bytes('transfer group'))))", + ), + ("int 1", "pt.Int(1)"), + ('byte "transfer group"', "pt.Bytes('transfer group')"), + ( + "app_local_get", + "pt.App.localGet(pt.Int(1), pt.Bytes('transfer group'))", + ), + ("itob", "pt.Itob(pt.App.localGet(pt.Int(1), pt.Bytes('transfer group')))"), + ( + "concat", + "pt.Concat(pt.Bytes('rule'), pt.Itob(pt.App.localGet(pt.Int(0), pt.Bytes('transfer group'))), pt.Itob(pt.App.localGet(pt.Int(1), pt.Bytes('transfer group'))))", + ), + ( + "app_global_get", + "pt.App.globalGet(pt.Concat(pt.Bytes('rule'), pt.Itob(pt.App.localGet(pt.Int(0), pt.Bytes('transfer group'))), pt.Itob(pt.App.localGet(pt.Int(1), pt.Bytes('transfer group')))))", + ), + ("||", BIG_OR), + ["return", C], + ], + 2, + "Application", + ), + ( # 21: EcdsaCurve Sha512_256 + lambda pt: pt.EcdsaVerify( + pt.EcdsaCurve.Secp256k1, + pt.Sha512_256(pt.Bytes("testdata")), + pt.Bytes( + "base16", + "33602297203d2753372cea7794ffe1756a278cbc4907b15a0dd132c9fb82555e", + ), + pt.Bytes( + "base16", + "20f112126cf3e2eac6e8d4f97a403d21bab07b8dbb77154511bb7b07c0173195", + ), + ( + pt.Bytes( + "base16", + "d6143a58c90c06b594e4414cb788659c2805e0056b1dfceea32c03f59efec517", + ), + pt.Bytes( + "base16", + "00bd2400c479efe5ea556f37e1dc11ccb20f1e642dbfe00ca346fffeae508298", + ), + ), + ), + [ + [P, C], + ['byte "testdata"', "pt.Bytes('testdata')"], + ["sha512_256", "pt.Sha512_256(pt.Bytes('testdata'))"], + [ + "byte 0x33602297203d2753372cea7794ffe1756a278cbc4907b15a0dd132c9fb82555e", + "pt.Bytes('base16', '33602297203d2753372cea7794ffe1756a278cbc4907b15a0dd132c9fb82555e')", + ], + [ + "byte 0x20f112126cf3e2eac6e8d4f97a403d21bab07b8dbb77154511bb7b07c0173195", + "pt.Bytes('base16', '20f112126cf3e2eac6e8d4f97a403d21bab07b8dbb77154511bb7b07c0173195')", + ], + [ + "byte 0xd6143a58c90c06b594e4414cb788659c2805e0056b1dfceea32c03f59efec517", + "pt.Bytes('base16', 'd6143a58c90c06b594e4414cb788659c2805e0056b1dfceea32c03f59efec517')", + ], + [ + "byte 0x00bd2400c479efe5ea556f37e1dc11ccb20f1e642dbfe00ca346fffeae508298", + "pt.Bytes('base16', '00bd2400c479efe5ea556f37e1dc11ccb20f1e642dbfe00ca346fffeae508298')", + ], + [ + "ecdsa_verify Secp256k1", + "pt.EcdsaVerify(pt.EcdsaCurve.Secp256k1, pt.Sha512_256(pt.Bytes('testdata')), pt.Bytes('base16', '33602297203d2753372cea7794ffe1756a278cbc4907b15a0dd132c9fb82555e'), pt.Bytes('base16', '20f112126cf3e2eac6e8d4f97a403d21bab07b8dbb77154511bb7b07c0173195'), (pt.Bytes('base16', 'd6143a58c90c06b594e4414cb788659c2805e0056b1dfceea32c03f59efec517'), pt.Bytes('base16', '00bd2400c479efe5ea556f37e1dc11ccb20f1e642dbfe00ca346fffeae508298')))", + ], + ["return", C], + ], + 5, + ), + ( # 22: ScratchVar (simple Assert ) + lambda pt: [ + myvar := pt.ScratchVar( + pt.TealType.uint64 + ), # assign a scratch slot in any available slot + anotherVar := pt.ScratchVar( + pt.TealType.bytes, 4 + ), # assign this scratch slot to slot #4 + pt.Seq( + [ + myvar.store(pt.Int(5)), + anotherVar.store(pt.Bytes("hello")), + pt.Assert(myvar.load() == pt.Int(5)), + pt.Return(pt.Int(1)), + ] + ), + ][-1], + [ + [P, C], + ["int 5", "pt.Int(5)"], + ["store 0", "myvar.store(pt.Int(5))"], + ['byte "hello"', "pt.Bytes('hello')"], + ["store 4", "anotherVar.store(pt.Bytes('hello'))"], + ["load 0", "myvar.load()"], + ["int 5", "pt.Int(5)"], + ["==", "myvar.load() == pt.Int(5)"], + ["assert", "pt.Assert(myvar.load() == pt.Int(5))"], + ["int 1", "pt.Int(1)"], + ["return", "pt.Return(pt.Int(1))"], + ], + 3, + ), + ( # 23: DynamicScratchVar + lambda pt: [ + s := pt.ScratchVar(pt.TealType.uint64), + d := pt.DynamicScratchVar(pt.TealType.uint64), + pt.Seq( + d.set_index(s), + s.store(pt.Int(7)), + d.store(d.load() + pt.Int(3)), + pt.Assert(s.load() == pt.Int(10)), + pt.Int(1), + ), + ][-1], + [ + [P, C], + ["int 0", "d.set_index(s)"], + ["store 1", "d.set_index(s)"], + ["int 7", "pt.Int(7)"], + ["store 0", "s.store(pt.Int(7))"], + ["load 1", "d.store(d.load() + pt.Int(3))"], + ["load 1", "d.load()"], + ["loads", "d.load()"], + ["int 3", "pt.Int(3)"], + ["+", "d.load() + pt.Int(3)"], + ["stores", "d.store(d.load() + pt.Int(3))"], + ["load 0", "s.load()"], + ["int 10", "pt.Int(10)"], + ["==", "s.load() == pt.Int(10)"], + ["assert", "pt.Assert(s.load() == pt.Int(10))"], + ["int 1", "pt.Int(1)"], + ["return", C], + ], + 5, + ), + ( # 24: If/Then/Else ImportScratchVaplue + lambda pt: [ # App is called at transaction index 0 + greeting := pt.ScratchVar( + pt.TealType.bytes, 20 + ), # this variable will live in scratch slot 20 + app1_seq := pt.Seq( + [ + pt.If(pt.Txn.sender() == pt.App.globalGet(pt.Bytes("creator"))) + .Then(greeting.store(pt.Bytes("hi creator!"))) + .Else(greeting.store(pt.Bytes("hi user!"))), + pt.Int(1), + ] + ), + greetingFromPreviousApp := pt.ImportScratchValue( + 0, 20 + ), # loading scratch slot 20 from the transaction at index 0 + app2_seq := pt.Seq( + [ + # not shown: make sure that the transaction at index 0 is an app call to App A + pt.App.globalPut( + pt.Bytes("greeting from prev app"), greetingFromPreviousApp + ), + pt.Int(1), + ] + ), + pt.And(app1_seq, app2_seq), + ][-1], + [ + [P, C], + ["txn Sender", "pt.Txn.sender()"], + ['byte "creator"', "pt.Bytes('creator')"], + ["app_global_get", "pt.App.globalGet(pt.Bytes('creator'))"], + ["==", "pt.Txn.sender() == pt.App.globalGet(pt.Bytes('creator'))"], + [ + "bnz main_l2", + "pt.If(pt.Txn.sender() == pt.App.globalGet(pt.Bytes('creator')))", + ], + ['byte "hi user!"', "pt.Bytes('hi user!')"], + ["store 20", "greeting.store(pt.Bytes('hi user!'))"], + [ + "b main_l3", + "pt.If(pt.Txn.sender() == pt.App.globalGet(pt.Bytes('creator')))", + ], + [ + "main_l2:", + "pt.If(pt.Txn.sender() == pt.App.globalGet(pt.Bytes('creator')))", + ], + ['byte "hi creator!"', "pt.Bytes('hi creator!')"], + ["store 20", "greeting.store(pt.Bytes('hi creator!'))"], + [ + "main_l3:", + "pt.If(pt.Txn.sender() == pt.App.globalGet(pt.Bytes('creator')))", + ], # pt.If(...) etc. sent us over here + ["int 1", "pt.Int(1)"], + ['byte "greeting from prev app"', "pt.Bytes('greeting from prev app')"], + ["gload 0 20", "pt.ImportScratchValue(0, 20)"], + [ + "app_global_put", + "pt.App.globalPut(pt.Bytes('greeting from prev app'), greetingFromPreviousApp)", + ], + ["int 1", "pt.Int(1)"], + ["&&", "pt.And(app1_seq, app2_seq)"], + ["return", C], + ], + 4, + "Application", + ), + ( # 25: If/Then/ElseIf/ElseIf/Else + lambda pt: [ + arg := pt.Btoi(pt.Arg(1)), + pt.If(arg == pt.Int(0)) + .Then(pt.Reject()) + .ElseIf(arg == pt.Int(1)) + .Then(pt.Reject()) + .ElseIf(arg == pt.Int(2)) + .Then(pt.Approve()) + .Else(pt.Reject()), + ][-1], + [ + [P, C], + ("arg 1", "pt.Arg(1)"), + ("btoi", "pt.Btoi(pt.Arg(1))"), + ("int 0", "pt.Int(0)"), + ("==", "arg == pt.Int(0)"), + ("bnz main_l6", "pt.If(arg == pt.Int(0))"), + ("arg 1", "pt.Arg(1)"), + ("btoi", "pt.Btoi(pt.Arg(1))"), + ("int 1", "pt.Int(1)"), + ("==", "arg == pt.Int(1)"), + ("bnz main_l5", "arg == pt.Int(1)"), + ("arg 1", "pt.Arg(1)"), + ("btoi", "pt.Btoi(pt.Arg(1))"), + ("int 2", "pt.Int(2)"), + ("==", "arg == pt.Int(2)"), + ("bnz main_l4", "arg == pt.Int(2)"), + ("int 0", "pt.Reject()"), + ("return", "pt.Reject()"), + ("main_l4:", "arg == pt.Int(2)"), + ("int 1", "pt.Approve()"), + ("return", "pt.Approve()"), + ("main_l5:", "arg == pt.Int(1)"), + ("int 0", "pt.Reject()"), + ("return", "pt.Reject()"), + ("main_l6:", "pt.If(arg == pt.Int(0))"), + ("int 0", "pt.Reject()"), + ("return", "pt.Reject()"), + ], + 2, + "Signature", + ), + ( # 26: While/Do + lambda pt: [ + totalFees := pt.ScratchVar(pt.TealType.uint64), + i := pt.ScratchVar(pt.TealType.uint64), + pt.Seq( + i.store(pt.Int(0)), + totalFees.store(pt.Int(0)), + pt.While(i.load() < pt.Global.group_size()).Do( + totalFees.store(totalFees.load() + pt.Gtxn[i.load()].fee()), + i.store(i.load() + pt.Int(1)), + ), + pt.Approve(), + ), + ][-1], + ( + [P, C], + ("int 0", "pt.Int(0)"), + ("store 1", "i.store(pt.Int(0))"), + ("int 0", "pt.Int(0)"), + ("store 0", "totalFees.store(pt.Int(0))"), + ("main_l1:", BIG_W), # yes, this makes sense to be While(...) + ("load 1", "i.load()"), + ("global GroupSize", "pt.Global.group_size()"), + ("<", "i.load() < pt.Global.group_size()"), + ("bz main_l3", BIG_W), # yes, this as well cause we're exiting while + ("load 0", "totalFees.load()"), + ("load 1", "i.load()"), + ("gtxns Fee", "pt.Gtxn[i.load()].fee()"), + ("+", "totalFees.load() + pt.Gtxn[i.load()].fee()"), + ("store 0", "totalFees.store(totalFees.load() + pt.Gtxn[i.load()].fee())"), + ("load 1", "i.load()"), + ("int 1", "pt.Int(1)"), + ("+", "i.load() + pt.Int(1)"), + ("store 1", "i.store(i.load() + pt.Int(1))"), + ("b main_l1", BIG_W), # but the only reason for this is While(...) + ("main_l3:", BIG_W), # and this exit condition as well for the While(...) + ("int 1", "pt.Approve()"), + ("return", "pt.Approve()"), + ), + 3, + ), + ( # 27: For/Do + lambda pt: [ + totalFees := pt.ScratchVar(pt.TealType.uint64), + i := pt.ScratchVar(pt.TealType.uint64), + pt.Seq( + totalFees.store(pt.Int(0)), + pt.For( + i.store(pt.Int(0)), + i.load() < pt.Global.group_size(), + i.store(i.load() + pt.Int(1)), + ).Do(totalFees.store(totalFees.load() + pt.Gtxn[i.load()].fee())), + pt.Approve(), + ), + ][-1], + [ + [P, C], + ("int 0", "pt.Int(0)"), + ("store 0", "totalFees.store(pt.Int(0))"), + ("int 0", "pt.Int(0)"), + ("store 1", "i.store(pt.Int(0))"), + ("main_l1:", BIG_F), + ("load 1", "i.load()"), + ("global GroupSize", "pt.Global.group_size()"), + ("<", "i.load() < pt.Global.group_size()"), + ("bz main_l3", BIG_F), # .Do(...) seems a bit more appropriate here + ("load 0", "totalFees.load()"), + ("load 1", "i.load()"), + ("gtxns Fee", "pt.Gtxn[i.load()].fee()"), + ("+", "totalFees.load() + pt.Gtxn[i.load()].fee()"), + ("store 0", "totalFees.store(totalFees.load() + pt.Gtxn[i.load()].fee())"), + ("load 1", "i.load()"), + ("int 1", "pt.Int(1)"), + ("+", "i.load() + pt.Int(1)"), + ("store 1", "i.store(i.load() + pt.Int(1))"), + ("b main_l1", BIG_F), + ("main_l3:", BIG_F), + ("int 1", "pt.Approve()"), + ("return", "pt.Approve()"), + ], + 3, + ), + ( # 28: For/Do nested with If/Then/ElseIf/Then + Break/Continue + lambda pt: [ + numPayments := pt.ScratchVar(pt.TealType.uint64), + i := pt.ScratchVar(pt.TealType.uint64), + pt.Seq( + numPayments.store(pt.Int(0)), + pt.For( + i.store(pt.Int(0)), + i.load() < pt.Global.group_size(), + i.store(i.load() + pt.Int(1)), + ).Do( + pt.If(pt.Gtxn[i.load()].type_enum() != pt.TxnType.Payment) + .Then(pt.Continue()) + .ElseIf(pt.Int(42)) + .Then(pt.Break()), + numPayments.store(numPayments.load() + pt.Int(1)), + ), + pt.Approve(), + ), + ][-1], + [ + [P, C], + ("int 0", "pt.Int(0)"), + ("store 0", "numPayments.store(pt.Int(0))"), + ("int 0", "pt.Int(0)"), + ("store 1", "i.store(pt.Int(0))"), + ( + "main_l1:", + "pt.For(i.store(pt.Int(0)), i.load() < pt.Global.group_size(), i.store(i.load() + pt.Int(1)))", + ), + ("load 1", "i.load()"), + ("global GroupSize", "pt.Global.group_size()"), + ("<", "i.load() < pt.Global.group_size()"), + ( + "bz main_l6", + "pt.For(i.store(pt.Int(0)), i.load() < pt.Global.group_size(), i.store(i.load() + pt.Int(1)))", + ), + ("load 1", "i.load()"), + ("gtxns TypeEnum", "pt.Gtxn[i.load()].type_enum()"), + ("int pay", "pt.Gtxn[i.load()].type_enum() != pt.TxnType.Payment"), + ("!=", "pt.Gtxn[i.load()].type_enum() != pt.TxnType.Payment"), + ( + "bnz main_l5", + "pt.If(pt.Gtxn[i.load()].type_enum() != pt.TxnType.Payment)", # pt.Continue() is better + ), + ("int 42", "pt.Int(42)"), + ( + "bnz main_l6", + "pt.If(pt.Gtxn[i.load()].type_enum() != pt.TxnType.Payment).Then(pt.Continue()).ElseIf(pt.Int(42))", # pt.Break() would be better + ), + ("load 0", "numPayments.load()"), + ("int 1", "pt.Int(1)"), + ("+", "numPayments.load() + pt.Int(1)"), + ("store 0", "numPayments.store(numPayments.load() + pt.Int(1))"), + ( + "main_l5:", + "pt.For(i.store(pt.Int(0)), i.load() < pt.Global.group_size(), i.store(i.load() + pt.Int(1)))", + ), + ("load 1", "i.load()"), + ("int 1", "pt.Int(1)"), + ("+", "i.load() + pt.Int(1)"), + ("store 1", "i.store(i.load() + pt.Int(1))"), + ( + "b main_l1", + "pt.For(i.store(pt.Int(0)), i.load() < pt.Global.group_size(), i.store(i.load() + pt.Int(1)))", + ), + ( + "main_l6:", + "pt.For(i.store(pt.Int(0)), i.load() < pt.Global.group_size(), i.store(i.load() + pt.Int(1)))", + ), + ("int 1", "pt.Approve()"), + ("return", "pt.Approve()"), + ], + 3, + ), + ( # 29: For/Do w embedded Cond + Break/Continue + lambda pt: pt.Seq( + pt.For(pt.Pop(pt.Int(1)), pt.Int(2), pt.Pop(pt.Int(3))).Do( + pt.Seq( + pt.Cond( + [pt.Int(4), pt.Continue()], + [pt.Int(5), pt.Break()], + ), + pt.Pop(pt.Int(6)), + ) + ), + pt.Reject(), + ), + [ + [P, C], + ("int 1", "pt.Int(1)"), + ("pop", "pt.Pop(pt.Int(1))"), + ("main_l1:", "pt.For(pt.Pop(pt.Int(1)), pt.Int(2), pt.Pop(pt.Int(3)))"), + ("int 2", "pt.Int(2)"), + ("bz main_l6", "pt.For(pt.Pop(pt.Int(1)), pt.Int(2), pt.Pop(pt.Int(3)))"), + ("int 4", "pt.Int(4)"), + ( + "bnz main_l5", + "pt.Int(4)", + ), + ("int 5", "pt.Int(5)"), + ( + "bnz main_l6", + "pt.Int(5)", + ), + ("err", "pt.Cond([pt.Int(4), pt.Continue()], [pt.Int(5), pt.Break()])"), + ( + "main_l5:", + "pt.For(pt.Pop(pt.Int(1)), pt.Int(2), pt.Pop(pt.Int(3)))", # Continue would be better + ), + ("int 3", "pt.Int(3)"), + ("pop", "pt.Pop(pt.Int(3))"), + ( + "b main_l1", + "pt.For(pt.Pop(pt.Int(1)), pt.Int(2), pt.Pop(pt.Int(3)))", + ), + ("main_l6:", "pt.For(pt.Pop(pt.Int(1)), pt.Int(2), pt.Pop(pt.Int(3)))"), + ("int 0", "pt.Reject()"), + ("return", "pt.Reject()"), + ], + ), + ( # 30: While/Do w empbedded Cond + Break/Continue + lambda pt: pt.Seq( + pt.Pop(pt.Int(0)), + pt.While(pt.Int(1)).Do( + pt.Cond( + [pt.Int(2), pt.Continue()], + [pt.Int(3), pt.Break()], + [pt.Int(4), pt.Pop(pt.Int(5))], + ), + pt.Pop(pt.Int(6)), + ), + pt.Reject(), + ), + # This example shows that Continue() and Break() don't receive credit for the labelled targets + [ + [P, C], + ("int 0", "pt.Int(0)"), + ("pop", "pt.Pop(pt.Int(0))"), + ("main_l1:", "pt.While(pt.Int(1))"), + ("int 1", "pt.Int(1)"), + ( + "bz main_l7", + "pt.While(pt.Int(1))", + ), # makes sense as While determines where to branch + ("int 2", "pt.Int(2)"), + ( + "bnz main_l1", # TODO: this could be improved as Continue() ought to get credit here + "pt.Int(2)", + ), + ("int 3", "pt.Int(3)"), + ( + "bnz main_l7", # TODO: this could be improved as Break() ought to get credit here + "pt.Int(3)", + ), + ("int 4", "pt.Int(4)"), + ( + "bnz main_l6", # makes sense + "pt.Int(4)", + ), + ( + "err", # makes sense + "pt.Cond([pt.Int(2), pt.Continue()], [pt.Int(3), pt.Break()], [pt.Int(4), pt.Pop(pt.Int(5))])", + ), + ( + "main_l6:", # makes sense + "pt.While(pt.Int(1))", + ), + ("int 5", "pt.Int(5)"), + ("pop", "pt.Pop(pt.Int(5))"), + ("int 6", "pt.Int(6)"), + ("pop", "pt.Pop(pt.Int(6))"), + ( + "b main_l1", + "pt.While(pt.Int(1))", + ), + ( + "main_l7:", + "pt.While(pt.Int(1))", + ), # makes sense as this is the exit condition - but it could also have been Break() + ("int 0", "pt.Reject()"), + ("return", "pt.Reject()"), + ], + ), + ( # 31: Assert (varargs) + lambda pt: pt.Seq( + foo := pt.App.globalGetEx(pt.Txn.applications[0], pt.Bytes("flo")), + pt.Assert( + pt.Txn.sender() == pt.App.globalGet(pt.Bytes("alice")), + pt.App.globalGet(pt.Bytes("bob")) != pt.Int(0), + pt.App.globalGet(pt.Bytes("chiro")) == pt.Bytes("danillo"), + pt.Global.latest_timestamp() > pt.App.globalGet(pt.Bytes("enya")), + foo.hasValue(), + ), + pt.Int(42), + ), + [ + [P, C], + ("txna Applications 0", "pt.Txn.applications[0]"), + ('byte "flo"', "pt.Bytes('flo')"), + ( + "app_global_get_ex", + "pt.App.globalGetEx(pt.Txn.applications[0], pt.Bytes('flo'))", + ), + ("store 1", "pt.App.globalGetEx(pt.Txn.applications[0], pt.Bytes('flo'))"), + ("store 0", "pt.App.globalGetEx(pt.Txn.applications[0], pt.Bytes('flo'))"), + ("txn Sender", "pt.Txn.sender()"), + ('byte "alice"', "pt.Bytes('alice')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('alice'))"), + ("==", "pt.Txn.sender() == pt.App.globalGet(pt.Bytes('alice'))"), + ("assert", "pt.Txn.sender() == pt.App.globalGet(pt.Bytes('alice'))"), + ('byte "bob"', "pt.Bytes('bob')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('bob'))"), + ("int 0", "pt.Int(0)"), + ("!=", "pt.App.globalGet(pt.Bytes('bob')) != pt.Int(0)"), + ("assert", "pt.App.globalGet(pt.Bytes('bob')) != pt.Int(0)"), + ('byte "chiro"', "pt.Bytes('chiro')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('chiro'))"), + ('byte "danillo"', "pt.Bytes('danillo')"), + ("==", "pt.App.globalGet(pt.Bytes('chiro')) == pt.Bytes('danillo')"), + ("assert", "pt.App.globalGet(pt.Bytes('chiro')) == pt.Bytes('danillo')"), + ("global LatestTimestamp", "pt.Global.latest_timestamp()"), + ('byte "enya"', "pt.Bytes('enya')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('enya'))"), + (">", "pt.Global.latest_timestamp() > pt.App.globalGet(pt.Bytes('enya'))"), + ( + "assert", + "pt.Global.latest_timestamp() > pt.App.globalGet(pt.Bytes('enya'))", + ), + ("load 1", "foo.hasValue()"), + ("assert", "foo.hasValue()"), + ("int 42", "pt.Int(42)"), + ["return", C], + ], + 3, + "Application", + ), + ( # 32 - arithmetic + lambda pt: pt.And( + pt.Int(1) - pt.Int(2), + pt.Not(pt.Int(3)), + pt.Int(4) ^ pt.Int(5), + ~pt.Int(6), + pt.BytesEq(pt.Bytes("7"), pt.Bytes("8")), + pt.GetBit(pt.Int(9), pt.Int(10)), + pt.SetBit(pt.Int(11), pt.Int(12), pt.Int(13)), + pt.GetByte(pt.Bytes("14"), pt.Int(15)), + pt.Btoi( + pt.Concat( + pt.BytesDiv(pt.Bytes("101"), pt.Bytes("102")), + pt.BytesNot(pt.Bytes("103")), + pt.BytesZero(pt.Int(10)), + pt.SetBit(pt.Bytes("105"), pt.Int(106), pt.Int(107)), + pt.SetByte(pt.Bytes("108"), pt.Int(109), pt.Int(110)), + ) + ), + ), + [ + [P, C], + ("int 1", "pt.Int(1)"), + ("int 2", "pt.Int(2)"), + ("-", "pt.Int(1) - pt.Int(2)"), + ("int 3", "pt.Int(3)"), + ("!", "pt.Not(pt.Int(3))"), + ("&&", BIG_A2), + ("int 4", "pt.Int(4)"), + ("int 5", "pt.Int(5)"), + ("^", "pt.Int(4) ^ pt.Int(5)"), + ("&&", BIG_A2), + ("int 6", "pt.Int(6)"), + ("~", "~pt.Int(6)"), + ("&&", BIG_A2), + ('byte "7"', "pt.Bytes('7')"), + ('byte "8"', "pt.Bytes('8')"), + ("b==", "pt.BytesEq(pt.Bytes('7'), pt.Bytes('8'))"), + ("&&", BIG_A2), + ("int 9", "pt.Int(9)"), + ("int 10", "pt.Int(10)"), + ("getbit", "pt.GetBit(pt.Int(9), pt.Int(10))"), + ("&&", BIG_A2), + ("int 11", "pt.Int(11)"), + ("int 12", "pt.Int(12)"), + ("int 13", "pt.Int(13)"), + ("setbit", "pt.SetBit(pt.Int(11), pt.Int(12), pt.Int(13))"), + ("&&", BIG_A2), + ('byte "14"', "pt.Bytes('14')"), + ("int 15", "pt.Int(15)"), + ("getbyte", "pt.GetByte(pt.Bytes('14'), pt.Int(15))"), + ("&&", BIG_A2), + ('byte "101"', "pt.Bytes('101')"), + ('byte "102"', "pt.Bytes('102')"), + ("b/", "pt.BytesDiv(pt.Bytes('101'), pt.Bytes('102'))"), + ('byte "103"', "pt.Bytes('103')"), + ("b~", "pt.BytesNot(pt.Bytes('103'))"), + ("concat", BIG_C2), + ("int 10", "pt.Int(10)"), + ("bzero", "pt.BytesZero(pt.Int(10))"), + ("concat", BIG_C2), + ('byte "105"', "pt.Bytes('105')"), + ("int 106", "pt.Int(106)"), + ("int 107", "pt.Int(107)"), + ("setbit", "pt.SetBit(pt.Bytes('105'), pt.Int(106), pt.Int(107))"), + ("concat", BIG_C2), + ('byte "108"', "pt.Bytes('108')"), + ("int 109", "pt.Int(109)"), + ("int 110", "pt.Int(110)"), + ("setbyte", "pt.SetByte(pt.Bytes('108'), pt.Int(109), pt.Int(110))"), + ("concat", BIG_C2), + ("btoi", f"pt.Btoi({BIG_C2})"), + ("&&", BIG_A2), + ["return", C], + ], + 4, + ), + ( # 33 - ABI Subroutine + NamedTuple (scratch slots) + abi_named_tuple_example, + [ + [P, C], + ("callsub status_0", "status()"), + ("store 0", "status().store_into(output)"), + ("int 1", "pt.Int(1)"), + ["return", C], + ("", "def status(*, output: PollStatus) -> pt.Expr:"), + ("// status", "def status(*, output: PollStatus) -> pt.Expr:"), + ("status_0:", "def status(*, output: PollStatus) -> pt.Expr:"), + ('byte "1"', "pt.Bytes('1')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('1'))"), + ("store 2", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("load 2", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("len", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("itob", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("extract 6 0", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("load 2", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("concat", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("store 2", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ('byte "2"', "pt.Bytes('2')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('2'))"), + ("!", "can_resubmit.set(pt.App.globalGet(pt.Bytes('2')))"), + ("!", "can_resubmit.set(pt.App.globalGet(pt.Bytes('2')))"), + ("store 3", "can_resubmit.set(pt.App.globalGet(pt.Bytes('2')))"), + ('byte "3"', "pt.Bytes('3')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('3'))"), + ("!", "is_open.set(pt.App.globalGet(pt.Bytes('3')))"), + ("!", "is_open.set(pt.App.globalGet(pt.Bytes('3')))"), + ("store 4", "is_open.set(pt.App.globalGet(pt.Bytes('3')))"), + ('byte ""', "results.set([])"), + ("store 5", "results.set([])"), + ("load 2", "output.set(question, can_resubmit, is_open, results)"), + ("store 9", "output.set(question, can_resubmit, is_open, results)"), + ("load 9", "output.set(question, can_resubmit, is_open, results)"), + ("store 8", "output.set(question, can_resubmit, is_open, results)"), + ("int 5", "output.set(question, can_resubmit, is_open, results)"), + ("store 6", "output.set(question, can_resubmit, is_open, results)"), + ("load 6", "output.set(question, can_resubmit, is_open, results)"), + ("load 9", "output.set(question, can_resubmit, is_open, results)"), + ("len", "output.set(question, can_resubmit, is_open, results)"), + ("+", "output.set(question, can_resubmit, is_open, results)"), + ("store 7", "output.set(question, can_resubmit, is_open, results)"), + ("load 7", "output.set(question, can_resubmit, is_open, results)"), + ("int 65536", "output.set(question, can_resubmit, is_open, results)"), + ("<", "output.set(question, can_resubmit, is_open, results)"), + ("assert", "output.set(question, can_resubmit, is_open, results)"), + ("load 6", "output.set(question, can_resubmit, is_open, results)"), + ("itob", "output.set(question, can_resubmit, is_open, results)"), + ("extract 6 0", "output.set(question, can_resubmit, is_open, results)"), + ("byte 0x00", "output.set(question, can_resubmit, is_open, results)"), + ("int 0", "output.set(question, can_resubmit, is_open, results)"), + ("load 3", "output.set(question, can_resubmit, is_open, results)"), + ("setbit", "output.set(question, can_resubmit, is_open, results)"), + ("int 1", "output.set(question, can_resubmit, is_open, results)"), + ("load 4", "output.set(question, can_resubmit, is_open, results)"), + ("setbit", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("load 5", "output.set(question, can_resubmit, is_open, results)"), + ("store 9", "output.set(question, can_resubmit, is_open, results)"), + ("load 8", "output.set(question, can_resubmit, is_open, results)"), + ("load 9", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("store 8", "output.set(question, can_resubmit, is_open, results)"), + ("load 7", "output.set(question, can_resubmit, is_open, results)"), + ("store 6", "output.set(question, can_resubmit, is_open, results)"), + ("load 6", "output.set(question, can_resubmit, is_open, results)"), + ("itob", "output.set(question, can_resubmit, is_open, results)"), + ("extract 6 0", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("load 8", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("store 1", "output.set(question, can_resubmit, is_open, results)"), + ("load 1", "status().store_into(output)"), + ("retsub", "def status(*, output: PollStatus) -> pt.Expr:"), + ], + 5, + "Application", + dict(frame_pointers=False), + ), + ( # 34 - ABI Subroutine + NamedTuple (frame pointers) + abi_named_tuple_example, + [ + [P, C], + ("callsub status_0", "status()"), + ("store 0", "status().store_into(output)"), + ("int 1", "pt.Int(1)"), + ["return", C], + ("", "def status(*, output: PollStatus) -> pt.Expr:"), + ("// status", "def status(*, output: PollStatus) -> pt.Expr:"), + ("status_0:", "def status(*, output: PollStatus) -> pt.Expr:"), + ("proto 0 1", "def status(*, output: PollStatus) -> pt.Expr:"), + ('byte ""', "def status(*, output: PollStatus) -> pt.Expr:"), + ("dup", "def status(*, output: PollStatus) -> pt.Expr:"), + ("int 0", "def status(*, output: PollStatus) -> pt.Expr:"), + ("dup", "def status(*, output: PollStatus) -> pt.Expr:"), + ('byte ""', "def status(*, output: PollStatus) -> pt.Expr:"), + ("int 0", "def status(*, output: PollStatus) -> pt.Expr:"), + ("dup", "def status(*, output: PollStatus) -> pt.Expr:"), + ('byte ""', "def status(*, output: PollStatus) -> pt.Expr:"), + ("dup", "def status(*, output: PollStatus) -> pt.Expr:"), + ("int 0", "def status(*, output: PollStatus) -> pt.Expr:"), + ("dup", "def status(*, output: PollStatus) -> pt.Expr:"), + ('byte ""', "def status(*, output: PollStatus) -> pt.Expr:"), + ("dup", "def status(*, output: PollStatus) -> pt.Expr:"), + ('byte "1"', "pt.Bytes('1')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('1'))"), + ("frame_bury 1", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("frame_dig 1", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("len", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("itob", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("extract 6 0", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("frame_dig 1", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("concat", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ("frame_bury 1", "question.set(pt.App.globalGet(pt.Bytes('1')))"), + ('byte "2"', "pt.Bytes('2')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('2'))"), + ("!", "can_resubmit.set(pt.App.globalGet(pt.Bytes('2')))"), + ("!", "can_resubmit.set(pt.App.globalGet(pt.Bytes('2')))"), + ("frame_bury 2", "can_resubmit.set(pt.App.globalGet(pt.Bytes('2')))"), + ('byte "3"', "pt.Bytes('3')"), + ("app_global_get", "pt.App.globalGet(pt.Bytes('3'))"), + ("!", "is_open.set(pt.App.globalGet(pt.Bytes('3')))"), + ("!", "is_open.set(pt.App.globalGet(pt.Bytes('3')))"), + ("frame_bury 3", "is_open.set(pt.App.globalGet(pt.Bytes('3')))"), + ('byte ""', "results.set([])"), + ("frame_bury 4", "results.set([])"), + ("frame_dig 1", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 12", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 12", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 11", "output.set(question, can_resubmit, is_open, results)"), + ("int 5", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 9", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 9", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 12", "output.set(question, can_resubmit, is_open, results)"), + ("len", "output.set(question, can_resubmit, is_open, results)"), + ("+", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 10", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 10", "output.set(question, can_resubmit, is_open, results)"), + ("int 65536", "output.set(question, can_resubmit, is_open, results)"), + ("<", "output.set(question, can_resubmit, is_open, results)"), + ("assert", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 9", "output.set(question, can_resubmit, is_open, results)"), + ("itob", "output.set(question, can_resubmit, is_open, results)"), + ("extract 6 0", "output.set(question, can_resubmit, is_open, results)"), + ("byte 0x00", "output.set(question, can_resubmit, is_open, results)"), + ("int 0", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 2", "output.set(question, can_resubmit, is_open, results)"), + ("setbit", "output.set(question, can_resubmit, is_open, results)"), + ("int 1", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 3", "output.set(question, can_resubmit, is_open, results)"), + ("setbit", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 4", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 12", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 11", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 12", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 11", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 10", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 9", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 9", "output.set(question, can_resubmit, is_open, results)"), + ("itob", "output.set(question, can_resubmit, is_open, results)"), + ("extract 6 0", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("frame_dig 11", "output.set(question, can_resubmit, is_open, results)"), + ("concat", "output.set(question, can_resubmit, is_open, results)"), + ("frame_bury 0", "output.set(question, can_resubmit, is_open, results)"), + ("retsub", "def status(*, output: PollStatus) -> pt.Expr:"), + ], + 8, + "Application", + dict(frame_pointers=True), + ), + ( # 35 - ABI Subroutine + MethodReturn (scratch slots) + abi_method_return_example, + [ + [P, C], + ("txna ApplicationArgs 1", "pt.Txn.application_args[1]"), + ( + "store 0", + "(to_sum_arr := pt.abi.make(pt.abi.DynamicArray[pt.abi.Uint64])).decode(pt.Txn.application_args[1])", + ), + ( + "load 0", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ("callsub abisum_0", "abi_sum(to_sum_arr)"), + ("store 1", "(res := pt.abi.Uint64()).set(abi_sum(to_sum_arr))"), + ("byte 0x151f7c75", "pt.abi.MethodReturn(res)"), + ("load 1", "pt.abi.MethodReturn(res)"), + ("itob", "pt.abi.MethodReturn(res)"), + ("concat", "pt.abi.MethodReturn(res)"), + ("log", "pt.abi.MethodReturn(res)"), + ("int 1", "pt.Approve()"), + ("return", "pt.Approve()"), + ( + "", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ( + "// abi_sum", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ( + "abisum_0:", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ("store 2", "(res := pt.abi.Uint64()).set(abi_sum(to_sum_arr))"), + ("int 0", "output.set(0)"), + ("store 3", "output.set(0)"), + ("int 0", "pt.Int(0)"), + ("store 4", "i.store(pt.Int(0))"), + ( + "abisum_0_l1:", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ("load 4", "i.load()"), + ("load 2", "toSum.length()"), + ("int 0", "toSum.length()"), + ("extract_uint16", "toSum.length()"), + ("store 6", "toSum.length()"), + ("load 6", "toSum.length()"), + ("<", "i.load() < toSum.length()"), + ( + "bz abisum_0_l3", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ("load 2", "toSum[i.load()].store_into(valueAtIndex)"), + ("int 8", "toSum[i.load()].store_into(valueAtIndex)"), + ("load 4", "i.load()"), + ("*", "toSum[i.load()].store_into(valueAtIndex)"), + ("int 2", "toSum[i.load()].store_into(valueAtIndex)"), + ("+", "toSum[i.load()].store_into(valueAtIndex)"), + ("extract_uint64", "toSum[i.load()].store_into(valueAtIndex)"), + ("store 5", "toSum[i.load()].store_into(valueAtIndex)"), + ("load 3", "output.get()"), + ("load 5", "valueAtIndex.get()"), + ("+", "output.get() + valueAtIndex.get()"), + ("store 3", "output.set(output.get() + valueAtIndex.get())"), + ("load 4", "i.load()"), + ("int 1", "pt.Int(1)"), + ("+", "i.load() + pt.Int(1)"), + ("store 4", "i.store(i.load() + pt.Int(1))"), + ( + "b abisum_0_l1", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ( + "abisum_0_l3:", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ("load 3", "(res := pt.abi.Uint64()).set(abi_sum(to_sum_arr))"), + ( + "retsub", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ], + 5, + "Application", + dict(frame_pointers=False), + ), + ( # 36 - ABI Subroutine + MethodReturn (frame pointers) + abi_method_return_example, + [ + [P, C], + ("txna ApplicationArgs 1", "pt.Txn.application_args[1]"), + ( + "store 0", + "(to_sum_arr := pt.abi.make(pt.abi.DynamicArray[pt.abi.Uint64])).decode(pt.Txn.application_args[1])", + ), + ( + "load 0", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ("callsub abisum_0", "abi_sum(to_sum_arr)"), + ("store 1", "(res := pt.abi.Uint64()).set(abi_sum(to_sum_arr))"), + ("byte 0x151f7c75", "pt.abi.MethodReturn(res)"), + ("load 1", "pt.abi.MethodReturn(res)"), + ("itob", "pt.abi.MethodReturn(res)"), + ("concat", "pt.abi.MethodReturn(res)"), + ("log", "pt.abi.MethodReturn(res)"), + ("int 1", "pt.Approve()"), + ("return", "pt.Approve()"), + ( + "", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ( + "// abi_sum", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ( + "abisum_0:", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ( + "proto 1 1", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ( + "int 0", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ( + "dupn 3", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ("int 0", "output.set(0)"), + ("frame_bury 0", "output.set(0)"), + ("int 0", "pt.Int(0)"), + ("store 2", "i.store(pt.Int(0))"), + ( + "abisum_0_l1:", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ("load 2", "i.load()"), + ("frame_dig -1", "toSum.length()"), + ("int 0", "toSum.length()"), + ("extract_uint16", "toSum.length()"), + ("frame_bury 2", "toSum.length()"), + ("frame_dig 2", "toSum.length()"), + ("<", "i.load() < toSum.length()"), + ( + "bz abisum_0_l3", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ("frame_dig -1", "toSum[i.load()].store_into(valueAtIndex)"), + ("int 8", "toSum[i.load()].store_into(valueAtIndex)"), + ("load 2", "i.load()"), + ("*", "toSum[i.load()].store_into(valueAtIndex)"), + ("int 2", "toSum[i.load()].store_into(valueAtIndex)"), + ("+", "toSum[i.load()].store_into(valueAtIndex)"), + ("extract_uint64", "toSum[i.load()].store_into(valueAtIndex)"), + ("frame_bury 1", "toSum[i.load()].store_into(valueAtIndex)"), + ("frame_dig 0", "output.get()"), + ("frame_dig 1", "valueAtIndex.get()"), + ("+", "output.get() + valueAtIndex.get()"), + ("frame_bury 0", "output.set(output.get() + valueAtIndex.get())"), + ("load 2", "i.load()"), + ("int 1", "pt.Int(1)"), + ("+", "i.load() + pt.Int(1)"), + ("store 2", "i.store(i.load() + pt.Int(1))"), + ( + "b abisum_0_l1", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ( + "abisum_0_l3:", + "pt.For(i.store(pt.Int(0)), i.load() < toSum.length(), i.store(i.load() + pt.Int(1)))", + ), + ( + "retsub", + "def abi_sum(toSum: pt.abi.DynamicArray[pt.abi.Uint64], *, output: pt.abi.Uint64) -> pt.Expr:", + ), + ], + 8, + "Application", + dict(frame_pointers=True), + ), + ( # 37 - Router (scratch slots) + router_method_example, + [ + [P, R], + ("txn NumAppArgs", R), + ("int 0", R), + ("==", R), + ("bnz main_l4", R), + ("txna ApplicationArgs 0", R), + ('method "add(uint64,uint64)uint64"', R), + ("==", R), + ("bnz main_l3", R), + ("err", R), + ("main_l3:", R), + ("txn OnCompletion", A), + ("int NoOp", A), + ("==", A), + ("txn ApplicationID", A), + ("int 0", A), + ("!=", A), + ("&&", A), + ("assert", A), + ("txna ApplicationArgs 1", R), + ("btoi", R), + ("store 0", A), + ("txna ApplicationArgs 2", R), + ("btoi", R), + ("store 1", A), + ("load 0", A), + ("load 1", A), + ("callsub add_0", R), + ("store 2", A), + ("byte 0x151f7c75", A), + ("load 2", A), + ("itob", A), + ("concat", A), + ("log", A), + ("int 1", R), + ("return", A), + ("main_l4:", R), + ("txn OnCompletion", R), + ("int OptIn", R), + ("==", R), + ("bnz main_l6", R), + ("err", R), + ("main_l6:", R), + ("txn ApplicationID", R), + ("int 0", R), + ("!=", R), + ("assert", R), + ('byte "optin call"', "pt.Bytes('optin call')"), + ("log", "pt.Log(pt.Bytes('optin call'))"), + ("int 1", R), + ("return", R), + ("", A), + ("// add", A), + ("add_0:", A), + ("store 4", R), + ("store 3", R), + ("load 3", "a.get()"), + ("load 4", "b.get()"), + ("+", "a.get() + b.get()"), + ("store 5", "output.set(a.get() + b.get())"), + ("load 5", R), + ("retsub", A), + ], + 5, + "Application", + dict(frame_pointers=False), + ), + ( # 38 - Router (frame pointers) + router_method_example, + [ + [P, R], + ("txn NumAppArgs", R), + ("int 0", R), + ("==", R), + ("bnz main_l4", R), + ("txna ApplicationArgs 0", R), + ('method "add(uint64,uint64)uint64"', R), + ("==", R), + ("bnz main_l3", R), + ("err", R), + ("main_l3:", R), + ("txn OnCompletion", A), + ("int NoOp", A), + ("==", A), + ("txn ApplicationID", A), + ("int 0", A), + ("!=", A), + ("&&", A), + ("assert", A), + ("callsub addcaster_1", A), + ("int 1", R), + ("return", A), + ("main_l4:", R), + ("txn OnCompletion", R), + ("int OptIn", R), + ("==", R), + ("bnz main_l6", R), + ("err", R), + ("main_l6:", R), + ("txn ApplicationID", R), + ("int 0", R), + ("!=", R), + ("assert", R), + ('byte "optin call"', "pt.Bytes('optin call')"), + ("log", "pt.Log(pt.Bytes('optin call'))"), + ("int 1", R), + ("return", R), + ("", A), + ("// add", A), + ("add_0:", A), + ("proto 2 1", A), + ("int 0", A), + ("frame_dig -2", "a.get()"), + ("frame_dig -1", "b.get()"), + ("+", "a.get() + b.get()"), + ("frame_bury 0", "output.set(a.get() + b.get())"), + ("retsub", A), + ("", R), + ("// add_caster", R), + ("addcaster_1:", R), + ("proto 0 0", R), + ("int 0", R), + ("dupn 2", R), + ("txna ApplicationArgs 1", R), + ("btoi", R), + ("frame_bury 1", R), + ("txna ApplicationArgs 2", R), + ("btoi", R), + ("frame_bury 2", R), + ("frame_dig 1", A), + ("frame_dig 2", A), + ("callsub add_0", R), + ("frame_bury 0", R), + ("byte 0x151f7c75", R), + ("frame_dig 0", R), + ("itob", R), + ("concat", R), + ("log", R), + ("retsub", R), + ], + 8, + "Application", + dict(frame_pointers=True), + ), + ( # 39 - Router static subroutine (scratch slots) + router_static_abisubroutine, + [ + [P, R], + ("txna ApplicationArgs 0", R), + ('method "set_foo(uint64)void"', R), + ("==", R), + ("bnz main_l2", R), + ("err", R), + ("main_l2:", R), + ("txn OnCompletion", RA), + ("int NoOp", RA), + ("==", RA), + ("txn ApplicationID", RA), + ("int 0", RA), + ("!=", RA), + ("&&", RA), + ("assert", RA), + ("txna ApplicationArgs 1", R), + ("btoi", R), + ("store 1", RA), + ("load 1", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("callsub setfoo_0", RA), + ("int 1", R), + ("return", RA), + ("", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("// set_foo", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("setfoo_0:", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("store 2", R), + ("load 2", "Foo.app_id.set(_app_id)"), + ("store 0", "Foo.app_id.set(_app_id)"), + ("retsub", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ], + 5, + "Application", + dict(frame_pointers=False), + ), + ( # 40 - Router static subroutine (frame pointers) + router_static_abisubroutine, + [ + [P, R], + ("txna ApplicationArgs 0", R), + ('method "set_foo(uint64)void"', R), + ("==", R), + ("bnz main_l2", R), + ("err", R), + ("main_l2:", R), + ("txn OnCompletion", RA), + ("int NoOp", RA), + ("==", RA), + ("txn ApplicationID", RA), + ("int 0", RA), + ("!=", RA), + ("&&", RA), + ("assert", RA), + ("callsub setfoocaster_1", RA), + ("int 1", R), + ("return", RA), + ("", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("// set_foo", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("setfoo_0:", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("proto 1 0", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("frame_dig -1", "Foo.app_id.set(_app_id)"), + ("store 0", "Foo.app_id.set(_app_id)"), + ("retsub", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("", R), + ("// set_foo_caster", R), + ("setfoocaster_1:", R), + ("proto 0 0", R), + ("int 0", R), + ("txna ApplicationArgs 1", R), + ("btoi", R), + ("frame_bury 0", R), + ("frame_dig 0", "def set_foo(_app_id: AppId) -> pt.Expr:"), + ("callsub setfoo_0", R), + ("retsub", R), + ], + 8, + "Application", + dict(frame_pointers=True), + ), + ( # 41 - Recursive Subroutine (scratch slots) + recursive_subroutine_example, + [ + [P, C], + ("int 10", "pt.Int(10)"), + ("callsub fib_0", "fib(pt.Int(10))"), + ("return", C), + ("", "def fib(n) -> pt.Expr:"), + ("// fib", "def fib(n) -> pt.Expr:"), + ("fib_0:", "def fib(n) -> pt.Expr:"), + ("store 0", C), + ("load 0", C), + ("int 1", "pt.Int(1)"), + ("<=", "n <= pt.Int(1)"), + ( + "bnz fib_0_l2", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ("load 0", C), + ("int 1", "pt.Int(1)"), + ("-", "n - pt.Int(1)"), + ("load 0", C), + ("swap", C), + ("callsub fib_0", "fib(n - pt.Int(1))"), + ("swap", C), + ("store 0", C), + ("load 0", C), + ("int 2", "pt.Int(2)"), + ("-", "n - pt.Int(2)"), + ("load 0", C), + ("swap", C), + ("callsub fib_0", "fib(n - pt.Int(2))"), + ("swap", C), + ("store 0", C), + ("+", "fib(n - pt.Int(1)) + fib(n - pt.Int(2))"), + ( + "b fib_0_l3", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ( + "fib_0_l2:", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ("load 0", C), + ( + "fib_0_l3:", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ("retsub", "def fib(n) -> pt.Expr:"), + ], + 5, + "Application", + dict(frame_pointers=False), + ), + ( # 42 - Recursive Subroutine (frame pointers) + recursive_subroutine_example, + [ + [P, C], + ("int 10", "pt.Int(10)"), + ("callsub fib_0", "fib(pt.Int(10))"), + ("return", C), + ("", "def fib(n) -> pt.Expr:"), + ("// fib", "def fib(n) -> pt.Expr:"), + ("fib_0:", "def fib(n) -> pt.Expr:"), + ("proto 1 1", "def fib(n) -> pt.Expr:"), + ("frame_dig -1", C), + ("int 1", "pt.Int(1)"), + ("<=", "n <= pt.Int(1)"), + ( + "bnz fib_0_l2", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ("frame_dig -1", C), + ("int 1", "pt.Int(1)"), + ("-", "n - pt.Int(1)"), + ("callsub fib_0", "fib(n - pt.Int(1))"), + ("frame_dig -1", C), + ("int 2", "pt.Int(2)"), + ("-", "n - pt.Int(2)"), + ("callsub fib_0", "fib(n - pt.Int(2))"), + ("+", "fib(n - pt.Int(1)) + fib(n - pt.Int(2))"), + ( + "b fib_0_l3", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ( + "fib_0_l2:", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ("frame_dig -1", C), + ( + "fib_0_l3:", + "pt.If(n <= pt.Int(1), n, fib(n - pt.Int(1)) + fib(n - pt.Int(2)))", + ), + ("retsub", "def fib(n) -> pt.Expr:"), + ], + 8, + "Application", + dict(frame_pointers=True), + ), +] + + +@pytest.mark.slow +@pytest.mark.serial +@pytest.mark.skipif(sys.version_info < (3, 11), reason="Too slow in Python 3.10") +@pytest.mark.parametrize("i, test_case", enumerate(CONSTRUCTS)) +@pytest.mark.parametrize("mode", ["Application", "Signature"]) +@pytest.mark.parametrize("version", range(2, CONSTRUCTS_LATEST_VERSION + 1)) +def test_constructs_311(mock_ConfigParser, i, test_case, mode, version): + constructs_test(i, test_case, mode, version) + + +def constructs_test(i, test_case, mode, version): + import pyteal as pt + + expr, line2unparsed = test_case[:2] + line2unparsed = deepcopy(line2unparsed) + # fill the pragma template: + line2unparsed[0][0] = line2unparsed[0][0].format(v=version) + + expr = expr(pt) + if len(test_case) > 2: + min_version = test_case[2] + if version < min_version: + return + if len(test_case) > 3: + fixed_mode = test_case[3] + if mode != fixed_mode: + return + optimize = None + if len(test_case) > 4: + optimize = pt.OptimizeOptions(**test_case[4]) + + mode = getattr(pt.Mode, mode) + if isinstance(expr, pt.Router): + assert len(test_case) > 3 and fixed_mode == "Application" # type: ignore + + rci = pt.ast.router._RouterCompileInput( + version=version, + assemble_constants=False, + optimize=optimize, + with_sourcemaps=True, + ) + sourcemap = expr._build_impl(rci).approval_sourcemapper + else: + comp = pt.Compilation( + expr, mode, version=version, assemble_constants=False, optimize=optimize + ) + bundle = comp._compile_impl(with_sourcemap=True) + sourcemap = bundle.sourcemapper + + msg = f"[CASE #{i}]: {expr=}" + + assert sourcemap, msg + + tmis = sourcemap.as_list() + N = len(tmis) + + teal_linenos = [tmi.teal_lineno for tmi in tmis] + assert list(range(1, N + 1)) == teal_linenos + + teal_lines = [tmi.teal_line for tmi in tmis] + assert teal_lines == [ + l for chunk in sourcemap.teal_chunks for l in chunk.splitlines() + ] + + unparsed = [tmi.hybrid_unparsed() for tmi in tmis] # type: ignore + msg = f"{msg}, {tmis=}" + FORCE_FAIL_FOR_CASE_CREATION = False + if FORCE_FAIL_FOR_CASE_CREATION: + ouch = [(t, unparsed[i]) for i, t in enumerate(teal_lines)] + print(ouch) + x = "DEBUG" * 100 + assert not x, ouch + + expected_lines, expected_unparsed = list(zip(*line2unparsed)) + + assert list(expected_lines) == teal_lines, msg + assert list(expected_unparsed) == unparsed, msg diff --git a/tests/unit/sourcemap_constructs_allpy_test.py b/tests/unit/sourcemap_constructs_allpy_test.py new file mode 100644 index 000000000..a76691480 --- /dev/null +++ b/tests/unit/sourcemap_constructs_allpy_test.py @@ -0,0 +1,27 @@ +from configparser import ConfigParser +from unittest import mock + +import pytest + +from tests.unit.sourcemap_constructs311_test import ( + CONSTRUCTS, + CONSTRUCTS_LATEST_VERSION, + constructs_test, +) + + +@pytest.fixture +def mock_ConfigParser(): + patcher = mock.patch.object(ConfigParser, "getboolean", return_value=True) + patcher.start() + yield + patcher.stop() + + +@pytest.mark.slow +@pytest.mark.serial +@pytest.mark.parametrize("i, test_case", enumerate(CONSTRUCTS)) +@pytest.mark.parametrize("mode", ["Application", "Signature"]) +@pytest.mark.parametrize("version", range(2, CONSTRUCTS_LATEST_VERSION + 1)) +def test_constructs_very_slow(mock_ConfigParser, i, test_case, mode, version): + constructs_test(i, test_case, mode, version) diff --git a/tests/unit/sourcemap_monkey_raises_test.py b/tests/unit/sourcemap_monkey_raises_test.py new file mode 100644 index 000000000..20a0d1d05 --- /dev/null +++ b/tests/unit/sourcemap_monkey_raises_test.py @@ -0,0 +1,31 @@ +from configparser import ConfigParser +from unittest import mock + +import pytest + + +@pytest.fixture +def mock_ConfigParser(): + patcher = mock.patch.object( + ConfigParser, "getboolean", side_effect=Exception("1337") + ) + patcher.start() + yield + patcher.stop() + + +@pytest.mark.serial +def test_sourcemap_fails_elegantly_when_no_ini(mock_ConfigParser): + from examples.application.abi.algobank import router + from pyteal import OptimizeOptions + from pyteal.errors import SourceMapDisabledError + + with pytest.raises(SourceMapDisabledError) as smde: + router.compile( + version=6, + optimize=OptimizeOptions(scratch_slots=True), + with_sourcemaps=True, + ) + + assert "pyteal.ini" in str(smde.value) + assert "1337" not in str(smde.value) diff --git a/tests/unit/sourcemap_monkey_unit_test.py b/tests/unit/sourcemap_monkey_unit_test.py new file mode 100644 index 000000000..cdbd07421 --- /dev/null +++ b/tests/unit/sourcemap_monkey_unit_test.py @@ -0,0 +1,817 @@ +"""Includeds monkey-patches of ConfigParser +""" + +from ast import FunctionDef +from contextlib import contextmanager +from configparser import ConfigParser +from pathlib import Path +import pytest +import sys +from unittest import mock + + +ALGOBANK = Path.cwd() / "examples" / "application" / "abi" + +FIXTURES = Path.cwd() / "tests" / "unit" / "sourcemaps" + + +@pytest.fixture +def mock_ConfigParser(): + patcher = mock.patch.object(ConfigParser, "getboolean", return_value=True) + patcher.start() + yield + patcher.stop() + + +@pytest.fixture +def context_StackFrame_keep_all_debugging(): + from pyteal.stack_frame import NatalStackFrame + + NatalStackFrame._keep_all_debugging = True + yield + NatalStackFrame._keep_all_debugging = False + + +@pytest.mark.skipif( + sys.version_info < (3, 11), + reason="Currently, this test only works in python 3.11 and above", +) +@pytest.mark.serial +def test_r3sourcemap(mock_ConfigParser): + from examples.application.abi.algobank import router + from pyteal.ast.router import _RouterCompileInput + from pyteal import OptimizeOptions + from pyteal.compiler.sourcemap import R3SourceMap + + filename = "dummy filename" + rci = _RouterCompileInput( + version=6, + assemble_constants=False, + optimize=OptimizeOptions(scratch_slots=True), + approval_filename=filename, + with_sourcemaps=True, + ) + compile_bundle = router._build_impl(rci) + + ptsm = compile_bundle.approval_sourcemapper + assert ptsm + + actual_unparsed = [x._hybrid_w_offset() for x in ptsm._cached_tmis] + assert_algobank_unparsed_as_expected(actual_unparsed) + + r3sm = ptsm._cached_r3sourcemap + assert r3sm + + assert filename == r3sm.filename + assert str(r3sm.source_root).startswith(str(Path.cwd())) + assert list(range(len(r3sm.entries))) == [line for line, _ in r3sm.entries] + assert all(c == 0 for _, c in r3sm.entries) + assert all(x == (0,) for x in r3sm.index) + assert len(r3sm.entries) == len(r3sm.index) + + this_file = __file__.split("/")[-1] + expected_source_files = [ + "examples/application/abi/algobank.py", + f"tests/unit/{this_file}", + ] + assert expected_source_files == r3sm.source_files + + r3sm_json = r3sm.to_json() + + assert "mappings" in r3sm_json + assert ( + "AAqDqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACwBrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ADxBqB;AAAA;ACwBrB;ADxBqB;AAAA;AAAA;ACwBrB;AAAA;AAAA;AAAA;ADxBqB;ACwBrB;ADxBqB;ACWrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ADXqB;AAAA;AAAA;AAAA;ACWrB;AAAA;AAAA;AAAA;AAAA;AAAA;ADXqB;ACWrB;ADXqB;ACbrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ADaqB;AAAA;AAAA;ACbrB;ADaqB;AAAA;AAAA;ACbrB;ADaqB;ACbrB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ADaqB;ACbrB;ADaqB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;ACzCjB;AACc;AAAd;AAA4C;AAAc;AAA3B;AAA/B;AAFuB;AD0CN;AAAA;AAAA;AC/BkB;AAAA;AD+BlB;AAAA;AAAA;AAAA;AAAA;ACjCiB;AAAA;AAdtC;AAAA;AAAA;AACkB;AAAgB;AAAhB;AAAP;AADX;AAkCA;AAAA;AAAA;ADaqB;AAAA;ACAN;AAAA;AAA0B;AAAA;AAA1B;AAAP;AACO;AAAA;AAA4B;AAA5B;AAAP;AAEI;AAAA;AACA;AACa;AAAA;AAAkB;AAA/B;AAAmD;AAAA;AAAnD;AAHJ;AAfR;AAwBA;AAAA;AAAA;AASmC;AAAgB;AAA7B;AATtB;AAaA;AAAA;AAAA;ADxBqB;AAAA;AC4CT;AACA;AACa;AAAc;AAA3B;AAA+C;AAA/C;AAHJ;AAKA;AA7DR;AA8DQ;AAG2B;AAAA;AAH3B;AAIyB;AAJzB;AAKsB;AALtB;AAQA;AAjCR" + == r3sm_json["mappings"] + ) + + assert "file" in r3sm_json + assert filename == r3sm_json["file"] + + assert "sources" in r3sm_json + + # jsonizing creates it's own separate order based on first seen and defaultdict with autoindex + expected_json_source_files = [ + f"tests/unit/{this_file}", + "examples/application/abi/algobank.py", + ] + assert set(expected_json_source_files) == set(r3sm_json["sources"]) + + assert "sourceRoot" in r3sm_json + assert r3sm.source_root == r3sm_json["sourceRoot"] + + target = "\n".join(r.target_extract for r in r3sm.entries.values()) # type: ignore + round_trip = R3SourceMap.from_json(r3sm_json, target=target) + + assert r3sm_json == round_trip.to_json() + + +@pytest.mark.serial +def test_reconstruct(mock_ConfigParser): + from examples.application.abi.algobank import router + from pyteal.ast.router import _RouterCompileInput + from pyteal import OptimizeOptions + + rci = _RouterCompileInput( + version=6, + assemble_constants=False, + optimize=OptimizeOptions(scratch_slots=True), + with_sourcemaps=True, + ) + compile_bundle = router._build_impl(rci) + + assert compile_bundle.approval_sourcemapper + assert compile_bundle.clear_sourcemapper + + def compare_and_assert(file, sourcemap): + with open(ALGOBANK / file, "r") as f: + expected_lines = f.read().splitlines() + actual_lines = sourcemap.pure_teal().splitlines() + assert len(expected_lines) == len(actual_lines) + assert expected_lines == actual_lines + + compare_and_assert("algobank_approval.teal", compile_bundle.approval_sourcemapper) + compare_and_assert("algobank_clear_state.teal", compile_bundle.clear_sourcemapper) + + +@pytest.mark.serial +def test_mocked_config_for_frames(mock_ConfigParser): + config = ConfigParser() + assert config.getboolean("pyteal-source-mapper", "enabled") is True + from pyteal.stack_frame import NatalStackFrame + + assert NatalStackFrame.sourcemapping_is_off() is False + assert NatalStackFrame.sourcemapping_is_off(_force_refresh=True) is False + + +def make(x, y, z): + import pyteal as pt + + return pt.Int(x) + pt.Int(y) + pt.Int(z) + + +@pytest.mark.serial +def test_lots_o_indirection(mock_ConfigParser): + import pyteal as pt + + e1 = pt.Seq(pt.Pop(make(1, 2, 3)), pt.Pop(make(4, 5, 6)), make(7, 8, 9)) + + @pt.Subroutine(pt.TealType.uint64) + def foo(x): + return pt.Seq(pt.Pop(e1), e1) + + pt.Compilation(foo(pt.Int(42)), pt.Mode.Application, version=6)._compile_impl( + with_sourcemap=True + ) + + +@pytest.mark.serial +def test_frame_info_is_right_before_core_last_drop_idx( + context_StackFrame_keep_all_debugging, +): + import pyteal as pt + from pyteal.stack_frame import StackFrame + + e1 = pt.Seq(pt.Pop(make(1, 2, 3)), pt.Pop(make(4, 5, 6)), make(7, 8, 9)) + + frame_infos = e1.stack_frames._frames + last_drop_idx = 1 + assert StackFrame._frame_info_is_right_before_core( + frame_infos[last_drop_idx].frame_info + ), "Uh oh! Something about NatalStackFrame as changes which puts in jeopardy Source Map functionality" + + +def router_static_abisubroutine(pt): + AppId = pt.abi.Uint64 + + class Foo: + """Some class docstring""" + + page_size = pt.Int(128 - 1) + + account = pt.abi.Address() + app_id = pt.abi.Uint64() + + @staticmethod + @pt.ABIReturnSubroutine + def set_foo( + _app_id: AppId, + ) -> pt.Expr: # type: ignore + "Some docstring" + return Foo.app_id.set(_app_id) + + router = pt.Router("foo") + router.add_method_handler( + Foo.set_foo, + "set_foo", + pt.MethodConfig(no_op=pt.CallConfig.CALL), + "Foo the foo", + ) + return router + + +SUCCESSFUL_SUBROUTINE_LINENO = 196 + + +@pytest.mark.serial +def test_hybrid_w_offset(mock_ConfigParser): + import pyteal as pt + + router = router_static_abisubroutine(pt) + rci = pt.ast.router._RouterCompileInput( + version=7, + assemble_constants=True, + with_sourcemaps=True, + ) + + sourcemap = router._build_impl(rci).approval_sourcemapper + + # expected: + etarget = "def set_foo(_app_id: AppId) -> pt.Expr:" + func_source = f'@staticmethod\n@pt.ABIReturnSubroutine\n{etarget}\n """Some docstring"""\n return Foo.app_id.set(_app_id)' + + # actual: + lbf = sourcemap._best_frames[-1] + hwo = lbf._hybrid_w_offset() + nsource = lbf.node_source() + raw_code = lbf.raw_code() + naive_line = lbf.frame_info.lineno + + # consistent across versions: + assert etarget == hwo[0] + assert func_source == nsource + + # inconsistent between 3.10 and 3.11: + if sys.version_info[:2] <= (3, 10): + assert 0 == hwo[1] + assert "def set_foo(" == raw_code + assert SUCCESSFUL_SUBROUTINE_LINENO == naive_line + else: + assert 1 == hwo[1] + assert "@pt.ABIReturnSubroutine" == raw_code + assert SUCCESSFUL_SUBROUTINE_LINENO - 1 == naive_line + + +PyTealFrame_CASES = [ + ( + "some code", + False, + "some chunk", + ("some chunk", 0), + ), + ( + "some code", + True, + "some chunk", + ("some chunk", 0), + ), + ( + "first line", + True, + "first line and more\nsecond line", + ("first line and more", 0), + ), + ( + "first line", + True, + "first line and more\ndef second line", + ("def second line", 1), + ), + ( + "first line", + False, + None, + ("first line", 0), + ), + ( + "first line", + True, + None, + ("first line", 0), + ), +] + + +@contextmanager +def patch_pt_frame(code, is_funcdef, pt_chunk): + from pyteal.stack_frame import PyTealFrame + + node = None + if is_funcdef: + node = FunctionDef(name="foo", body=[], decorator_list=[], returns=None) + + frame = PyTealFrame( + frame_info="dummy frame_info", node=node, creator=None, full_stack=None + ) + with mock.patch.object(frame, "raw_code", return_value=code), mock.patch.object( + frame, "node_source", return_value=pt_chunk + ): + + yield frame + + +@pytest.mark.parametrize("code, is_funcdef, pt_chunk, expected", PyTealFrame_CASES) +def test_mock_hybrid_w_offset(code, is_funcdef, pt_chunk, expected): + with patch_pt_frame(code, is_funcdef, pt_chunk) as pt_frame: + assert expected == pt_frame._hybrid_w_offset() + + +def test_tabulate_args_can_be_dictified(): + from pyteal.compiler.sourcemap import _PyTealSourceMapper, TealMapItem + + tmi = TealMapItem( + pt_frame=mock.MagicMock(), + teal_lineno=13, + teal_line="some teal line", + teal_component="some teal component", + ) + all_cols = {v: v for v in _PyTealSourceMapper._tabulate_param_defaults.values()} + full_dict = tmi.asdict(**all_cols) + assert set(all_cols.keys()) == set(full_dict.keys()) + + +def assert_algobank_unparsed_as_expected(actual): + expected = [ + (0, ("router._build_impl(rci)", 0)), + (1, ("router._build_impl(rci)", 0)), + (2, ("router._build_impl(rci)", 0)), + (3, ("router._build_impl(rci)", 0)), + (4, ("router._build_impl(rci)", 0)), + (5, ("router._build_impl(rci)", 0)), + (6, ("router._build_impl(rci)", 0)), + (7, ("router._build_impl(rci)", 0)), + (8, ("router._build_impl(rci)", 0)), + (9, ("router._build_impl(rci)", 0)), + (10, ("router._build_impl(rci)", 0)), + (11, ("router._build_impl(rci)", 0)), + (12, ("router._build_impl(rci)", 0)), + (13, ("router._build_impl(rci)", 0)), + (14, ("router._build_impl(rci)", 0)), + (15, ("router._build_impl(rci)", 0)), + (16, ("router._build_impl(rci)", 0)), + (17, ("router._build_impl(rci)", 0)), + (18, ("router._build_impl(rci)", 0)), + (19, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (20, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (21, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (22, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (23, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (24, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (25, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (26, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (27, ("router._build_impl(rci)", 0)), + (28, ("router._build_impl(rci)", 0)), + (29, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (30, ("router._build_impl(rci)", 0)), + (31, ("router._build_impl(rci)", 0)), + (32, ("router._build_impl(rci)", 0)), + (33, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (34, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (35, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (36, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (37, ("router._build_impl(rci)", 0)), + (38, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (39, ("router._build_impl(rci)", 0)), + (40, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (41, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (42, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (43, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (44, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (45, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (46, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (47, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (48, ("router._build_impl(rci)", 0)), + (49, ("router._build_impl(rci)", 0)), + (50, ("router._build_impl(rci)", 0)), + (51, ("router._build_impl(rci)", 0)), + (52, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (53, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (54, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (55, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (56, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (57, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (58, ("router._build_impl(rci)", 0)), + (59, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (60, ("router._build_impl(rci)", 0)), + ( + 61, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 62, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 63, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 64, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 65, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 66, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 67, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 68, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 69, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 70, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 71, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 72, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 73, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 74, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 75, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 76, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + (77, ("router._build_impl(rci)", 0)), + (78, ("router._build_impl(rci)", 0)), + (79, ("router._build_impl(rci)", 0)), + ( + 80, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + (81, ("router._build_impl(rci)", 0)), + (82, ("router._build_impl(rci)", 0)), + (83, ("router._build_impl(rci)", 0)), + ( + 84, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + (85, ("router._build_impl(rci)", 0)), + ( + 86, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 87, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 88, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 89, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 90, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 91, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 92, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + (93, ("router._build_impl(rci)", 0)), + ( + 94, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + (95, ("router._build_impl(rci)", 0)), + (96, ("router._build_impl(rci)", 0)), + (97, ("router._build_impl(rci)", 0)), + (98, ("router._build_impl(rci)", 0)), + (99, ("router._build_impl(rci)", 0)), + (100, ("router._build_impl(rci)", 0)), + (101, ("router._build_impl(rci)", 0)), + (102, ("router._build_impl(rci)", 0)), + (103, ("router._build_impl(rci)", 0)), + (104, ("router._build_impl(rci)", 0)), + (105, ("router._build_impl(rci)", 0)), + (106, ("router._build_impl(rci)", 0)), + (107, ("router._build_impl(rci)", 0)), + (108, ("router._build_impl(rci)", 0)), + (109, ("router._build_impl(rci)", 0)), + (110, ("router._build_impl(rci)", 0)), + (111, ("router._build_impl(rci)", 0)), + (112, ("router._build_impl(rci)", 0)), + (113, ("router._build_impl(rci)", 0)), + (114, ("router._build_impl(rci)", 0)), + (115, ("router._build_impl(rci)", 0)), + (116, ("router._build_impl(rci)", 0)), + (117, ("router._build_impl(rci)", 0)), + (118, ("router._build_impl(rci)", 0)), + (119, ("router._build_impl(rci)", 0)), + (120, ("router._build_impl(rci)", 0)), + (121, ("router._build_impl(rci)", 0)), + (122, ("router._build_impl(rci)", 0)), + (123, ("router._build_impl(rci)", 0)), + (124, ("router._build_impl(rci)", 0)), + (125, ("router._build_impl(rci)", 0)), + (126, ("router._build_impl(rci)", 0)), + (127, ("router._build_impl(rci)", 0)), + (128, ("router._build_impl(rci)", 0)), + (129, ("router._build_impl(rci)", 0)), + (130, ("router._build_impl(rci)", 0)), + (131, ("router._build_impl(rci)", 0)), + (132, ("router._build_impl(rci)", 0)), + (133, ("router._build_impl(rci)", 0)), + (134, ("router._build_impl(rci)", 0)), + (135, ("router._build_impl(rci)", 0)), + (136, ("router._build_impl(rci)", 0)), + (137, ("router._build_impl(rci)", 0)), + (138, ("Bytes('lost')", 0)), + (139, ("Bytes('lost')", 0)), + (140, ("App.globalGet(Bytes('lost'))", 0)), + (141, ("Txn.sender()", 0)), + (142, ("Bytes('balance')", 0)), + (143, ("App.localGet(Txn.sender(), Bytes('balance'))", 0)), + ( + 144, + ( + "App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance'))", + 0, + ), + ), + ( + 145, + ( + "App.globalPut(Bytes('lost'), App.globalGet(Bytes('lost')) + App.localGet(Txn.sender(), Bytes('balance')))", + 0, + ), + ), + (146, ("router._build_impl(rci)", 0)), + (147, ("router._build_impl(rci)", 0)), + (148, ("router._build_impl(rci)", 0)), + (149, ("Approve()", 0)), + (150, ("Approve()", 0)), + (151, ("router._build_impl(rci)", 0)), + (152, ("router._build_impl(rci)", 0)), + (153, ("router._build_impl(rci)", 0)), + (154, ("router._build_impl(rci)", 0)), + (155, ("router._build_impl(rci)", 0)), + (156, ("Approve()", 0)), + (157, ("Approve()", 0)), + (158, ("def assert_sender_is_creator() -> Expr:", 1)), + (159, ("def assert_sender_is_creator() -> Expr:", 1)), + (160, ("def assert_sender_is_creator() -> Expr:", 1)), + (161, ("Txn.sender()", 0)), + (162, ("Global.creator_address()", 0)), + (163, ("Txn.sender() == Global.creator_address()", 0)), + (164, ("Assert(Txn.sender() == Global.creator_address())", 0)), + (165, ("def assert_sender_is_creator() -> Expr:", 1)), + ( + 166, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 167, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 168, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + (169, ("router._build_impl(rci)", 0)), + (170, ("router._build_impl(rci)", 0)), + (171, ("payment.get()", 0)), + (172, ("payment.get().sender()", 0)), + (173, ("sender.address()", 0)), + (174, ("sender.address()", 0)), + (175, ("payment.get().sender() == sender.address()", 0)), + (176, ("Assert(payment.get().sender() == sender.address())", 0)), + (177, ("payment.get()", 0)), + (178, ("payment.get().receiver()", 0)), + (179, ("Global.current_application_address()", 0)), + (180, ("payment.get().receiver() == Global.current_application_address()", 0)), + ( + 181, + ( + "Assert(payment.get().receiver() == Global.current_application_address())", + 0, + ), + ), + (182, ("sender.address()", 0)), + (183, ("sender.address()", 0)), + (184, ("Bytes('balance')", 0)), + (185, ("sender.address()", 0)), + (186, ("sender.address()", 0)), + (187, ("Bytes('balance')", 0)), + (188, ("App.localGet(sender.address(), Bytes('balance'))", 0)), + (189, ("payment.get()", 0)), + (190, ("payment.get().amount()", 0)), + ( + 191, + ( + "App.localGet(sender.address(), Bytes('balance')) + payment.get().amount()", + 0, + ), + ), + ( + 192, + ( + "App.localPut(sender.address(), Bytes('balance'), App.localGet(sender.address(), Bytes('balance')) + payment.get().amount())", + 0, + ), + ), + ( + 193, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + (194, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (195, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (196, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (197, ("user.address()", 0)), + (198, ("Bytes('balance')", 0)), + (199, ("App.localGet(user.address(), Bytes('balance'))", 0)), + (200, ("def getBalance(user: abi.Account, *, output: abi.Uint64) -> Expr:", 1)), + (201, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (202, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (203, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + (204, ("router._build_impl(rci)", 0)), + (205, ("router._build_impl(rci)", 0)), + (206, ("Txn.sender()", 0)), + (207, ("Bytes('balance')", 0)), + (208, ("Txn.sender()", 0)), + (209, ("Bytes('balance')", 0)), + (210, ("App.localGet(Txn.sender(), Bytes('balance'))", 0)), + (211, ("amount.get()", 0)), + (212, ("App.localGet(Txn.sender(), Bytes('balance')) - amount.get()", 0)), + ( + 213, + ( + "App.localPut(Txn.sender(), Bytes('balance'), App.localGet(Txn.sender(), Bytes('balance')) - amount.get())", + 0, + ), + ), + (214, ("InnerTxnBuilder.Begin()", 0)), + ( + 215, + ( + "def deposit(payment: abi.PaymentTransaction, sender: abi.Account) -> Expr:", + 1, + ), + ), + ( + 216, + ( + "InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: recipient.address(), TxnField.amount: amount.get(), TxnField.fee: Int(0)})", + 0, + ), + ), + (217, ("recipient.address()", 0)), + (218, ("recipient.address()", 0)), + ( + 219, + ( + "InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: recipient.address(), TxnField.amount: amount.get(), TxnField.fee: Int(0)})", + 0, + ), + ), + (220, ("amount.get()", 0)), + ( + 221, + ( + "InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: recipient.address(), TxnField.amount: amount.get(), TxnField.fee: Int(0)})", + 0, + ), + ), + (222, ("Int(0)", 0)), + ( + 223, + ( + "InnerTxnBuilder.SetFields({TxnField.type_enum: TxnType.Payment, TxnField.receiver: recipient.address(), TxnField.amount: amount.get(), TxnField.fee: Int(0)})", + 0, + ), + ), + (224, ("InnerTxnBuilder.Submit()", 0)), + (225, ("def withdraw(amount: abi.Uint64, recipient: abi.Account) -> Expr:", 1)), + ] + assert len(expected) == len(actual) + print(list(enumerate(actual))) + + for i, a in enumerate(actual): + assert ( + e := expected[i][1] + ) == a, f"""discrepancy at index {i=} +expected: +{e} +!= actual: +{a}""" diff --git a/tests/unit/sourcemap_rps_test.py b/tests/unit/sourcemap_rps_test.py new file mode 100644 index 000000000..e662ed8a1 --- /dev/null +++ b/tests/unit/sourcemap_rps_test.py @@ -0,0 +1,87 @@ +from configparser import ConfigParser +from pathlib import Path +from unittest import mock + +import pytest + +RPS = Path.cwd() / "tests" / "teal" + + +@pytest.fixture +def mock_ConfigParser(): + patcher = mock.patch.object(ConfigParser, "getboolean", return_value=True) + patcher.start() + yield + patcher.stop() + + +def compare_and_assert(file, actual): + with open(file, "r") as f: + expected_lines = f.read().splitlines() + actual_lines = actual.splitlines() + assert len(expected_lines) == len(actual_lines) + assert expected_lines == actual_lines + + +def no_regressions_rps(): + from pyteal import compileTeal, Mode + from tests.teal.rps import approval_program + + compiled = compileTeal(approval_program(), Mode.Application, version=6) + compare_and_assert(RPS / "rps.teal", compiled) + + +@pytest.mark.serial +def test_annotated_rps(mock_ConfigParser): + from pyteal import Compilation, Mode + from pyteal.compiler.sourcemap import _PyTealSourceMapper + from tests.teal.rps import approval_program + + with open(RPS / "rps.teal", "r") as f: + expected_teal = f.read() + + with open(RPS / "rps_annotated.teal", "r") as f: + expected_annotated = f.read() + + cbundle = Compilation(approval_program(), Mode.Application, version=6).compile( + with_sourcemap=True, + annotate_teal=True, + annotate_teal_headers=True, + annotate_teal_concise=False, + ) + teal, annotated = cbundle.teal, cbundle.sourcemap.annotated_teal + + assert expected_teal == teal + assert expected_annotated == annotated + _PyTealSourceMapper._validate_annotated( + False, teal.splitlines(), annotated.splitlines() + ) + + +@pytest.mark.serial +def test_no_regression_with_sourcemap_as_configured_rps(): + no_regressions_rps() + + +@pytest.mark.serial +def test_no_regression_with_sourcemap_enabled_rps(): + from pyteal.stack_frame import NatalStackFrame + + originally = NatalStackFrame._no_stackframes + NatalStackFrame._no_stackframes = False + + no_regressions_rps() + + NatalStackFrame._no_stackframes = originally + + +@pytest.mark.serial +def test_no_regression_with_sourcemap_disabled_rps(): + from pyteal.stack_frame import NatalStackFrame + + originally = NatalStackFrame._no_stackframes + NatalStackFrame._no_stackframes = True + + no_regressions_rps() + + NatalStackFrame._no_stackframes = originally diff --git a/tests/unit/sourcemap_test.py b/tests/unit/sourcemap_test.py new file mode 100644 index 000000000..b9a1d570b --- /dev/null +++ b/tests/unit/sourcemap_test.py @@ -0,0 +1,541 @@ +""" +This test would typically reside right next to `pyteal/compiler/sourcemap.py`. +However, since the path `pyteal/compiler` is on the StackFrame._internal_paths +blacklist, we need to move the test elsewhere to get reliable results. +""" + +import ast +import json +import time +from configparser import ConfigParser +from pathlib import Path +from unittest import mock + +import pytest + +from pyteal.compiler.sourcemap import R3SourceMap, R3SourceMapJSON + +ALGOBANK = Path.cwd() / "examples" / "application" / "abi" + + +@pytest.fixture +def context_StackFrame_keep_all_debugging(): + from pyteal.stack_frame import NatalStackFrame + + NatalStackFrame._keep_all_debugging = True + yield + NatalStackFrame._keep_all_debugging = False + + +@pytest.mark.serial +def test_frames(context_StackFrame_keep_all_debugging): + from pyteal.stack_frame import NatalStackFrame + + originally = NatalStackFrame._no_stackframes + NatalStackFrame._no_stackframes = False + + this_file, this_func = "sourcemap_test.py", "test_frames" + this_lineno, this_frame = 38, NatalStackFrame()._frames[1] + code = ( + f" this_lineno, this_frame = {this_lineno}, NatalStackFrame()._frames[1]\n" + ) + this_col_offset, this_end_col_offset = 34, 51 + frame_info, node = this_frame.frame_info, this_frame.node + + assert frame_info.filename.endswith(this_file) + assert this_func == frame_info.function + assert frame_info.code_context + assert len(frame_info.code_context) == 1 + assert code == frame_info.code_context[0] + assert this_lineno == frame_info.lineno + + assert node + assert this_lineno == node.lineno == node.end_lineno + assert this_col_offset == node.col_offset + assert this_end_col_offset == node.end_col_offset + assert isinstance(node, ast.Call) + assert isinstance(node.parent, ast.Attribute) # type: ignore + assert isinstance(node.parent.parent, ast.Subscript) # type: ignore + + NatalStackFrame._no_stackframes = originally + + +@pytest.mark.serial +def test_TealMapItem_source_mapping(): + from pyteal.stack_frame import NatalStackFrame + + originally = NatalStackFrame._no_stackframes + + NatalStackFrame._no_stackframes = False + + import pyteal as pt + from pyteal.compiler.sourcemap import TealMapItem + + expr = pt.Int(0) + pt.Int(1) + expr_line_offset, expr_str = 50, "expr = pt.Int(0) + pt.Int(1)" + + def mock_teal(ops): + return [f"{i+1}. {op}" for i, op in enumerate(ops)] + + components = [] + b = expr.__teal__(pt.CompileOptions())[0] + while b: + components.extend(b.ops) + b = b.nextBlock # type: ignore + + teals = mock_teal(components) + tmis = [ + TealMapItem(op.expr.stack_frames._frames[0].as_pyteal_frame(), i, teals[i], op) + for i, op in enumerate(components) + ] + + mock_source_lines = [""] * 500 + mock_source_lines[expr_line_offset] = expr_str + source_files = ["sourcemap_test.py"] + r3sm = R3SourceMap( + filename="dohhh.teal", + source_root="~", + entries={(i, 0): tmi.source_mapping() for i, tmi in enumerate(tmis)}, + index=[(0,) for _ in range(3)], + file_lines=list(map(lambda x: x.teal_line, tmis)), + source_files=source_files, + source_files_lines=[mock_source_lines], + ) + expected_json = '{"version": 3, "sources": ["tests/unit/sourcemap_test.py"], "names": [], "mappings": "AAyEW;AAAY;AAAZ", "file": "dohhh.teal", "sourceRoot": "~"}' + + assert expected_json == json.dumps(r3sm.to_json()) + + r3sm_unmarshalled = R3SourceMap.from_json( + R3SourceMapJSON(**json.loads(expected_json)), # type: ignore + sources_content_override=["\n".join(mock_source_lines)], + target="\n".join(teals), + ) + + # TODO: test various properties of r3sm_unmarshalled + + assert expected_json == json.dumps(r3sm_unmarshalled.to_json()) + + NatalStackFrame._no_stackframes = originally + + +def compare_and_assert(file, actual): + with open(file, "r") as f: + expected_lines = f.read().splitlines() + actual_lines = actual.splitlines() + assert len(expected_lines) == len(actual_lines) + assert expected_lines == actual_lines + + +def no_regressions_algobank(): + from examples.application.abi.algobank import router + from pyteal import OptimizeOptions + + approval, clear, contract = router.compile_program( + version=6, optimize=OptimizeOptions(scratch_slots=True) + ) + + compare_and_assert( + ALGOBANK / "algobank.json", json.dumps(contract.dictify(), indent=4) + ) + compare_and_assert(ALGOBANK / "algobank_clear_state.teal", clear) + compare_and_assert(ALGOBANK / "algobank_approval.teal", approval) + + +@pytest.mark.serial +def test_no_regression_with_sourcemap_as_configured_algobank(): + no_regressions_algobank() + + +@pytest.mark.serial +def test_no_regression_with_sourcemap_enabled_algobank(): + from pyteal.stack_frame import NatalStackFrame + + originally = NatalStackFrame._no_stackframes + NatalStackFrame._no_stackframes = False + + no_regressions_algobank() + + NatalStackFrame._no_stackframes = originally + + +@pytest.mark.serial +def test_no_regression_with_sourcemap_disabled_algobank(): + from pyteal.stack_frame import NatalStackFrame + + originally = NatalStackFrame._no_stackframes + NatalStackFrame._no_stackframes = True + + no_regressions_algobank() + + NatalStackFrame._no_stackframes = originally + + +@pytest.mark.serial +def test_sourcemap_fails_because_unconfigured(): + from examples.application.abi.algobank import router + from pyteal import OptimizeOptions + from pyteal.errors import SourceMapDisabledError + + with pytest.raises(SourceMapDisabledError) as smde: + router.compile( + version=6, + optimize=OptimizeOptions(scratch_slots=True), + with_sourcemaps=True, + ) + + assert "pyteal.ini" in str(smde.value) + + +@pytest.mark.serial +def test_config(): + from pyteal.stack_frame import NatalStackFrame + + config = ConfigParser() + config.read([".flake8", "mypy.ini", "pyteal.ini"]) + + assert [ + "flake8", + "mypy", + "mypy-semantic_version.*", + "mypy-pytest.*", + "mypy-algosdk.*", + "pyteal", + "pyteal-source-mapper", + ] == config.sections() + + assert ["ignore", "per-file-ignores", "ban-relative-imports"] == config.options( + "flake8" + ) + + assert ["enabled", "debug"] == config.options("pyteal-source-mapper") + + assert config.getboolean("pyteal-source-mapper", "enabled") is False + assert NatalStackFrame.sourcemapping_is_off() is True + + originally = NatalStackFrame._no_stackframes + NatalStackFrame._no_stackframes = False + + NatalStackFrame._no_stackframes = False + assert NatalStackFrame.sourcemapping_is_off() is False + assert NatalStackFrame.sourcemapping_is_off(_force_refresh=True) is True + + NatalStackFrame._no_stackframes = originally + + +def test_PyTealSourceMapper_validate_build_annotate(): + from pyteal import TealInternalError + from pyteal.compiler.sourcemap import _PyTealSourceMapper + + # --- CONSTRUCTOR VALIDATIONS --- # + match = "Please provide non-empty teal_chunks" + with pytest.raises(TealInternalError, match=match): + _PyTealSourceMapper([], [], build=False, annotate_teal=False) + + match = "Please provide non-empty components" + with pytest.raises(TealInternalError, match=match): + _PyTealSourceMapper(["a chunk"], [], build=False, annotate_teal=False) + + # --- BUILD VALIDATIONS --- # + ptsm = _PyTealSourceMapper( + ["a chunk"], ["a component", "another"], build=False, annotate_teal=False + ) + + def full_match(s): + return f"""{s} + {_PyTealSourceMapper.UNEXPECTED_ERROR_SUFFIX}""" + + match = full_match( + r"expected same number of teal chunks \(1\) and components \(2\)" + ) + with pytest.raises(TealInternalError, match=match): + ptsm.build() + + ptsm.teal_chunks = ptsm.components = [] + match = full_match("cannot generate empty source map: no components") + with pytest.raises(TealInternalError, match=match): + ptsm.build() + + teal_chunks = ["first line\nsecond line", "third line\nfourth line\nfifth line"] + teal = [ + "first line", + "second line", + "third line", + "fourth line", + "fifth line", + ] + i = 0 + for chunk in teal_chunks: + for line in chunk.splitlines(): + assert teal[i] == line + i += 1 + + def mock_TealMapItem(s): + tmi = mock.Mock() + tmi.teal_line = s + return tmi + + def mock_R3SourceMap(lines): + r3sm = mock.Mock() + r3sm.file_lines = lines + return r3sm + + ptsm.teal_chunks = teal_chunks + ptsm._cached_tmis = [mock_TealMapItem(s) for s in teal] + ptsm._cached_r3sourcemap = mock_R3SourceMap(teal) + + ptsm._validate_build() + ptsm.teal_chunks.append("sixth line") + match = full_match( + r"teal chunks has 6 teal lines which doesn't match the number of cached TealMapItem's \(5\)" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_build() + + ptsm._cached_tmis.append(mock_TealMapItem("sixth line")) + ptsm._cached_r3sourcemap.file_lines.append("sixth line") + ptsm._validate_build() + + match = full_match( + r"teal chunks has 6 teal lines which doesn't match the number of cached TealMapItem's \(7\)" + ) + ptsm._cached_tmis.append(mock_TealMapItem("seventh line")) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_build() + + del ptsm._cached_tmis[-1] + ptsm._validate_build() + + ptsm._cached_tmis[-1] = mock_TealMapItem("NOT the sixth line") + match = full_match( + r"teal chunk lines don't match TealMapItem's at index 5. \('sixth line' v. 'NOT the sixth line'\)" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_build() + + ptsm._cached_tmis[-1] = mock_TealMapItem("sixth line") + ptsm._validate_build() + + ptsm._cached_r3sourcemap.file_lines.append("seventh line") + match = full_match( + r"there are 6 TealMapItem's which doesn't match the number of file_lines in the cached R3SourceMap \(7\)" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_build() + + del ptsm._cached_r3sourcemap.file_lines[-1] + ptsm._validate_build() + + ptsm._cached_r3sourcemap.file_lines[-1] = "NOT the sixth line" + match = full_match( + r"TealMapItem's don't match R3SourceMap.file_lines at index 5. \('sixth line' v. 'NOT the sixth line'\)" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_build() + + ptsm._cached_r3sourcemap.file_lines[-1] = "sixth line" + ptsm._validate_build() + + # --- ANNOTATE VALIDATIONS --- # + annotated = [f"{teal} // some other stuff{i}" for i, teal in enumerate(teal)] + omit_headers = True + ptsm._validate_annotated(omit_headers, teal, annotated) + + omit_headers = False + match = full_match( + r"mismatch between count of teal_lines \(6\) and annotated_lines \(6\) for the case omit_headers=False" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_annotated(omit_headers, teal, annotated) + + annotated_w_headers = ["// some header"] + annotated + ptsm._validate_annotated(omit_headers, teal, annotated_w_headers) + + omit_headers = True + match = full_match( + r"mismatch between count of teal_lines \(6\) and annotated_lines \(7\) for the case omit_headers=True" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_annotated(omit_headers, teal, annotated_w_headers) + + annotated_3 = annotated[3] + annotated[3] = "doesn't begin with the teal line" + match = full_match( + r"annotated teal ought to begin exactly with the teal line but line 4 \[doesn't begin with the teal line\] doesn't start with \[fourth line\]" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_annotated(omit_headers, teal, annotated) + + annotated[3] = annotated_3 + ptsm._validate_annotated(omit_headers, teal, annotated) + + annotated_w_headers[4] = "doesn't begin with the teal line" + omit_headers = False + with pytest.raises(TealInternalError, match=match): + ptsm._validate_annotated(omit_headers, teal, annotated_w_headers) + + annotated_w_headers[4] = annotated_3 + ptsm._validate_annotated(omit_headers, teal, annotated_w_headers) + + omit_headers = True + annotated_2 = annotated[2] + annotated[2] = f"{teal[2]} some other stuff not all // commented out" + match = full_match( + rf"annotated teal ought to begin exactly with the teal line followed by annotation in comments but line 3 \[{annotated[2]}\] has non-commented out annotations" + ) + with pytest.raises(TealInternalError, match=match): + ptsm._validate_annotated(omit_headers, teal, annotated) + + annotated[2] = annotated_2 + ptsm._validate_annotated(omit_headers, teal, annotated) + + omit_headers = False + annotated_w_headers[3] = f"{teal[2]} some other stuff not all // commented out" + with pytest.raises(TealInternalError, match=match): + ptsm._validate_annotated(omit_headers, teal, annotated_w_headers) + + annotated_w_headers[3] = annotated_2 + ptsm._validate_annotated(omit_headers, teal, annotated_w_headers) + + # --- ANNOTATE VALIDATIONS - SPECIAL CASE --- # + meth_sig = "deposit(pay,account)void" + special = f"method {meth_sig} // (30)" + teal.append(special) + annotated_w_headers.append(special) + ptsm._validate_annotated(omit_headers, teal, annotated_w_headers) + + +@pytest.mark.skip( + reason="""Supressing this flaky test as +router_test::test_router_compile_program_idempotence is similar in its goals +and we expect flakiness to persist until https://github.com/algorand/pyteal/issues/199 +is finally addressed """ +) +@pytest.mark.serial +def test_idempotent(): + # make sure we get clean up properly and therefore get idempotent results + from examples.application.abi.algobank import router + from pyteal import OptimizeOptions + + approval1, clear1, contract1 = ( + func := lambda: router.compile_program( + version=6, optimize=OptimizeOptions(scratch_slots=True) + ) + )() + approval2, clear2, contract2 = func() + + assert contract1.dictify() == contract2.dictify() + assert len(clear1.splitlines()) == len(clear2.splitlines()) + assert clear1 == clear2 + assert len(approval1.splitlines()) == len(approval2.splitlines()) + assert approval1 == approval2 + + +# ---- BENCHMARKS - SKIPPED BY DEFAULT ---- # + + +def time_for_n_secs(f, n): + start = time.time() + + def since(): + return time.time() - start + + total_time = 0.0 + snapshots = [0.0] + while total_time < n: + f() + total_time = since() + snapshots.append(total_time) + + trials = [snapshots[i + 1] - s for i, s in enumerate(snapshots[:-1])] + return trials, total_time + + +def simple_compilation(): + from examples.application.abi.algobank import router + from pyteal import OptimizeOptions + + router.compile_program(version=6, optimize=OptimizeOptions(scratch_slots=True)) + + +def source_map_compilation(): + from examples.application.abi.algobank import router + from pyteal import OptimizeOptions + + router.compile( + version=6, + optimize=OptimizeOptions(scratch_slots=True), + with_sourcemaps=True, + ) + + +def annotated_teal(): + from examples.application.abi.algobank import router + from pyteal import OptimizeOptions + + compilation = router.compile( + version=6, + optimize=OptimizeOptions(scratch_slots=True), + with_sourcemaps=True, + ) + + assert compilation.approval_sourcemapper + + return compilation.approval_sourcemapper.annotated_teal() + + +summaries_only = True + + +def trial(func): + trials, tot = time_for_n_secs(simple_compilation, 10) + avg = tot / len(trials) + N = len(trials) + trials = "" if summaries_only else f"{trials=}" + print( + f""" +{func.__name__}: {avg=}, {N=} +{trials}""" + ) + + +@pytest.mark.skip(reason="Benchmarks are too slow to run every time") +@pytest.mark.serial +def test_time_benchmark_under_config(): + from pyteal.stack_frame import NatalStackFrame + + print(f"{NatalStackFrame.sourcemapping_is_off()=}") + + trial(simple_compilation) + trial(simple_compilation) + + assert False + + +@pytest.mark.skip(reason="Benchmarks are too slow to run every time") +@mock.patch.object(ConfigParser, "getboolean", return_value=True) +@pytest.mark.serial +def test_time_benchmark_sourcemap_enabled(_): + """ + UPSHOT: expect deterioration of (5 to 15)X when enabling source maps. + """ + from pyteal.stack_frame import NatalStackFrame + + print(f"{NatalStackFrame.sourcemapping_is_off()=}") + print( + """ +keep_all: bool = True, +stop_after_first_pyteal: bool = True, +keep_one_frame_only: bool = True, +""" + ) + + trial(simple_compilation) + trial(simple_compilation) + + trial(source_map_compilation) + trial(source_map_compilation) + + trial(annotated_teal) + trial(annotated_teal) + + assert False