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

Add subroutine support #99

Merged
merged 17 commits into from
Aug 27, 2021
Merged

Add subroutine support #99

merged 17 commits into from
Aug 27, 2021

Conversation

jasonpaulos
Copy link
Contributor

@jasonpaulos jasonpaulos commented Aug 19, 2021

This PR implements subroutines in PyTeal. Subroutines may be declared as functions with the Subroutine function decorator, like so:

@Subroutine(TealType.uint64)
def isEven(i: Expr) -> Expr:
    return i % Int(2) == Int(0)

program = Seq([
    Assert(isEven(Int(10))),
    Approve(),
])
tealSrc = compileTeal(program, mode=Mode.Application, version=4)

Subroutine functions may accept any number of arguments, but the arguments must not be keyword-only Python arguments, must not have default values, and must not be variable-length (e.g. *args). The argument in the Subroutine decorator function is the return type of the subroutine, so in this example the subroutine must return an expression that evaluates to TealType.uint64. Subroutine functions are executed only once during the compilation process in order to extract their AST.

Since there can now be multiple "functions" in a program, I changed how the Return expression works. Now, Return means return from the current subroutine (retsub op), or if the main routine is executing, end the program with the return op. However, if you'd like to exit the program from within a subroutine, I've introduced Approve() and Reject() expressions which always evaluate to the ops int 1; return and int 0; return, respectively.

In the compiler, each subroutine is compiled independently, then they are all combined into a single series of TealComponents with the flattenSubroutines function. I had to make changes to the way scratch slots and labels are resolved in order to make this work.

Closes #71

@jasonpaulos jasonpaulos marked this pull request as ready for review August 25, 2021 16:19
def __str__(self):
return "slot#{}".format(self.id)

def __eq__(self, other):
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had to remove this so that ScratchSlot(1) != ScratchSlot(1), which is required in order to detect duplicate requested slot IDs when I add all slots to a set.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds fine to me -- I think I previously checked for the id of the object e.g. id(slot1) == id(slot2) for detecting duplicates, but with this change it seems like we can remove this check

@@ -0,0 +1,20 @@
class LabelReference:
Copy link
Contributor Author

@jasonpaulos jasonpaulos Aug 25, 2021

Choose a reason for hiding this comment

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

This class is basically a pass-by-reference string. It was added so that flattenSubroutines can easily prefix all the labels in a given subroutine in order to differentiate them.

Copy link
Contributor

@algochoi algochoi left a comment

Choose a reason for hiding this comment

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

It looking good, I really like the recursive test coverage as well. Just minor comments/questions.

# from the main routine, and "subX_" (the subroutine label) to the
# ones from each subroutine

mainRoutine = subroutineMapping[None]
Copy link
Contributor

Choose a reason for hiding this comment

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

This is an interesting trick, using None as a key for a dict.

pyteal/ast/subroutine.py Outdated Show resolved Hide resolved
pyteal/compiler/scratchslots.py Show resolved Hide resolved
def __str__(self):
return "slot#{}".format(self.id)

def __eq__(self, other):
Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds fine to me -- I think I previously checked for the id of the object e.g. id(slot1) == id(slot2) for detecting duplicates, but with this change it seems like we can remove this check

pyteal/ast/if_.py Show resolved Hide resolved
pyteal/ast/return_.py Show resolved Hide resolved
examples/signature/atomic_swap.teal Outdated Show resolved Hide resolved
pyteal/ast/binaryexpr.py Outdated Show resolved Hide resolved
pyteal/ast/cond.py Show resolved Hide resolved
pyteal/ast/return_.py Outdated Show resolved Hide resolved
pyteal/ast/subroutine.py Outdated Show resolved Hide resolved
pyteal/compiler/compiler.py Show resolved Hide resolved
pyteal/compiler/compiler.py Show resolved Hide resolved
] = dict()
subroutineGraph: Dict[SubroutineDefinition, Set[SubroutineDefinition]] = dict()
subroutineBlocks: Dict[Optional[SubroutineDefinition], TealBlock] = dict()
compileSubroutine(
Copy link
Contributor

Choose a reason for hiding this comment

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

maybe this should be named compile() since it's not a compiler specific to subroutines only?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have mostly been treating the main routine as a special subroutine which gets invoked first (and doesn't have a SubroutineDefinition object). So I think compileSubroutine is an ok name. Would compileRoutine be better?

The only reason I want to avoid compile() is because that's already a builtin Python function: https://docs.python.org/3/library/functions.html#compile

Copy link
Contributor

Choose a reason for hiding this comment

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

in that case, compileSubroutine is better.

@shiqizng
Copy link
Contributor

shiqizng commented Aug 26, 2021

Jason, can you please elaborate on what has_return() do? It's not always clear to me why it's True/False in some cases. For example, in If(), why is it False when there's no else branch? if self.elseBranch is None: return False

@jasonpaulos
Copy link
Contributor Author

jasonpaulos commented Aug 27, 2021

Jason, can you please elaborate what has_return() do? It's not always clear to me why it's True/False in some cases. For example, in If(), why is it False when there's no else branch? if self.elseBranch is None: return False

Yes, I have updated the docstring to be more specific, and added comments to each complex class which overrides it. The docstring is now says "Check if this expression always returns from the current subroutine or program."

That check is needed in order to insert retsub and return ops at the end of a subroutine/main routine if they are not already present. See here:

if not ast.has_return():

I thought this was a better solution than requiring every subroutine and main routine to end with a Return statement.

Copy link
Contributor

@ahangsu ahangsu left a comment

Choose a reason for hiding this comment

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

LGTM

self.continueBlocksStack[-1].append(block)

def exitLoop(self) -> Tuple[List[TealSimpleBlock], List[TealSimpleBlock]]:
return (self.breakBlocksStack.pop(), self.continueBlocksStack.pop())
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can remove redundant parentheses, i.e., return self.breakBlocksStack.pop(), self.continueBlocksStack.pop() should work.

if self.value is not None:
args.append(self.value)

return TealBlock.FromOp(options, TealOp(self, op), *args)
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: can be done as a one-liner:

return TealBlock.FromOp(options, TealOp(self, op), 
    *([] if self.value is None else [self.value])
)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, but I think I'll stick with the current code since it's a little easier to see what's happening

pyteal/compiler/compiler.py Outdated Show resolved Hide resolved
@algochoi algochoi self-requested a review August 27, 2021 18:37
Copy link
Contributor

@algochoi algochoi left a comment

Choose a reason for hiding this comment

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

Changes look good to me 👍

@ahangsu ahangsu self-requested a review August 27, 2021 18:39
Copy link
Contributor

@ahangsu ahangsu left a comment

Choose a reason for hiding this comment

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

LGTM

@jasonpaulos jasonpaulos merged commit d922247 into master Aug 27, 2021
@jasonpaulos jasonpaulos deleted the jason/subroutines branch August 27, 2021 22:24
@jasonpaulos jasonpaulos mentioned this pull request Aug 31, 2021
@tzaffi tzaffi mentioned this pull request Apr 13, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add subroutines
4 participants