From cf95165ce67bd465e02e501f307476a085e784c0 Mon Sep 17 00:00:00 2001 From: Zeph Grunschlag Date: Tue, 1 Mar 2022 16:10:54 -0600 Subject: [PATCH] Pass-by-Ref / Dynamic Scratch Variables via the `loads` and `stores` opcodes (#198) * Pass-by-Reference Semantics * Use a Dynamic ScratchVar to "iterate" over other ScratchVar's * Another approach for E2E testing --- pyteal/__init__.pyi | 250 ++++++++++---------- pyteal/ast/__init__.py | 16 +- pyteal/ast/multi_test.py | 6 +- pyteal/ast/scratch.py | 113 ++++++++- pyteal/ast/scratch_test.py | 54 +++++ pyteal/ast/scratchvar.py | 84 ++++++- pyteal/ast/scratchvar_test.py | 111 +++++++++ pyteal/ast/subroutine.py | 170 +++++++++++-- pyteal/ast/subroutine_test.py | 106 ++++++++- pyteal/compiler/compiler.py | 2 - requirements.txt | 2 +- scripts/generate_init.py | 11 +- tests/compile_asserts.py | 73 ++++++ tests/pass_by_ref_test.py | 207 ++++++++++++++++ tests/teal/fac_by_ref_expected.teal | 41 ++++ tests/teal/sub_even_expected.teal | 35 +++ tests/teal/sub_fastfib_expected.teal | 35 +++ tests/teal/sub_logcat_dynamic_expected.teal | 28 +++ tests/teal/sub_logcat_expected.teal | 23 ++ tests/teal/sub_mixed_expected.teal | 24 ++ tests/teal/sub_slowfib_expected.teal | 34 +++ tests/teal/swapper_expected.teal | 47 ++++ tests/teal/wilt_the_stilt_expected.teal | 38 +++ 23 files changed, 1327 insertions(+), 183 deletions(-) create mode 100644 tests/compile_asserts.py create mode 100644 tests/pass_by_ref_test.py create mode 100644 tests/teal/fac_by_ref_expected.teal create mode 100644 tests/teal/sub_even_expected.teal create mode 100644 tests/teal/sub_fastfib_expected.teal create mode 100644 tests/teal/sub_logcat_dynamic_expected.teal create mode 100644 tests/teal/sub_logcat_expected.teal create mode 100644 tests/teal/sub_mixed_expected.teal create mode 100644 tests/teal/sub_slowfib_expected.teal create mode 100644 tests/teal/swapper_expected.teal create mode 100644 tests/teal/wilt_the_stilt_expected.teal diff --git a/pyteal/__init__.pyi b/pyteal/__init__.pyi index b96384383..1591faeea 100644 --- a/pyteal/__init__.pyi +++ b/pyteal/__init__.pyi @@ -17,156 +17,158 @@ from .errors import TealInternalError, TealTypeError, TealInputError, TealCompil from .config import MAX_GROUP_SIZE, NUM_SLOTS __all__ = [ - "Expr", - "LeafExpr", + "AccountParam", + "Add", "Addr", - "Bytes", - "Int", - "EnumInt", - "MethodSignature", - "Arg", - "TxnType", - "TxnField", - "TxnExpr", - "TxnaExpr", - "TxnArray", - "TxnObject", - "Txn", - "GtxnExpr", - "GtxnaExpr", - "TxnGroup", - "Gtxn", - "GeneratedID", - "ImportScratchValue", - "Global", - "GlobalField", + "And", "App", "AppField", - "OnComplete", "AppParam", + "Approve", + "Arg", + "Array", + "Assert", "AssetHolding", "AssetParam", - "AccountParam", - "InnerTxnBuilder", - "InnerTxn", - "InnerTxnAction", - "Gitxn", - "GitxnExpr", - "GitxnaExpr", - "InnerTxnGroup", - "Array", - "Tmpl", - "Nonce", - "UnaryExpr", - "Btoi", - "Itob", - "Len", - "BitLen", - "Sha256", - "Sha512_256", - "Keccak256", - "Not", - "BitwiseNot", - "Sqrt", - "Pop", "Balance", - "MinBalance", "BinaryExpr", - "Add", - "Minus", - "Mul", - "Div", - "Mod", - "Exp", - "Divw", + "BitLen", "BitwiseAnd", + "BitwiseNot", "BitwiseOr", "BitwiseXor", - "ShiftLeft", - "ShiftRight", - "Eq", - "Neq", - "Lt", - "Le", - "Gt", - "Ge", - "GetBit", - "GetByte", - "Ed25519Verify", - "Substring", - "Extract", - "Suffix", - "SetBit", - "SetByte", - "NaryExpr", - "And", - "Or", - "Concat", - "WideRatio", - "If", - "Cond", - "Seq", - "Assert", - "Err", - "Return", - "Approve", - "Reject", - "Subroutine", - "SubroutineDefinition", - "SubroutineDeclaration", - "SubroutineCall", - "SubroutineFnWrapper", - "ScratchSlot", - "ScratchLoad", - "ScratchStore", - "ScratchStackStore", - "ScratchVar", - "MaybeValue", - "MultiValue", + "Break", + "Btoi", + "Bytes", "BytesAdd", - "BytesMinus", - "BytesDiv", - "BytesMul", - "BytesMod", "BytesAnd", - "BytesOr", - "BytesXor", + "BytesDiv", "BytesEq", - "BytesNeq", - "BytesLt", - "BytesLe", - "BytesGt", "BytesGe", + "BytesGt", + "BytesLe", + "BytesLt", + "BytesMinus", + "BytesMod", + "BytesMul", + "BytesNeq", "BytesNot", + "BytesOr", "BytesSqrt", + "BytesXor", "BytesZero", + "CompileOptions", + "Concat", + "Cond", + "Continue", + "DEFAULT_TEAL_VERSION", + "Div", + "Divw", + "DynamicScratchVar", + "Ed25519Verify", + "EnumInt", + "Eq", + "Err", + "Exp", + "Expr", + "Extract", "ExtractUint16", "ExtractUint32", "ExtractUint64", - "Log", - "While", "For", - "Break", - "Continue", - "Op", + "Ge", + "GeneratedID", + "GetBit", + "GetByte", + "Gitxn", + "GitxnExpr", + "GitxnaExpr", + "Global", + "GlobalField", + "Gt", + "Gtxn", + "GtxnExpr", + "GtxnaExpr", + "If", + "ImportScratchValue", + "InnerTxn", + "InnerTxnAction", + "InnerTxnBuilder", + "InnerTxnGroup", + "Int", + "Itob", + "Keccak256", + "LabelReference", + "Le", + "LeafExpr", + "Len", + "Log", + "Lt", + "MAX_GROUP_SIZE", + "MAX_TEAL_VERSION", + "MIN_TEAL_VERSION", + "MaybeValue", + "MethodSignature", + "MinBalance", + "Minus", + "Mod", "Mode", + "Mul", + "MultiValue", + "NUM_SLOTS", + "NaryExpr", + "Neq", + "Nonce", + "Not", + "OnComplete", + "Op", + "Or", + "Pop", + "Reject", + "Return", + "ScratchIndex", + "ScratchLoad", + "ScratchSlot", + "ScratchStackStore", + "ScratchStore", + "ScratchVar", + "Seq", + "SetBit", + "SetByte", + "Sha256", + "Sha512_256", + "ShiftLeft", + "ShiftRight", + "Sqrt", + "Subroutine", + "SubroutineCall", + "SubroutineDeclaration", + "SubroutineDefinition", + "SubroutineFnWrapper", + "Substring", + "Suffix", + "TealBlock", + "TealCompileError", "TealComponent", - "TealOp", + "TealConditionalBlock", + "TealInputError", + "TealInternalError", "TealLabel", - "TealBlock", + "TealOp", "TealSimpleBlock", - "TealConditionalBlock", - "LabelReference", - "MAX_TEAL_VERSION", - "MIN_TEAL_VERSION", - "DEFAULT_TEAL_VERSION", - "CompileOptions", - "compileTeal", "TealType", - "TealInternalError", "TealTypeError", - "TealInputError", - "TealCompileError", - "MAX_GROUP_SIZE", - "NUM_SLOTS", + "Tmpl", + "Txn", + "TxnArray", + "TxnExpr", + "TxnField", + "TxnGroup", + "TxnObject", + "TxnType", + "TxnaExpr", + "UnaryExpr", + "While", + "WideRatio", + "compileTeal", ] diff --git a/pyteal/ast/__init__.py b/pyteal/ast/__init__.py index b2bc5aa07..f554daad6 100644 --- a/pyteal/ast/__init__.py +++ b/pyteal/ast/__init__.py @@ -120,8 +120,14 @@ # misc -from .scratch import ScratchSlot, ScratchLoad, ScratchStore, ScratchStackStore -from .scratchvar import ScratchVar +from .scratch import ( + ScratchIndex, + ScratchLoad, + ScratchSlot, + ScratchStackStore, + ScratchStore, +) +from .scratchvar import DynamicScratchVar, ScratchVar from .maybe import MaybeValue from .multi import MultiValue @@ -225,10 +231,12 @@ "SubroutineDeclaration", "SubroutineCall", "SubroutineFnWrapper", - "ScratchSlot", + "ScratchIndex", "ScratchLoad", - "ScratchStore", + "ScratchSlot", "ScratchStackStore", + "ScratchStore", + "DynamicScratchVar", "ScratchVar", "MaybeValue", "MultiValue", diff --git a/pyteal/ast/multi_test.py b/pyteal/ast/multi_test.py index 665d7517b..b710a027a 100644 --- a/pyteal/ast/multi_test.py +++ b/pyteal/ast/multi_test.py @@ -1,10 +1,6 @@ -import pytest - -from .. import * from typing import List -# this is not necessary but mypy complains if it's not included -from .. import CompileOptions +from .. import * options = CompileOptions() diff --git a/pyteal/ast/scratch.py b/pyteal/ast/scratch.py index b056dd70f..ba738f64c 100644 --- a/pyteal/ast/scratch.py +++ b/pyteal/ast/scratch.py @@ -1,8 +1,8 @@ -from typing import TYPE_CHECKING +from typing import cast, TYPE_CHECKING, Optional -from ..types import TealType +from ..types import TealType, require_type from ..config import NUM_SLOTS -from ..errors import TealInputError +from ..errors import TealInputError, TealInternalError from .expr import Expr if TYPE_CHECKING: @@ -59,41 +59,98 @@ def load(self, type: TealType = TealType.anytype) -> "ScratchLoad": """ return ScratchLoad(self, type) + def index(self) -> "ScratchIndex": + return ScratchIndex(self) + def __repr__(self): return "ScratchSlot({})".format(self.id) def __str__(self): return "slot#{}".format(self.id) - def __hash__(self): - return hash(self.id) - ScratchSlot.__module__ = "pyteal" +class ScratchIndex(Expr): + def __init__(self, slot: ScratchSlot): + super().__init__() + self.slot = slot + + def __str__(self): + return "(ScratchIndex {})".format(self.slot) + + def type_of(self): + return TealType.uint64 + + def has_return(self): + return False + + def __teal__(self, options: "CompileOptions"): + from ..ir import TealOp, Op, TealBlock + + op = TealOp(self, Op.int, self.slot) + return TealBlock.FromOp(options, op) + + +ScratchIndex.__module__ = "pyteal" + + class ScratchLoad(Expr): """Expression to load a value from scratch space.""" - def __init__(self, slot: ScratchSlot, type: TealType = TealType.anytype): + def __init__( + self, + slot: ScratchSlot = None, + type: TealType = TealType.anytype, + index_expression: Expr = None, + ): """Create a new ScratchLoad expression. Args: - slot: The slot to load the value from. + slot (optional): The slot to load the value from. type (optional): The type being loaded from this slot, if known. Defaults to TealType.anytype. + index_expression (optional): As an alternative to slot, + an expression can be supplied for the slot index. """ super().__init__() + + if (slot is None) == (index_expression is None): + raise TealInputError( + "Exactly one of slot or index_expressions must be provided" + ) + + if index_expression: + if not isinstance(index_expression, Expr): + raise TealInputError( + "index_expression must be an Expr but was of type {}".format( + type(index_expression) + ) + ) + require_type(index_expression, TealType.uint64) + + if slot and not isinstance(slot, ScratchSlot): + raise TealInputError( + "cannot handle slot of type {}".format(type(self.slot)) + ) + self.slot = slot self.type = type + self.index_expression = index_expression def __str__(self): - return "(Load {})".format(self.slot) + return "(Load {})".format(self.slot if self.slot else self.index_expression) def __teal__(self, options: "CompileOptions"): from ..ir import TealOp, Op, TealBlock - op = TealOp(self, Op.load, self.slot) + if self.index_expression is not None: + op = TealOp(self, Op.loads) + return TealBlock.FromOp(options, op, self.index_expression) + + s = cast(ScratchSlot, self.slot) + op = TealOp(self, Op.load, s) return TealBlock.FromOp(options, op) def type_of(self): @@ -109,23 +166,53 @@ def has_return(self): class ScratchStore(Expr): """Expression to store a value in scratch space.""" - def __init__(self, slot: ScratchSlot, value: Expr): + def __init__( + self, slot: Optional[ScratchSlot], value: Expr, index_expression: Expr = None + ): """Create a new ScratchStore expression. Args: - slot: The slot to store the value in. + slot (optional): The slot to store the value in. value: The value to store. + index_expression (optional): As an alternative to slot, + an expression can be supplied for the slot index. """ super().__init__() + + if (slot is None) == (index_expression is None): + raise TealInternalError( + "Exactly one of slot or index_expressions must be provided" + ) + + if index_expression: + if not isinstance(index_expression, Expr): + raise TealInputError( + "index_expression must be an Expr but was of type {}".format( + type(index_expression) + ) + ) + require_type(index_expression, TealType.uint64) + self.slot = slot self.value = value + self.index_expression = index_expression def __str__(self): - return "(Store {} {})".format(self.slot, self.value) + return "(Store {} {})".format( + self.slot if self.slot else self.index_expression, self.value + ) def __teal__(self, options: "CompileOptions"): from ..ir import TealOp, Op, TealBlock + if self.index_expression is not None: + op = TealOp(self, Op.stores) + return TealBlock.FromOp(options, op, self.index_expression, self.value) + + if not isinstance(self.slot, ScratchSlot): + raise TealInternalError( + "cannot handle slot of type {}".format(type(self.slot)) + ) op = TealOp(self, Op.store, self.slot) return TealBlock.FromOp(options, op, self.value) diff --git a/pyteal/ast/scratch_test.py b/pyteal/ast/scratch_test.py index 9661ef1b5..6379efb1c 100644 --- a/pyteal/ast/scratch_test.py +++ b/pyteal/ast/scratch_test.py @@ -43,6 +43,19 @@ def test_scratch_load_default(): assert actual == expected +def test_scratch_load_index_expression(): + expr = ScratchLoad(slot=None, index_expression=Int(1337)) + assert expr.type_of() == TealType.anytype + + expected = TealSimpleBlock([TealOp(Int(1337), Op.int, 1337)]) + expected.setNextBlock(TealSimpleBlock([TealOp(None, Op.loads)])) + + actual, _ = expr.__teal__(options) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + def test_scratch_load_type(): for type in (TealType.uint64, TealType.bytes, TealType.anytype): slot = ScratchSlot() @@ -76,6 +89,29 @@ def test_scratch_store(): assert actual == expected +def test_scratch_store_index_expression(): + for value in ( + Int(1), + Bytes("test"), + App.globalGet(Bytes("key")), + If(Int(1), Int(2), Int(3)), + ): + expr = ScratchStore(slot=None, value=value, index_expression=Int(1337)) + assert expr.type_of() == TealType.none + + expected = TealSimpleBlock([TealOp(None, Op.int, 1337)]) + valueStart, valueEnd = value.__teal__(options) + expected.setNextBlock(valueStart) + + storeBlock = TealSimpleBlock([TealOp(expr, Op.stores)]) + valueEnd.setNextBlock(storeBlock) + + actual, _ = expr.__teal__(options) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + def test_scratch_stack_store(): slot = ScratchSlot() expr = ScratchStackStore(slot) @@ -106,3 +142,21 @@ def test_scratch_assign_id_invalid(): with pytest.raises(TealInputError): slot = ScratchSlot(NUM_SLOTS) + + +def test_scratch_index(): + slot = ScratchSlot() + + index = ScratchIndex(slot) + assert index.slot is slot + + assert str(index) == "(ScratchIndex " + str(slot) + ")" + + assert index.type_of() == TealType.uint64 + + assert not index.has_return() + + expected = TealSimpleBlock([TealOp(index, Op.int, slot)]) + actual, _ = index.__teal__(options) + + assert actual == expected diff --git a/pyteal/ast/scratchvar.py b/pyteal/ast/scratchvar.py index 13956097a..f194e462b 100644 --- a/pyteal/ast/scratchvar.py +++ b/pyteal/ast/scratchvar.py @@ -1,6 +1,8 @@ +from ..errors import TealInputError from ..types import TealType, require_type + from .expr import Expr -from .scratch import ScratchSlot, ScratchLoad +from .scratch import ScratchSlot, ScratchLoad, ScratchStore class ScratchVar: @@ -47,5 +49,85 @@ def load(self) -> ScratchLoad: """Load value from Scratch Space""" return self.slot.load(self.type) + def index(self) -> Expr: + return self.slot.index() + ScratchVar.__module__ = "pyteal" + + +class DynamicScratchVar(ScratchVar): + """ + Example of Dynamic Scratch space whereby the slot index is picked up from the stack: + .. code-block:: python1 + + player_score = DynamicScratchVar(TealType.uint64) + + wilt = ScratchVar(TealType.uint64, 129) + kobe = ScratchVar(TealType.uint64) + dt = ScratchVar(TealType.uint64, 131) + + seq = Seq( + player_score.set_index(wilt), + player_score.store(Int(100)), + player_score.set_index(kobe), + player_score.store(Int(81)), + player_score.set_index(dt), + player_score.store(Int(73)), + Assert(player_score.load() == Int(73)), + Assert(player_score.index() == Int(131)), + player_score.set_index(wilt), + Assert(player_score.load() == Int(100)), + Assert(player_score.index() == Int(129)), + Int(100), + ) + """ + + def __init__(self, ttype: TealType = TealType.anytype): + """Create a new DynamicScratchVar which references other ScratchVar's + + Args: + ttype (optional): The type that this variable can hold. Defaults to TealType.anytype. + """ + super().__init__(TealType.uint64) + self.dynamic_type = ttype # differentiates from ScratchVar.type + + def set_index(self, index_var: ScratchVar) -> Expr: + """Set this DynamicScratchVar to reference the provided `index_var`. + Followup `store`, `load` and `index` operations will use the provided `index_var` until + `set_index()` is called again to reset the referenced ScratchVar. + """ + if type(index_var) is not ScratchVar: + raise TealInputError( + "Only allowed to use ScratchVar objects for setting indices, but was given a {}".format( + type(index_var) + ) + ) + + return super().store(index_var.index()) + + def storage_type(self) -> TealType: + """Get the type of expressions that can be stored in this ScratchVar.""" + return self.dynamic_type + + def store(self, value: Expr) -> Expr: + """Store the value in the referenced ScratchVar.""" + require_type(value, self.dynamic_type) + return ScratchStore(slot=None, value=value, index_expression=self.index()) + + def load(self) -> ScratchLoad: + """Load the current value from the referenced ScratchVar.""" + return ScratchLoad( + slot=None, type=self.dynamic_type, index_expression=self.index() + ) + + def index(self) -> Expr: + """Get the index of the referenced ScratchVar.""" + return super().load() + + def internal_index(self) -> Expr: + """Get the index of _this_ DynamicScratchVar, as opposed to that of the referenced ScratchVar.""" + return super().index() + + +DynamicScratchVar.__module__ = "pyteal" diff --git a/pyteal/ast/scratchvar_test.py b/pyteal/ast/scratchvar_test.py index a8ddbc9e3..b757db862 100644 --- a/pyteal/ast/scratchvar_test.py +++ b/pyteal/ast/scratchvar_test.py @@ -72,6 +72,19 @@ def test_scratchvar_load(): assert actual == expected +def test_scratchvar_index(): + myvar = ScratchVar() + expr = myvar.index() + + expected = TealSimpleBlock([TealOp(expr, Op.int, myvar.slot)]) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + def test_scratchvar_assign_store(): slotId = 2 myvar = ScratchVar(TealType.uint64, slotId) @@ -104,3 +117,101 @@ def test_scratchvar_assign_load(): actual = TealBlock.NormalizeBlocks(actual) assert actual == expected + + +def test_dynamic_scratchvar_type(): + myvar_default = DynamicScratchVar() + assert myvar_default.storage_type() == TealType.anytype + assert myvar_default.store(Bytes("value")).type_of() == TealType.none + assert myvar_default.load().type_of() == TealType.anytype + + with pytest.raises(TealTypeError): + myvar_default.store(Pop(Int(1))) + + myvar_int = DynamicScratchVar(TealType.uint64) + assert myvar_int.storage_type() == TealType.uint64 + assert myvar_int.store(Int(1)).type_of() == TealType.none + assert myvar_int.load().type_of() == TealType.uint64 + + with pytest.raises(TealTypeError): + myvar_int.store(Bytes("value")) + + with pytest.raises(TealTypeError): + myvar_int.store(Pop(Int(1))) + + myvar_bytes = DynamicScratchVar(TealType.bytes) + assert myvar_bytes.storage_type() == TealType.bytes + assert myvar_bytes.store(Bytes("value")).type_of() == TealType.none + assert myvar_bytes.load().type_of() == TealType.bytes + + with pytest.raises(TealTypeError): + myvar_bytes.store(Int(0)) + + with pytest.raises(TealTypeError): + myvar_bytes.store(Pop(Int(1))) + + +def test_dynamic_scratchvar_load(): + myvar = DynamicScratchVar() + expr = myvar.load() + + expected = TealSimpleBlock( + [ + TealOp(ScratchLoad(myvar.slot), Op.load, myvar.slot), + TealOp(expr, Op.loads), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_dynamic_scratchvar_store(): + myvar = DynamicScratchVar(TealType.bytes) + arg = Bytes("value") + expr = myvar.store(arg) + + expected = TealSimpleBlock( + [ + TealOp(ScratchLoad(myvar.slot), Op.load, myvar.slot), + TealOp(arg, Op.byte, '"value"'), + TealOp(expr, Op.stores), + ] + ) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + with TealComponent.Context.ignoreExprEquality(): + assert actual == expected + + +def test_dynamic_scratchvar_index(): + myvar = DynamicScratchVar() + expr = myvar.index() + + expected = TealSimpleBlock([TealOp(expr, Op.load, myvar.slot)]) + + actual, _ = expr.__teal__(options) + actual.addIncoming() + actual = TealBlock.NormalizeBlocks(actual) + + assert actual == expected + + +def test_dynamic_scratchvar_cannot_set_index_to_another_dynamic(): + myvar = DynamicScratchVar() + myvar.load() + + regvar = ScratchVar() + myvar.set_index(regvar) + + dynvar = DynamicScratchVar() + + with pytest.raises(TealInputError): + myvar.set_index(dynvar) diff --git a/pyteal/ast/subroutine.py b/pyteal/ast/subroutine.py index 0bf688d28..35210e54b 100644 --- a/pyteal/ast/subroutine.py +++ b/pyteal/ast/subroutine.py @@ -1,19 +1,20 @@ -from typing import Callable, List, Optional, TYPE_CHECKING -from inspect import Parameter, signature +from collections import OrderedDict +from inspect import isclass, Parameter, signature +from typing import Callable, List, Optional, Set, Type, Union, TYPE_CHECKING -from ..types import TealType -from ..ir import TealOp, Op, TealBlock from ..errors import TealInputError, verifyTealVersion +from ..ir import TealOp, Op, TealBlock +from ..types import TealType + from .expr import Expr from .seq import Seq -from .scratchvar import ScratchVar +from .scratchvar import DynamicScratchVar, ScratchVar if TYPE_CHECKING: from ..compiler import CompileOptions class SubroutineDefinition: - nextSubroutineId = 0 def __init__( @@ -26,11 +27,24 @@ def __init__( self.id = SubroutineDefinition.nextSubroutineId SubroutineDefinition.nextSubroutineId += 1 + self.by_ref_args: Set[str] = set() + + self.expected_arg_types: List[Type[Union[Expr, ScratchVar]]] = [] + if not callable(implementation): raise TealInputError("Input to SubroutineDefinition is not callable") sig = signature(implementation) + annotations = getattr(implementation, "__annotations__", OrderedDict()) + + if "return" in annotations and annotations["return"] is not Expr: + raise TealInputError( + "Function has return of disallowed type {}. Only Expr is allowed".format( + annotations["return"] + ) + ) + for name, param in sig.parameters.items(): if param.kind not in ( Parameter.POSITIONAL_ONLY, @@ -49,15 +63,11 @@ def __init__( ) ) - for var, var_type in implementation.__annotations__.items(): - if var_type is not Expr: - stub = "Return" if var == "return" else ("parameter " + var) + expected_arg_type = self._validate_parameter_type(annotations, name) - raise TealInputError( - "Function has {} of disallowed type {}. Only type Expr is allowed".format( - stub, var_type - ) - ) + self.expected_arg_types.append(expected_arg_type) + if expected_arg_type is ScratchVar: + self.by_ref_args.add(name) self.implementation = implementation self.implementationParams = sig.parameters @@ -66,6 +76,38 @@ def __init__( self.declaration: Optional["SubroutineDeclaration"] = None self.__name = self.implementation.__name__ if nameStr is None else nameStr + @staticmethod + def _validate_parameter_type( + user_defined_annotations: dict, parameter_name: str + ) -> Type[Union[Expr, ScratchVar]]: + ptype = user_defined_annotations.get(parameter_name, None) + if ptype is None: + # Without a type annotation, `SubroutineDefinition` presumes an implicit `Expr` declaration rather than these alternatives: + # * Throw error requiring type annotation. + # * Defer parameter type checks until arguments provided during invocation. + # + # * Rationale: + # * Provide an upfront, best-effort type check before invocation. + # * Preserve backwards compatibility with TEAL programs written when `Expr` is the only supported annotation type. + # * `invoke` type checks provided arguments against parameter types to catch mismatches. + return Expr + else: + if not isclass(ptype): + raise TealInputError( + "Function has parameter {} of declared type {} which is not a class".format( + parameter_name, ptype + ) + ) + + if ptype not in (Expr, ScratchVar): + raise TealInputError( + "Function has parameter {} of disallowed type {}. Only the types {} are allowed".format( + parameter_name, ptype, (Expr, ScratchVar) + ) + ) + + return ptype + def getDeclaration(self) -> "SubroutineDeclaration": if self.declaration is None: # lazy evaluate subroutine @@ -78,7 +120,10 @@ def name(self) -> str: def argumentCount(self) -> int: return len(self.implementationParams) - def invoke(self, args: List[Expr]) -> "SubroutineCall": + def arguments(self) -> List[str]: + return list(self.implementationParams.keys()) + + def invoke(self, args: List[Union[Expr, ScratchVar]]) -> "SubroutineCall": if len(args) != self.argumentCount(): raise TealInputError( "Incorrect number of arguments for subroutine call. Expected {} arguments, got {}".format( @@ -87,10 +132,11 @@ def invoke(self, args: List[Expr]) -> "SubroutineCall": ) for i, arg in enumerate(args): - if not isinstance(arg, Expr): + atype = self.expected_arg_types[i] + if not isinstance(arg, atype): raise TealInputError( - "Argument at index {} of subroutine call is not a PyTeal expression: {}".format( - i, arg + "supplied argument {} at index {} had type {} but was expecting type {}".format( + arg, i, type(arg), atype ) ) @@ -136,28 +182,55 @@ def has_return(self): class SubroutineCall(Expr): - def __init__(self, subroutine: SubroutineDefinition, args: List[Expr]) -> None: + def __init__( + self, subroutine: SubroutineDefinition, args: List[Union[Expr, ScratchVar]] + ) -> None: super().__init__() self.subroutine = subroutine self.args = args for i, arg in enumerate(args): - if arg.type_of() == TealType.none: + arg_type = None + + if not isinstance(arg, (Expr, ScratchVar)): + raise TealInputError( + "Subroutine argument {} at index {} was of unexpected Python type {}".format( + arg, i, type(arg) + ) + ) + + arg_type = arg.type_of() if isinstance(arg, Expr) else arg.type + + if arg_type == TealType.none: raise TealInputError( - "Subroutine argument at index {} evaluates to TealType.none".format( - i + "Subroutine argument {} at index {} evaluates to TealType.none".format( + arg, i ) ) def __teal__(self, options: "CompileOptions"): + """ + Generate the subroutine's start and end teal blocks. + The subroutine's arguments are pushed on the stack to be picked up into local scratch variables. + There are 2 cases to consider for the pushed arg expression: + + 1. (by-value) In the case of typical arguments of type Expr, the expression ITSELF is evaluated for the stack + and will be stored in a local ScratchVar for subroutine evaluation + + 2. (by-reference) In the case of a by-reference argument of type ScratchVar, its SLOT INDEX is put on the stack + and will be stored in a local DynamicScratchVar for subroutine evaluation + """ verifyTealVersion( Op.callsub.min_version, options.version, "TEAL version too low to use SubroutineCall expression", ) + def handle_arg(arg): + return arg.index() if isinstance(arg, ScratchVar) else arg + op = TealOp(self, Op.callsub, self.subroutine) - return TealBlock.FromOp(options, op, *self.args) + return TealBlock.FromOp(options, op, *(handle_arg(x) for x in self.args)) def __str__(self): ret_str = '(SubroutineCall "' + self.subroutine.name() + '" (' @@ -248,9 +321,55 @@ def __call__(self, fnImplementation: Callable[..., Expr]) -> SubroutineFnWrapper def evaluateSubroutine(subroutine: SubroutineDefinition) -> SubroutineDeclaration: - argumentVars = [ScratchVar() for _ in range(subroutine.argumentCount())] - loadedArgs = [var.load() for var in argumentVars] + """ + Puts together the data necessary to define the code for a subroutine. + "evaluate" is used here to connote evaluating the PyTEAL AST into a SubroutineDeclaration, + but not actually placing it at call locations. The trickiest part here is managing the subroutine's arguments. + The arguments are needed for two different code-paths, and there are 2 different argument types to consider + for each of the code-paths: + + 2 Argument Usages / Code-Paths + - -------- ------ ---------- + Usage (A) for run-time: "argumentVars" --reverse--> "bodyOps" + These are "store" expressions that pick up parameters that have been pre-placed on the stack prior to subroutine invocation. + The argumentVars are stored into local scratch space to be used by the TEAL subroutine. + + Usage (B) for compile-time: "loadedArgs" + These are expressions supplied to the user-defined PyTEAL function. + The loadedArgs are invoked to by the subroutine to create a self-contained AST which will translate into a TEAL subroutine. + + In both usage cases, we need to handle + + 2 Argument Types + - -------- ----- + Type 1 (by-value): these have python type Expr + Type 2 (by-reference): these have python type ScratchVar + + Usage (A) "argumentVars" - Storing pre-placed stack variables into local scratch space: + Type 1. (by-value) use ScratchVar.store() to pick the actual value into a local scratch space + Type 2. (by-reference) ALSO use ScratchVar.store() to pick up from the stack + NOTE: SubroutineCall.__teal__() has placed the _SLOT INDEX_ on the stack so this is stored into the local scratch space + + Usage (B) "loadedArgs" - Passing through to an invoked PyTEAL subroutine AST: + Type 1. (by-value) use ScratchVar.load() to have an Expr that can be compiled in python by the PyTEAL subroutine + Type 2. (by-reference) use a DynamicScratchVar as the user will have written the PyTEAL in a way that satisfies + the ScratchVar API. I.e., the user will write `x.load()` and `x.store(val)` as opposed to just `x`. + """ + + def var_n_loaded(param): + if param in subroutine.by_ref_args: + argVar = DynamicScratchVar(TealType.anytype) + loaded = argVar + else: + argVar = ScratchVar(TealType.anytype) + loaded = argVar.load() + + return argVar, loaded + + args = subroutine.arguments() + argumentVars, loadedArgs = zip(*map(var_n_loaded, args)) if args else ([], []) + # Arg usage "B" supplied to build an AST from the user-defined PyTEAL function: subroutineBody = subroutine.implementation(*loadedArgs) if not isinstance(subroutineBody, Expr): @@ -260,6 +379,7 @@ def evaluateSubroutine(subroutine: SubroutineDefinition) -> SubroutineDeclaratio ) ) + # Arg usage "A" to be pick up and store in scratch parameters that have been placed on the stack # need to reverse order of argumentVars because the last argument will be on top of the stack bodyOps = [var.slot.store() for var in argumentVars[::-1]] bodyOps.append(subroutineBody) diff --git a/pyteal/ast/subroutine_test.py b/pyteal/ast/subroutine_test.py index c60da0907..e7f93d901 100644 --- a/pyteal/ast/subroutine_test.py +++ b/pyteal/ast/subroutine_test.py @@ -78,6 +78,76 @@ def fnWithPartialExprAnnotations(a, b: Expr) -> Expr: assert invocation.args == args +def test_subroutine_invocation_param_types(): + def fnWithNoAnnotations(a, b): + return Return() + + def fnWithExprAnnotations(a: Expr, b: Expr) -> Expr: + return Return() + + def fnWithSVAnnotations(a: ScratchVar, b: ScratchVar): + return Return() + + def fnWithMixedAnns1(a: ScratchVar, b: Expr) -> Expr: + return Return() + + def fnWithMixedAnns2(a: ScratchVar, b) -> Expr: + return Return() + + def fnWithMixedAnns3(a: Expr, b: ScratchVar): + return Return() + + sv = ScratchVar() + x = Int(42) + s = Bytes("hello") + cases = [ + ("vanilla 1", fnWithNoAnnotations, [x, s], None), + ("vanilla 2", fnWithNoAnnotations, [x, x], None), + ("vanilla no sv's allowed 1", fnWithNoAnnotations, [x, sv], TealInputError), + ("exprs 1", fnWithExprAnnotations, [x, s], None), + ("exprs 2", fnWithExprAnnotations, [x, x], None), + ("exprs no sv's allowed 1", fnWithExprAnnotations, [x, sv], TealInputError), + ("all sv's 1", fnWithSVAnnotations, [sv, sv], None), + ("all sv's but strings", fnWithSVAnnotations, [s, s], TealInputError), + ("all sv's but ints", fnWithSVAnnotations, [x, x], TealInputError), + ("mixed1 copacetic", fnWithMixedAnns1, [sv, x], None), + ("mixed1 flipped", fnWithMixedAnns1, [x, sv], TealInputError), + ("mixed1 missing the sv", fnWithMixedAnns1, [x, s], TealInputError), + ("mixed1 missing the non-sv", fnWithMixedAnns1, [sv, sv], TealInputError), + ("mixed2 copacetic", fnWithMixedAnns2, [sv, x], None), + ("mixed2 flipped", fnWithMixedAnns2, [x, sv], TealInputError), + ("mixed2 missing the sv", fnWithMixedAnns2, [x, s], TealInputError), + ("mixed2 missing the non-sv", fnWithMixedAnns2, [sv, sv], TealInputError), + ("mixed3 copacetic", fnWithMixedAnns3, [s, sv], None), + ("mixed3 flipped", fnWithMixedAnns3, [sv, x], TealInputError), + ("mixed3 missing the sv", fnWithMixedAnns3, [x, s], TealInputError), + ] + for case_name, fn, args, err in cases: + definition = SubroutineDefinition(fn, TealType.none) + assert definition.argumentCount() == len(args), case_name + assert definition.name() == fn.__name__, case_name + + if err is None: + assert len(definition.by_ref_args) == len( + [x for x in args if isinstance(x, ScratchVar)] + ), case_name + + invocation = definition.invoke(args) + assert isinstance(invocation, SubroutineCall), case_name + assert invocation.subroutine is definition, case_name + assert invocation.args == args, case_name + assert invocation.has_return() is False, case_name + + else: + try: + with pytest.raises(err): + definition.invoke(args) + except Exception as e: + assert ( + not e + ), f"EXPECTED ERROR of type {err}. encountered unexpected error during invocation case <{case_name}>: {e}" + + def test_subroutine_definition_invalid(): def fnWithDefaults(a, b=None): return Return() @@ -94,6 +164,15 @@ def fnWithNonExprReturnAnnotation(a, b) -> TealType.uint64: def fnWithNonExprParamAnnotation(a, b: TealType.uint64): return Return() + def fnWithScratchVarSubclass(a, b: DynamicScratchVar): + return Return() + + def fnReturningExprSubclass(a: ScratchVar, b: Expr) -> Return: + return Return() + + def fnWithMixedAnns4AndBytesReturn(a: Expr, b: ScratchVar) -> Bytes: + return Bytes("helo") + cases = ( (1, "TealInputError('Input to SubroutineDefinition is not callable'"), (None, "TealInputError('Input to SubroutineDefinition is not callable'"), @@ -103,27 +182,40 @@ def fnWithNonExprParamAnnotation(a, b: TealType.uint64): ), ( fnWithKeywordArgs, - "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter b with type KEYWORD_ONLY'", + "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter b with type", ), ( fnWithVariableArgs, - "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter b with type VAR_POSITIONAL'", + "TealInputError('Function has a parameter type that is not allowed in a subroutine: parameter b with type", ), ( fnWithNonExprReturnAnnotation, - "TealInputError('Function has Return of disallowed type TealType.uint64. Only type Expr is allowed'", + "Function has return of disallowed type TealType.uint64. Only Expr is allowed", ), ( fnWithNonExprParamAnnotation, - "TealInputError('Function has parameter b of disallowed type TealType.uint64. Only type Expr is allowed'", + "Function has parameter b of declared type TealType.uint64 which is not a class", + ), + ( + fnWithScratchVarSubclass, + "Function has parameter b of disallowed type ", + ), + ( + fnReturningExprSubclass, + "Function has return of disallowed type ", + ), + ( + fnWithMixedAnns4AndBytesReturn, + "Function has return of disallowed type ", ), ) - for case, msg in cases: + for fn, msg in cases: with pytest.raises(TealInputError) as e: - SubroutineDefinition(case, TealType.none) + print(f"case=[{msg}]") + SubroutineDefinition(fn, TealType.none) - assert msg in str(e), "failed for case [{}]".format(case) + assert msg in str(e), "failed for case [{}]".format(fn.__name__) def test_subroutine_declaration(): diff --git a/pyteal/compiler/compiler.py b/pyteal/compiler/compiler.py index dc1199e45..8e0a6b9c0 100644 --- a/pyteal/compiler/compiler.py +++ b/pyteal/compiler/compiler.py @@ -5,7 +5,6 @@ Expr, Return, Seq, - ScratchSlot, SubroutineDefinition, SubroutineDeclaration, ) @@ -16,7 +15,6 @@ from .flatten import flattenBlocks, flattenSubroutines from .scratchslots import assignScratchSlotsToSubroutines from .subroutines import ( - findRecursionPoints, spillLocalSlotsDuringRecursion, resolveSubroutines, ) diff --git a/requirements.txt b/requirements.txt index a6f36fa28..e3d37e35a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ black==21.7b0 -mypy==0.910 +mypy==0.931 pytest pytest-timeout py-algorand-sdk diff --git a/scripts/generate_init.py b/scripts/generate_init.py index 09dd37efe..e9a1c85e6 100644 --- a/scripts/generate_init.py +++ b/scripts/generate_init.py @@ -1,4 +1,6 @@ import argparse, os, sys, difflib +from collections import Counter + from pyteal import __all__ as static_all @@ -37,7 +39,14 @@ def generate_init_pyi() -> str: start_idx = init_contents.index(begin_flag) end_idx = init_contents.index(end_flag) - all_imports = ",\n ".join(['"{}"'.format(s) for s in static_all]) + counts = Counter(static_all) + dupes = [x for x, n in counts.items() if n > 1] + BR = "\n" + assert ( + not dupes + ), f"Aborting pyi file generation. The following duplicate imports were detected:{BR}{BR.join(dupes)}" + + all_imports = ",\n ".join(['"{}"'.format(s) for s in sorted(static_all)]) return ( pyi_template diff --git a/tests/compile_asserts.py b/tests/compile_asserts.py new file mode 100644 index 000000000..240db3821 --- /dev/null +++ b/tests/compile_asserts.py @@ -0,0 +1,73 @@ +from pathlib import Path + +from pyteal.compiler import compileTeal +from pyteal.ir import Mode + + +def compile_and_save(approval): + teal = Path.cwd() / "tests" / "teal" + compiled = compileTeal(approval(), mode=Mode.Application, version=6) + name = approval.__name__ + with open(teal / (name + ".teal"), "w") as f: + f.write(compiled) + print( + f"""Successfuly tested approval program <<{name}>> having +compiled it into {len(compiled)} characters. See the results in: +{teal} +""" + ) + return teal, name, compiled + + +def mismatch_ligature(expected, actual): + la, le = len(actual), len(expected) + mm_idx = -1 + for i in range(min(la, le)): + if expected[i] != actual[i]: + mm_idx = i + break + if mm_idx < 0: + return "" + return " " * (mm_idx) + "X" + "-" * (max(la, le) - mm_idx - 1) + + +def assert_teal_as_expected(path2actual, path2expected): + with open(path2actual, "r") as fa, open(path2expected, "r") as fe: + alines = fa.read().split("\n") + elines = fe.read().split("\n") + + assert len(elines) == len( + alines + ), f"""EXPECTED {len(elines)} lines for {path2expected} +but ACTUALLY got {len(alines)} lines in {path2actual}""" + + for i, actual in enumerate(alines): + expected = elines[i] + assert expected.startswith( + actual + ), f"""ACTUAL line in {path2actual} +LINE{i+1}: +{actual} +{mismatch_ligature(expected, actual)} +DOES NOT prefix the EXPECTED (which should have been actual + some commentary) in {path2expected}: +LINE{i+1}: +{expected} +{mismatch_ligature(expected, actual)} +""" + + +def assert_new_v_old(approve_func): + try: + teal_dir, name, compiled = compile_and_save(approve_func) + + print( + f"""Compilation resulted in TEAL program of length {len(compiled)}. + To view output SEE <{name}.teal> in ({teal_dir}) + --------------""" + ) + + path2actual = teal_dir / (name + ".teal") + path2expected = teal_dir / (name + "_expected.teal") + assert_teal_as_expected(path2actual, path2expected) + except Exception as e: + assert not e, f"failed to ASSERT NEW v OLD for {approve_func.__name__}: {e}" diff --git a/tests/pass_by_ref_test.py b/tests/pass_by_ref_test.py new file mode 100644 index 000000000..fb9a9a079 --- /dev/null +++ b/tests/pass_by_ref_test.py @@ -0,0 +1,207 @@ +from pyteal import * + +from .compile_asserts import assert_new_v_old, compile_and_save + +# Set the following True, if don't want to run pass-by-ref dependent tests +OLD_CODE_ONLY = False + +#### TESTS FOR PyTEAL THAT PREDATE PASS-BY-REF +@Subroutine(TealType.bytes) +def logcat(some_bytes, an_int): + catted = ScratchVar(TealType.bytes) + return Seq( + catted.store(Concat(some_bytes, Itob(an_int))), + Log(catted.load()), + catted.load(), + ) + + +def sub_logcat(): + return Seq( + Assert(logcat(Bytes("hello"), Int(42)) == Bytes("hello42")), + Int(1), + ) + + +@Subroutine(TealType.uint64) +def slow_fibonacci(n): + return ( + If(n <= Int(1)) + .Then(n) + .Else(slow_fibonacci(n - Int(2)) + slow_fibonacci(n - Int(1))) + ) + + +def sub_slowfib(): + return slow_fibonacci(Int(3)) + + +@Subroutine(TealType.uint64) +def fast_fibonacci(n): + i = ScratchVar(TealType.uint64) + a = ScratchVar(TealType.uint64) + b = ScratchVar(TealType.uint64) + return Seq( + a.store(Int(0)), + b.store(Int(1)), + For(i.store(Int(1)), i.load() <= n, i.store(i.load() + Int(1))).Do( + Seq( + b.store(a.load() + b.load()), + a.store(b.load() - a.load()), + ) + ), + a.load(), + ) + + +def sub_fastfib(): + return fast_fibonacci(Int(3)) + + +@Subroutine(TealType.uint64) +def recursiveIsEven(i): + return ( + If(i == Int(0)) + .Then(Int(1)) + .ElseIf(i == Int(1)) + .Then(Int(0)) + .Else(recursiveIsEven(i - Int(2))) + ) + + +def sub_even(): + return Seq( + Pop(recursiveIsEven(Int(1000))), + recursiveIsEven(Int(1001)), + ) + + +if not OLD_CODE_ONLY: + #### TESTS FOR NEW PyTEAL THAT USES PASS-BY-REF / DYNAMIC + @Subroutine(TealType.none) + def logcat_dynamic(first: ScratchVar, an_int): + return Seq( + first.store(Concat(first.load(), Itob(an_int))), + Log(first.load()), + ) + + def sub_logcat_dynamic(): + first = ScratchVar(TealType.bytes) + return Seq( + first.store(Bytes("hello")), + logcat_dynamic(first, Int(42)), + Assert(Bytes("hello42") == first.load()), + Int(1), + ) + + def wilt_the_stilt(): + player_score = DynamicScratchVar(TealType.uint64) + + wilt = ScratchVar(TealType.uint64, 129) + kobe = ScratchVar(TealType.uint64) + dt = ScratchVar(TealType.uint64, 131) + + return Seq( + player_score.set_index(wilt), + player_score.store(Int(100)), + player_score.set_index(kobe), + player_score.store(Int(81)), + player_score.set_index(dt), + player_score.store(Int(73)), + Assert(player_score.load() == Int(73)), + Assert(player_score.index() == Int(131)), + player_score.set_index(wilt), + Assert(player_score.load() == Int(100)), + Assert(player_score.index() == Int(129)), + Int(100), + ) + + @Subroutine(TealType.none) + def swap(x: ScratchVar, y: ScratchVar): + z = ScratchVar(TealType.anytype) + return Seq( + z.store(x.load()), + x.store(y.load()), + y.store(z.load()), + ) + + @Subroutine(TealType.none) + def cat(x, y): + return Pop(Concat(x, y)) + + def swapper(): + a = ScratchVar(TealType.bytes) + b = ScratchVar(TealType.bytes) + return Seq( + a.store(Bytes("hello")), + b.store(Bytes("goodbye")), + cat(a.load(), b.load()), + swap(a, b), + Assert(a.load() == Bytes("goodbye")), + Assert(b.load() == Bytes("hello")), + Int(1000), + ) + + @Subroutine(TealType.none) + def factorial(n: ScratchVar): + tmp = ScratchVar(TealType.uint64) + return ( + If(n.load() <= Int(1)) + .Then(n.store(Int(1))) + .Else( + Seq( + tmp.store(n.load() - Int(1)), + factorial(tmp), + n.store(n.load() * tmp.load()), + ) + ) + ) + + def fac_by_ref(): + n = ScratchVar(TealType.uint64) + return Seq( + n.store(Int(42)), + factorial(n), + n.load(), + ) + + @Subroutine(TealType.uint64) + def mixed_annotations(x: Expr, y: Expr, z: ScratchVar) -> Expr: + return Seq( + z.store(x), + Log(Concat(y, Bytes("="), Itob(x))), + x, + ) + + def sub_mixed(): + x = Int(42) + y = Bytes("x") + z = ScratchVar(TealType.uint64) + return mixed_annotations(x, y, z) + + +OLD_CASES = (sub_logcat, sub_slowfib, sub_fastfib, sub_even) + + +def test_old(): + for pt in OLD_CASES: + assert_new_v_old(pt) + + +if __name__ == "__main__": + test_old() + + +if not OLD_CODE_ONLY: + NEW_CASES = (sub_logcat_dynamic, swapper, wilt_the_stilt, fac_by_ref, sub_mixed) + + def test_swapper(): + compile_and_save(swapper) + + def test_new(): + for pt in NEW_CASES: + assert_new_v_old(pt) + + if __name__ == "__main__": + test_swapper() + test_new() diff --git a/tests/teal/fac_by_ref_expected.teal b/tests/teal/fac_by_ref_expected.teal new file mode 100644 index 000000000..c08bbef0f --- /dev/null +++ b/tests/teal/fac_by_ref_expected.teal @@ -0,0 +1,41 @@ +#pragma version 6 // Case n = 1 Case n = 2 +int 42 // >1 >2 +store 0 // 0:1 0:2 +int 0 // >0 >0 +callsub factorial_0 // CALL CALL +load 0 // >1 >2 +return // RET(1!==1) RET(2!==1*2) + +// factorial +factorial_0: // *>0 *>0 *>2,0,1,2 +store 1 // 1: 0 1: 0 1: 2 +load 1 // >0 >0 *>2,0,1,2 +loads // >1 >2 *>2,0,1,1 +int 1 // >1,1 >2,1 *>2,0,1,1,1 +<= // *>1 *>2,0 *>2,0,1,1 +bnz factorial_0_l2 // BRANCH *>2 BRANCH +load 1 // *>2,0 +loads // *>2,2 +int 1 // *>2,2,1 +- // *>2,1 +store 2 // 2: 1 +int 2 // *>2,2 +load 1 // *>2,2,0 +load 2 // *>2,2,0,1 +uncover 2 // *>2,0,1,2 +callsub factorial_0 // CALL SRET >2,0,1 +store 2 // 2: 1 +store 1 // 1: 0 +load 1 // >2,0 +load 1 // >2,0,0 +loads // >2,0,2 +load 2 // >2,0,2,1 +* // >2,0,2 +stores // 0: 2 +b factorial_0_l3 // BRANCH +factorial_0_l2: // | *>2,0,1 +load 1 // *>0 | *>2,0,1,2 +int 1 // *>0,1 | *>2,0,1,2,1 +stores // 0: 1 | 2: 1 +factorial_0_l3: // >2 +retsub // SRET SRET>2 SRET*>2,0,1 \ No newline at end of file diff --git a/tests/teal/sub_even_expected.teal b/tests/teal/sub_even_expected.teal new file mode 100644 index 000000000..8cd15e13b --- /dev/null +++ b/tests/teal/sub_even_expected.teal @@ -0,0 +1,35 @@ +#pragma version 6 +int 1000 +callsub recursiveIsEven_0 +pop +int 1001 +callsub recursiveIsEven_0 +return + +// recursiveIsEven +recursiveIsEven_0: +store 0 +load 0 +int 0 +== +bnz recursiveIsEven_0_l4 +load 0 +int 1 +== +bnz recursiveIsEven_0_l3 +load 0 +int 2 +- +load 0 +swap +callsub recursiveIsEven_0 +swap +store 0 +b recursiveIsEven_0_l5 +recursiveIsEven_0_l3: +int 0 +b recursiveIsEven_0_l5 +recursiveIsEven_0_l4: +int 1 +recursiveIsEven_0_l5: +retsub \ No newline at end of file diff --git a/tests/teal/sub_fastfib_expected.teal b/tests/teal/sub_fastfib_expected.teal new file mode 100644 index 000000000..dea466e36 --- /dev/null +++ b/tests/teal/sub_fastfib_expected.teal @@ -0,0 +1,35 @@ +#pragma version 6 +int 3 +callsub fastfibonacci_0 +return + +// fast_fibonacci +fastfibonacci_0: +store 0 +int 0 +store 2 +int 1 +store 3 +int 1 +store 1 +fastfibonacci_0_l1: +load 1 +load 0 +<= +bz fastfibonacci_0_l3 +load 2 +load 3 ++ +store 3 +load 3 +load 2 +- +store 2 +load 1 +int 1 ++ +store 1 +b fastfibonacci_0_l1 +fastfibonacci_0_l3: +load 2 +retsub \ No newline at end of file diff --git a/tests/teal/sub_logcat_dynamic_expected.teal b/tests/teal/sub_logcat_dynamic_expected.teal new file mode 100644 index 000000000..87a430bc3 --- /dev/null +++ b/tests/teal/sub_logcat_dynamic_expected.teal @@ -0,0 +1,28 @@ +#pragma version 6 +byte "hello" +store 0 // 0: "hello" +int 0 +int 42 // >@0,42 +callsub logcatdynamic_0 // <> +byte "hello42" // >"hello42" +load 0 // >"hello42","hello42" +== // >1 +assert // <> +int 1 // >1 +return // < + +// logcat_dynamic +logcatdynamic_0: // >@0,42 +store 2 // 2: 42 +store 1 // 1: @0 +load 1 +load 1 // >@0,@0 +loads // >@0,"hello" +load 2 // >@0,"hello",42 +itob // >@0,"hello","42" +concat // >@0,"hello42" +stores // 0: "hello42" +load 1 // >@0 +loads // >"hello42" +log +retsub \ No newline at end of file diff --git a/tests/teal/sub_logcat_expected.teal b/tests/teal/sub_logcat_expected.teal new file mode 100644 index 000000000..8acb5c4df --- /dev/null +++ b/tests/teal/sub_logcat_expected.teal @@ -0,0 +1,23 @@ +#pragma version 6 +byte "hello" // >"hello" +int 42 // >"hello",42 +callsub logcat_0 // >"hello42" +byte "hello42" // >"hello42","hello42" +== // >1 +assert // <> +int 1 // >1 +return // <> + +// logcat +logcat_0: // >"hello",42 +store 1 // 1: 42 +store 0 // 0: "hello" +load 0 // >"hello" +load 1 // >"hello",42 +itob // >"hello","42" +concat // >"hello42" +store 2 // 2: "hello42" +load 2 // >"hello42" +log +load 2 // >"hello42" +retsub \ No newline at end of file diff --git a/tests/teal/sub_mixed_expected.teal b/tests/teal/sub_mixed_expected.teal new file mode 100644 index 000000000..98934bb83 --- /dev/null +++ b/tests/teal/sub_mixed_expected.teal @@ -0,0 +1,24 @@ +#pragma version 6 +int 42 // >42 +byte "x" // >42,"x" +int 0 // >42,"x",0 +callsub mixedannotations_0 // >42 +return // <> + +// mixed_annotations +mixedannotations_0: // >42,"x",0 +store 3 // 3:0 +store 2 // 2:"x" +store 1 // 1:42 +load 3 // >0 +load 1 // >0,42 +stores // 0:42 +load 2 // >"x" +byte "=" // >"x","=" +concat // >"x=" +load 1 // >"x=",42 +itob // >"x=","42" +concat // >"x=42" +log // LOG +load 1 // >42 +retsub \ No newline at end of file diff --git a/tests/teal/sub_slowfib_expected.teal b/tests/teal/sub_slowfib_expected.teal new file mode 100644 index 000000000..53bf3cd0e --- /dev/null +++ b/tests/teal/sub_slowfib_expected.teal @@ -0,0 +1,34 @@ +#pragma version 6 +int 3 +callsub slowfibonacci_0 +return + +// slow_fibonacci +slowfibonacci_0: +store 0 +load 0 +int 1 +<= +bnz slowfibonacci_0_l2 +load 0 +int 2 +- +load 0 +swap +callsub slowfibonacci_0 +swap +store 0 +load 0 +int 1 +- +load 0 +swap +callsub slowfibonacci_0 +swap +store 0 ++ +b slowfibonacci_0_l3 +slowfibonacci_0_l2: +load 0 +slowfibonacci_0_l3: +retsub \ No newline at end of file diff --git a/tests/teal/swapper_expected.teal b/tests/teal/swapper_expected.teal new file mode 100644 index 000000000..00a996f15 --- /dev/null +++ b/tests/teal/swapper_expected.teal @@ -0,0 +1,47 @@ +#pragma version 6 +byte "hello" +store 5 // 5: hello // x +byte "goodbye" +store 6 // 6: goodbye // y +load 5 +load 6 +callsub cat_1 // <> +int 5 +int 6 +callsub swap_0 // >5,6 +load 5 // >goodbye +byte "goodbye" +== +assert // <> +load 6 // >hello +byte "hello" +== +assert // <> +int 1000 // >1000 +return // <> + +// swap +swap_0: +store 1 // 1: 6 // @y +store 0 // 0: 5 // @x +load 0 // >5 +loads // >hello +store 2 // 2: hello // z +load 0 // >5 +load 1 // >5,6 +loads // >5,goodbye +stores // 5: goodbye +load 1 // >6 +load 2 // >6,hello +stores // 6: hello +retsub + +// cat +cat_1: +store 4 // 4: goodbye +store 3 // 3: hello +load 3 // >hello +load 4 // >hello,goodbye +concat // >hellogoodbye +pop // > +retsub \ No newline at end of file diff --git a/tests/teal/wilt_the_stilt_expected.teal b/tests/teal/wilt_the_stilt_expected.teal new file mode 100644 index 000000000..28d339350 --- /dev/null +++ b/tests/teal/wilt_the_stilt_expected.teal @@ -0,0 +1,38 @@ +#pragma version 6 +int 129 +store 0 // 0: @129 // pointer to wilt's address +load 0 +int 100 +stores // 129: 100 // set wilt's value +int 1 +store 0 // 0: @1 // pointer to kobe's address (compiler assigned) +load 0 +int 81 +stores // 1: 81 // set kobe's value +int 131 +store 0 // 0: @131 // pointer to dt's address +load 0 +int 73 +stores // 131: 73 // set dt's value +load 0 +loads +int 73 // >73,73 +== // >1 +assert // <> +load 0 +int 131 // >131,131 +== // >1 +assert // <> +int 129 +store 0 // 0: @129 +load 0 +loads +int 100 // >100,100 +== // >1 +assert // <> +load 0 +int 129 // >129,129 +== // >1 +assert // <> +int 100 +return \ No newline at end of file