Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

method pseudo-op support for ABI methods #153

Merged
merged 18 commits into from
Jan 3, 2022
Merged
2 changes: 2 additions & 0 deletions pyteal/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from .addr import Addr
from .bytes import Bytes
from .int import Int, EnumInt
from .method import Method

# properties
from .arg import Arg
Expand Down Expand Up @@ -126,6 +127,7 @@
"Bytes",
"Int",
"EnumInt",
"Method",
"Arg",
"TxnType",
"TxnField",
Expand Down
43 changes: 43 additions & 0 deletions pyteal/ast/method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from typing import TYPE_CHECKING
from pyteal.errors import TealInputError

from pyteal.types import TealType

from ..types import TealType
from ..ir import TealOp, Op, TealBlock
from .leafexpr import LeafExpr

if TYPE_CHECKING:
from ..compiler import CompileOptions


class Method(LeafExpr):
"""An expression that represents an ABI method selector"""

def __init__(self, methodName: str) -> None:
"""Create a new method selector for ABI method call.

Args:
methodName: A string containing a valid ABI method signature
"""
super().__init__()
if type(methodName) is not str:
raise TealInputError(
"invalid input type {} to Method".format(type(methodName))
)
elif len(methodName) == 0:
raise TealInputError("invalid input empty string to Method")
self.methodName = methodName

def __teal__(self, options: "CompileOptions"):
op = TealOp(self, Op.method, self.methodName)
return TealBlock.FromOp(options, op)

def __str__(self) -> str:
return "(method: {})".format(self.methodName)

def type_of(self) -> TealType:
return TealType.bytes


Method.__module__ = "pyteal"
25 changes: 25 additions & 0 deletions pyteal/ast/method_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import pytest

from pyteal.ast.method import Method

from .. import *


def test_method():
expr = Method("add(uint64,uint64)uint64")
assert expr.type_of() == TealType.bytes

expected = TealSimpleBlock([TealOp(expr, Op.method, "add(uint64,uint64)uint64")])
actual, _ = expr.__teal__(CompileOptions())
assert expected == actual


def test_method_invalid():
with pytest.raises(TealInputError):
Method(114514)

with pytest.raises(TealInputError):
Method(["m0()void", "m1()uint64"])

with pytest.raises(TealInputError):
Method("")
20 changes: 15 additions & 5 deletions pyteal/ast/subroutine.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
from .scratchvar import ScratchVar

if TYPE_CHECKING:
from ..ir import TealSimpleBlock
from ..compiler import CompileOptions


Expand All @@ -19,7 +18,10 @@ class SubroutineDefinition:
nextSubroutineId = 0

def __init__(
self, implementation: Callable[..., Expr], returnType: TealType
self,
implementation: Callable[..., Expr],
returnType: TealType,
methodSignature: str = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm less sure about the changes to this file, since not every subroutine will represent a method.

What if you changed methodSignature to just name? I.e. if you provide the name arg, then it would override whatever the function's __name__ is.

@barnjamin thoughts on this?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had thought we'd be able to infer the method signature from the method definition, including name and types it accepts. That would mean defining the ABI types with accessors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started something here https://github.com/barnjamin/pyteal-utils/blob/main/pytealutils/abi/types.py but maybe this kind of thing belongs in PyTEAL proper.

We should definitely have some representation of them so decoding from the over-the-wire format is painless

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up just adding the ABI types to utils for now using the py-sdk to test encoding/decoding: https://github.com/algorand/pyteal-utils/blob/a5f35c9e217b2c6e259b8a725bb988ec631e6fc8/pytealutils/abi/test_fixed_point.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just changed methodSignature name to nameStr, and override function's __name__ on initialization, but I still feel this needs to be polished as an implementation, and should be modified after ABI in PyTeal is introduced.

I would like to keep this thread open for more discussion on the design of subroutine (in combination with ABI methods).

) -> None:
super().__init__()
self.id = SubroutineDefinition.nextSubroutineId
Expand Down Expand Up @@ -53,6 +55,7 @@ def __init__(
self.returnType = returnType

self.declaration: Optional["SubroutineDeclaration"] = None
self.methodSignature = methodSignature

def getDeclaration(self) -> "SubroutineDeclaration":
if self.declaration is None:
Expand All @@ -61,7 +64,11 @@ def getDeclaration(self) -> "SubroutineDeclaration":
return self.declaration

def name(self) -> str:
return self.implementation.__name__
return (
self.implementation.__name__
if self.methodSignature is None
else self.methodSignature
)

def argumentCount(self) -> int:
return len(self.implementationParams)
Expand Down Expand Up @@ -181,17 +188,20 @@ def mySubroutine(a: Expr, b: Expr) -> Expr:
])
"""

def __init__(self, returnType: TealType) -> None:
def __init__(self, returnType: TealType, methodSignature: str = None) -> None:
"""Define a new subroutine with the given return type.

Args:
returnType: The type that the return value of this subroutine must conform to.
TealType.none indicates that this subroutine does not return any value.
"""
self.returnType = returnType
self.methodSignature = methodSignature

def __call__(self, fnImplementation: Callable[..., Expr]) -> Callable[..., Expr]:
subroutine = SubroutineDefinition(fnImplementation, self.returnType)
subroutine = SubroutineDefinition(
fnImplementation, self.returnType, self.methodSignature
)

@wraps(fnImplementation)
def subroutineCall(*args: Expr, **kwargs) -> Expr:
Expand Down
1 change: 1 addition & 0 deletions pyteal/ir/ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ def min_version(self) -> int:
bytec_3 = OpType("bytec_3", Mode.Signature | Mode.Application, 2)
byte = OpType("byte", Mode.Signature | Mode.Application, 2)
addr = OpType("addr", Mode.Signature | Mode.Application, 2)
method = OpType("method", Mode.Signature | Mode.Application, 2)
arg = OpType("arg", Mode.Signature, 2)
txn = OpType("txn", Mode.Signature | Mode.Application, 2)
global_ = OpType("global", Mode.Signature | Mode.Application, 2)
Expand Down
2 changes: 1 addition & 1 deletion pyteal/ir/tealop.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import cast, Union, List, Optional, TYPE_CHECKING
from typing import Union, List, Optional, TYPE_CHECKING

from .tealcomponent import TealComponent
from .labelref import LabelReference
Expand Down