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

Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
080e9ee
adding while loop
shiqizng Aug 6, 2021
3278f9f
basic while loop working
shiqizng Aug 6, 2021
e32090a
split failing
shiqizng Aug 9, 2021
4a504d1
compile tests work
shiqizng Aug 10, 2021
ddf4596
fix sort tests
shiqizng Aug 10, 2021
f220493
test fix and added for
shiqizng Aug 10, 2021
a228bc5
rm lines
shiqizng Aug 10, 2021
9300d08
Merge branch 'feature/loop' of https://github.com/algorand/pyteal int…
shiqizng Aug 10, 2021
b35e40f
revert teal simple block change
shiqizng Aug 10, 2021
f981278
update if check
shiqizng Aug 10, 2021
b240d45
test updates
shiqizng Aug 10, 2021
8509293
added tests, fixed nested loop
shiqizng Aug 11, 2021
572d6d7
disable slot validation
shiqizng Aug 11, 2021
a0d67cc
fix tests
shiqizng Aug 13, 2021
1e67815
index on loop: a0d67cc fix tests
shiqizng Aug 13, 2021
459cb87
WIP on loop: a0d67cc fix tests
shiqizng Aug 13, 2021
ac83095
Revert "fix tests"
shiqizng Aug 13, 2021
cdd08a9
sort test fix
shiqizng Aug 13, 2021
03d0c69
fix mypy tests
shiqizng Aug 13, 2021
7365703
fix unit tests
shiqizng Aug 16, 2021
e24c8c9
fix unit tests
shiqizng Aug 16, 2021
62439ec
add continue,break
shiqizng Aug 18, 2021
d6fc42c
add break, continue
shiqizng Aug 18, 2021
6eb6388
add new lines
shiqizng Aug 18, 2021
247f6d5
addressing reviews
shiqizng Aug 20, 2021
105374c
Merge commit 'formatting' into feature/loop
shiqizng Aug 20, 2021
13e0f0d
formatting
shiqizng Aug 20, 2021
6d17e63
Merge branch 'review' into feature/loop
shiqizng Aug 20, 2021
e05581e
error fix
shiqizng Aug 20, 2021
23c93b0
format
shiqizng Aug 20, 2021
48e46f8
addressing reviews
shiqizng Aug 23, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions pyteal/ast/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@
from .cond import Cond
from .seq import Seq
from .assert_ import Assert
from .while_ import While
from .for_ import For
from .break_ import Break
from .continue_ import Continue


# misc
from .scratch import ScratchSlot, ScratchLoad, ScratchStore, ScratchStackStore
Expand Down Expand Up @@ -200,4 +205,8 @@
"BytesGe",
"BytesNot",
"BytesZero",
"While",
"For",
"Break",
"Continue",
]
40 changes: 40 additions & 0 deletions pyteal/ast/break_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import TYPE_CHECKING

from ..types import TealType
from ..errors import TealCompileError
from .expr import Expr
from ..ir import TealSimpleBlock


if TYPE_CHECKING:
from ..compiler import CompileOptions


class Break(Expr):
"""A break expression"""

def __init__(self) -> None:
"""Create a new break expression.

This operation is only permitted in a loop.

"""
super().__init__()

def __str__(self) -> str:
return "break"

def __teal__(self, options: "CompileOptions"):
if options.currentLoop is None:
raise TealCompileError("break is only allowed in a loop", self)

start = TealSimpleBlock([])
options.breakBlocks.append(start)

return start, start

def type_of(self):
return TealType.none


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

from .. import *

# this is not necessary but mypy complains if it's not included
from .. import CompileOptions

options = CompileOptions()


def test_break_fail():

with pytest.raises(TealCompileError):
Break().__teal__(options)

with pytest.raises(TealCompileError):
If(Int(1), Break()).__teal__(options)

with pytest.raises(TealCompileError):
Seq([Break()]).__teal__(options)

with pytest.raises(TypeError):
Break(Int(1))


def test_break():

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

options.currentLoop = expr
Copy link
Contributor

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

