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

loop #95

Merged
merged 31 commits into from
Aug 23, 2021
Merged

loop #95

merged 31 commits into from
Aug 23, 2021

Conversation

shiqizng
Copy link
Contributor

@shiqizng shiqizng commented Aug 10, 2021

This PR adds While and For loops to Pyteal.

@jasonpaulos jasonpaulos linked an issue Aug 11, 2021 that may be closed by this pull request
@shiqizng shiqizng changed the title [WIP] loop loop Aug 18, 2021
@shiqizng shiqizng marked this pull request as ready for review August 18, 2021 15:41
Copy link
Member

@jasonpaulos jasonpaulos left a comment

Choose a reason for hiding this comment

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

Nice work, nested loops and continue/break seem to be working as expected! My primary concern is about the sorting function, which I explain more below.

return "(While {} {})".format(self.cond, self.doBlock)

def type_of(self):
if self.doBlock is None:
Copy link
Member

Choose a reason for hiding this comment

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

type_of is meant to indicate which type, if any, the expression pushes to the stack when it finishes, so I think this should return TealType.none instead.

return self.doBlock.type_of()

def Do(self, doBlock: Seq):
self.doBlock = doBlock
Copy link
Member

Choose a reason for hiding this comment

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

We don't want the doBlock to end with additional things on the stack (e.g. While(cond).Do(Int(1)) should be invalid), so could you add the check require_type(doBlock.type_of(), TealType.none) here?

Also, could you change the type annotation for doBlock from Seq to Expr? I expect most of the time a Seq will be used, but other expressions can be used as well.

if i == 0:
S.insert(0, m)
else:
if type(n) == TealConditionalBlock:
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason for the extra complexity here? I tested locally and this simpler version worked just as well, plus it is nearly identical to the old code generation when loops are not present:

S = [start]
order = []
visited = set() # I changed visited to a set to be more efficient

while len(S) != 0:
    n = S.pop()

    if id(n) in visited:
        continue

    S += n.getOutgoing()

    order.append(n)
    visited.add(id(n))

Copy link
Contributor

Choose a reason for hiding this comment

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

^ The previous version should be good here, even if For, While introduce cycles to the graph...


order.append(n)
visited.append(n)

Copy link
Member

Choose a reason for hiding this comment

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

Also, I realized there is a potential bug when a non-topological sort is used for this function. If there is a "diamond" graph structure, i.e. a diverging branch which later converges, like so:

    start
   /      \
true      false
   \     /
     end

then one potential ordering is [start, true, end, false], which translates to the TEAL code:

start:
...
bz false
true:
...
end:
...
false:
...
b end

This is a problem because if end does not contain a return op, the ops from false will be executed after end finishes.

To fix this, sortBlocks should take an additional argument, end, which is the ending node of the graph. Then, after sorting the blocks into the list order, we need to make the end block the last element in the list. This guarantees no code can accidentally execute after it.

Adding this code before the return statement should do it:

endIndex = -1
for i, block in enumerate(order):
    if end is block:
        endIndex = i
        break

if endIndex == -1:
    raise TealInternalError("End block not present")

order.pop(endIndex)
order.append(end)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

you're correct. I simplified the algorithm as suggested.


def __eq__(self, other: object) -> bool:
# check for loop
if self in self.blocks:
Copy link
Member

Choose a reason for hiding this comment

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

I don't love that these functions need to modify the object in order to work, but I can't think of a better solution right now -- I might make a follow up PR if I can think of something better.

In any case, self.blocks and self.visited do not need to be lists, since they only ever contain self or nothing. A cleaner solution would be to make a single self.visited boolean attribute, and the functions can set this to true/false to indicate whether self has been visited.

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.

Looking good, just had some small questions and comments - I think we might want to run some formatter (e.g. black) before pushing to make spacing/line breaks more consistent and delete any commented out code blocks.


def type_of(self):
if str(self.doBlock) == str(Seq([Int(0)])):
raise TealCompileError("For expression must have a thenBranch", self)
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a test for this branch? Also is this just checking if the doBlock is False or 0 (and if so do we just want to check for None here)?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, it should be checking for None.