expected, condEnd = items[0].__teal__(options)
do, doEnd = items[1].__teal__(options)
expectedBranch = TealConditionalBlock([])
end = TealSimpleBlock([])
expectedBranch.setTrueBlock(do)
expectedBranch.setFalseBlock(end)
condEnd.setNextBlock(expectedBranch)
doEnd.setNextBlock(expected)

actual, _ = expr.__teal__(options)
40 changes: 40 additions & 0 deletions pyteal/ast/continue_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from typing import TYPE_CHECKING

from ..types import TealType
from ..errors import TealCompileError
from .expr import Expr
from ..ir import TealSimpleBlock


if TYPE_CHECKING:
from ..compiler import CompileOptions


class Continue(Expr):
"""A continue expression"""

def __init__(self) -> None:
"""Create a new continue expression.

This operation is only permitted in a loop.

"""
super().__init__()

def __str__(self) -> str:
return "continue"

def __teal__(self, options: "CompileOptions"):
if options.currentLoop is None:
raise TealCompileError("continue is only allowed in a loop", self)

start = TealSimpleBlock([])
options.continueBlocks.append(start)

return start, start

def type_of(self):
return TealType.none


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

from .. import *

# this is not necessary but mypy complains if it's not included
from .. import CompileOptions

options = CompileOptions()


def test_continue_fail():
with pytest.raises(TealCompileError):
Continue().__teal__(options)

with pytest.raises(TealCompileError):
If(Int(1), Continue()).__teal__(options)

with pytest.raises(TealCompileError):
Seq([Continue()]).__teal__(options)

with pytest.raises(TypeError):
Continue(Int(1))


def test_continue():

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

options.currentLoop = expr
Copy link
Contributor

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.

expected, condEnd = items[0].__teal__(options)
do, doEnd = items[1].__teal__(options)
expectedBranch = TealConditionalBlock([])
end = TealSimpleBlock([])
expectedBranch.setTrueBlock(do)
expectedBranch.setFalseBlock(end)
condEnd.setNextBlock(expectedBranch)
doEnd.setNextBlock(expected)

actual, _ = expr.__teal__(options)
107 changes: 107 additions & 0 deletions pyteal/ast/for_.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
from typing import TYPE_CHECKING, Optional

from ..types import TealType, require_type
from ..ir import TealSimpleBlock, TealConditionalBlock
from ..errors import TealCompileError
from .expr import Expr
from .seq import Seq
from .int import Int

if TYPE_CHECKING:
from ..compiler import CompileOptions


class For(Expr):
"""For expression."""

def __init__(self, start: Expr, cond: Expr, step: Expr) -> None:
"""Create a new For expression.

When this For expression is executed, the condition will be evaluated, and if it produces a
true value, doBlock will be executed and return to the start of the expression execution.
Otherwise, no branch will be executed.

Args:
start: Expression setting the variable's initial value
cond: The condition to check. Must evaluate to uint64.
step: Expression to update the variable's value.
"""
super().__init__()
require_type(cond.type_of(), TealType.uint64)

Copy link
Contributor

Choose a reason for hiding this comment

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

After more thought I think the start and step expressions should also evaluate to TealType.none, since we don't want them to add things to the stack. Could you add a check for those too?

self.start = start
self.cond = cond
self.step = step
self.doBlock: Optional[Expr] = None

def __teal__(self, options: "CompileOptions"):
if self.doBlock is None:
raise TealCompileError("For expression must have a doBlock", self)

breakBlocks = options.breakBlocks
continueBlocks = options.continueBlocks
prevLoop = options.currentLoop

options.breakBlocks = []
options.continueBlocks = []
options.currentLoop = self

end = TealSimpleBlock([])
start, startEnd = self.start.__teal__(options)
condStart, condEnd = self.cond.__teal__(options)
doStart, doEnd = self.doBlock.__teal__(options)

stepStart, stepEnd = self.step.__teal__(options)
stepEnd.setNextBlock(condStart)
doEnd.setNextBlock(stepStart)

for block in options.breakBlocks:
block.setNextBlock(end)

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

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?