pyteal/ast/for_test.py Show resolved Hide resolved
pyteal/compiler/compiler_test.py Show resolved Hide resolved
&&
b l3
l2:
bz l2
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these auto-generated from the pyteal file?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, it seems Shiqi updated split.teal and dutch_auction.teal with new generated version.

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.

Looks good, I left some minor comments here and there for now.

pyteal/ast/for_.py Outdated Show resolved Hide resolved
pyteal/ast/for_.py Outdated Show resolved Hide resolved
pyteal/ast/while_.py Outdated Show resolved Hide resolved
pyteal/ast/for_.py Outdated Show resolved Hide resolved
if i == 0:
S.insert(0, m)
else:
if type(n) == TealConditionalBlock:
Copy link
Contributor

Choose a reason for hiding this comment

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

^ The previous version should be good here, even if For, While introduce cycles to the graph...

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.

The new formatting is much easier to read -- just one question about passing variables within CompileOptions

pyteal/ast/break_test.py Show resolved Hide resolved
pyteal/compiler/compiler.py Show resolved Hide resolved
Copy link
Member

@jasonpaulos jasonpaulos left a comment

Choose a reason for hiding this comment

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

After these are addressed, this looks good to merge

expr = While(items[0]).Do(items[1])
actual, _ = expr.__teal__(options)

options.currentLoop = expr
Copy link
Member

Choose a reason for hiding this comment

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

The code from this line downward does not seem to be needed, since it doesn't check anything new.

A more appropriate test would probably be to set options.currentLoop = expr, then do start, _ = items[1].__teal__(options) and verify options.breakBlocks contains only start

expr = While(items[0]).Do(items[1])
actual, _ = expr.__teal__(options)

options.currentLoop = expr
Copy link
Member

Choose a reason for hiding this comment

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

The above comment applies here too.


for block in options.continueBlocks:
block.setNextBlock(stepStart)
doEnd.setNextBlock(block)
Copy link
Member

Choose a reason for hiding this comment

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

I dont't think this line should be here?

condEnd.setNextBlock(branchBlock)
condStart.addIncoming(doStart)

startEnd.nextBlock = condStart
Copy link
Member

Choose a reason for hiding this comment

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

condStart.addIncoming(doStart) seems unnecessary here, since compileTeal invokes addIncoming on the start block of the entire AST.

Also, can you use setNextBlock() instead of directly setting startEnd.nextBlock?


def type_of(self):
if self.doBlock is None:
raise TealCompileError("For expression must have a thenBranch", self)
Copy link
Member

Choose a reason for hiding this comment

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

nit: some places refer to the doBlock as thenBranch, can you update these?

expr.__teal__(options)


def test_while():
Copy link
Member

Choose a reason for hiding this comment

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

Again, this test is really good, and this file could use 2 more versions of it which test continue and break.

Seq(
[
For(
i.store(Int(0)),
Copy link
Member

Choose a reason for hiding this comment

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

This should be j.store(Int(0))

i.store(Int(0)),
j.load() < Int(4),
j.store(j.load() + Int(2)),
).Do(Seq([j.store(Int(1))]))
Copy link
Member

Choose a reason for hiding this comment

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

Could you change the body to App.globalPut(Itob(j.load()), j.load() * Int(2)) or similar so this would not be an infinite loop?


endIndex = -1
for i, block in enumerate(order):
if block == end:
Copy link
Member

Choose a reason for hiding this comment

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

Can you check if block is end instead? This is a slightly stronger check, since we want to make sure we find the end block exactly, not a different block which happens to have the same ops as the end block, which is what == checks.

@@ -10,6 +10,8 @@ class TealSimpleBlock(TealBlock):
def __init__(self, ops: List[TealOp]) -> None:
super().__init__(ops)
self.nextBlock: Optional[TealBlock] = None
self.blocks: List[TealBlock] = []
Copy link
Member

Choose a reason for hiding this comment

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

This variable isn't needed anymore

Copy link
Member

@jasonpaulos jasonpaulos left a comment

Choose a reason for hiding this comment

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

Looks good!

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.

Should be good to go

@shiqizng shiqizng merged commit 8ad44a4 into master Aug 23, 2021
@jasonpaulos jasonpaulos mentioned this pull request Aug 31, 2021
@ahangsu ahangsu deleted the feature/loop branch March 2, 2022 14:55
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 looping
4 participants