options.breakBlocks = breakBlocks
options.continueBlocks = continueBlocks
options.currentLoop = prevLoop

branchBlock = TealConditionalBlock([])
branchBlock.setTrueBlock(doStart)
branchBlock.setFalseBlock(end)

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

startEnd.nextBlock = condStart
Copy link
Contributor

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?


return start, end

def __str__(self):
if self.start is None:
raise TealCompileError("For expression must have a start", self)
if self.cond is None:
raise TealCompileError("For expression must have a condition", self)
if self.step is None:
raise TealCompileError("For expression must have a end", self)
if self.doBlock is None:
raise TealCompileError("For expression must have a thenBranch", self)

return "(For {} {} {} {})".format(
self.start, self.cond, self.step, self.doBlock
)

def type_of(self):
if self.doBlock is None:
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.

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

return TealType.none

def Do(self, doBlock: Expr):
if self.doBlock is not None:
raise TealCompileError("For expression already has a doBlock", self)
require_type(doBlock.type_of(), TealType.none)
self.doBlock = doBlock
return self


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

from .. import *

# this is not necessary but mypy complains if it's not included
from .. import CompileOptions

options = CompileOptions()


def test_for_compiles():
expr = For(Int(0), Int(1), Int(2)).Do(App.globalPut(Itob(Int(0)), Itob(Int(2))))
assert expr.type_of() == TealType.none
expr.__teal__(options)

expr = For(Int(0), Int(1), Int(2)).Do(App.globalPut(Itob(Int(0)), Itob(Int(2))))
assert expr.type_of() == TealType.none
expr.__teal__(options)


def test_nested_for_compiles():
i = ScratchVar()
expr = For(Int(0), Int(1), Int(2)).Do(
Seq([For(Int(0), Int(1), Int(2)).Do(Seq([i.store(Int(0))]))])
)
assert expr.type_of() == TealType.none


def test_continue_break():
expr = For(Int(0), Int(1), Int(2)).Do(Seq([If(Int(1), Break(), Continue())]))
assert expr.type_of() == TealType.none
expr.__teal__(options)


def test_for():
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 a great test. Could you also 2 more versions of this which test break and continue? Hopefully it should be mostly copy-paste 😅

i = ScratchVar()
items = [
(i.store(Int(0))),
i.load() < Int(10),
i.store(i.load() + Int(1)),
App.globalPut(Itob(i.load()), i.load() * Int(2)),
]
expr = For(items[0], items[1], items[2]).Do(Seq([items[3]]))

assert expr.type_of() == TealType.none

expected, varEnd = items[0].__teal__(options)
condStart, condEnd = items[1].__teal__(options)
stepStart, stepEnd = items[2].__teal__(options)
do, doEnd = Seq([items[3]]).__teal__(options)
expectedBranch = TealConditionalBlock([])
end = TealSimpleBlock([])

varEnd.setNextBlock(condStart)
doEnd.setNextBlock(stepStart)

expectedBranch.setTrueBlock(do)
expectedBranch.setFalseBlock(end)
condEnd.setNextBlock(expectedBranch)
stepEnd.setNextBlock(condStart)

actual, _ = expr.__teal__(options)

assert actual == expected


def test_invalid_for():
with pytest.raises(TypeError):
expr = For()

with pytest.raises(TypeError):
expr = For(Int(2))

with pytest.raises(TypeError):
expr = For(Int(1), Int(2))

with pytest.raises(TealCompileError):
expr = For(Int(0), Int(1), Int(2))
expr.__teal__(options)

with pytest.raises(TealCompileError):
expr = For(Int(0), Int(1), Int(2))
expr.type_of()

with pytest.raises(TealCompileError):
expr = For(Int(0), Int(1), Int(2))
expr.__str__()

with pytest.raises(TealTypeError):
expr = For(Int(0), Int(1), Int(2)).Do(Int(0))

with pytest.raises(TealCompileError):
expr = For(Int(0), Int(1), Int(2)).Do(Continue()).Do(Continue())
expr.__str__()
Loading