Control Flow
PyTeal provides several control flow expressions to create programs.
Exiting the Program: Approve
and Reject
Note
The Approve
and Reject
expressions are only available in program version 4 or higher.
Prior to this, Return(Int(1))
is equivalent to Approve()
and Return(Int(0))
is equivalent to Reject()
.
The Approve
and Reject
expressions cause the program to immediately exit. If Approve
is used, then the execution is marked as successful, and if Reject
is used, then the execution
is marked as unsuccessful.
These expressions also work inside subroutines.
Chaining Expressions: Seq
The Seq
expression can be used to create a sequence of multiple expressions. It’s arguments are the
expressions to include in the sequence, either as a variable number of arguments, or as a single list
For example:
Seq(
App.globalPut(Bytes("creator"), Txn.sender()),
Return(Int(1))
)
A Seq
expression will take on the value of its last expression. Additionally, all
expressions in a Seq
expression, except the last one, must not return anything (e.g.
evaluate to TealType.none
). This restriction is in place because intermediate values must not
add things to the TEAL stack. As a result, the following is an invalid sequence:
Seq(
Txn.sender(),
Return(Int(1))
)
If you must include an operation that returns a value in the earlier
part of a sequence, you can wrap the value in a Pop
expression to discard it. For example,
Seq(
Pop(Txn.sender()),
Return(Int(1))
)
Simple Branching: If
In an If
expression,
If(test-expr, then-expr, else-expr)
the test-expr
is always evaluated and needs to be typed TealType.uint64
.
If it results in a value greater than 0, then the then-expr
is evaluated.
Otherwise, else-expr
is evaluated. Note that then-expr
and else-expr
must
evaluate to the same type (e.g. both TealType.uint64
).
You may also invoke an If
expression without an else-expr
:
If(test-expr, then-expr)
In this case, then-expr
must be typed TealType.none
.
There is also an alternate way to write an If
expression that makes reading
complex statements easier to read.
If(test-expr)
.Then(then-expr)
.ElseIf(test-expr)
.Then(then-expr)
.Else(else-expr)
Checking Conditions: Assert
The Assert
expression can be used to ensure that conditions are met before continuing the
program. The syntax for Assert
is:
Assert(test-expr)
If test-expr
is always evaluated and must be typed TealType.uint64
. If
test-expr
results in a value greater than 0, the program continues. Otherwise, the program
immediately exits and indicates that it encountered an error.
Example:
Assert(Txn.type_enum() == TxnType.Payment)
The above example will cause the program to immediately fail with an error if the transaction type is not a payment.
Chaining Tests: Cond
A Cond
expression chains a series of tests to select a result expression.
The syntax of Cond is:
Cond([test-expr-1, body-1],
[test-expr-2, body-2],
. . . )
Each test-expr
is evaluated in order. If it produces 0, the paired body
is ignored, and evaluation proceeds to the next test-expr
.
As soon as a test-expr
produces a true value (> 0),
its body
is evaluated to produce the value for this Cond
expression.
If none of test-expr
s evaluates to a true value, the Cond
expression will
be evaluated to err
, a TEAL opcode that causes the runtime panic.
In a Cond
expression, each test-expr
needs to be typed TealType.uint64
.
A body
could be typed either TealType.uint64
or TealType.bytes
. However, all
body
s must have the same data type. Otherwise, a TealTypeError
is triggered.
Example:
Cond([Global.group_size() == Int(5), bid],
[Global.group_size() == Int(4), redeem],
[Global.group_size() == Int(1), wrapup])
This PyTeal code branches on the size of the atomic transaction group.
Looping: While
Note
This expression is only available in program version 4 or higher.
The While
expression can be used to create simple loops in PyTeal. The syntax of While
is:
While(loop-condition).Do(loop-body)
The loop-condition
expression must evaluate to TealType.uint64
, and the loop-body
expression must evaluate to TealType.none
.
The loop-body
expression will continue to execute as long as loop-condition
produces
a true value (> 0).
For example, the following code uses ScratchVar
to iterate through every transaction in the
current group and sum up all of their fees.
totalFees = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
i.store(Int(0)),
totalFees.store(Int(0)),
While(i.load() < Global.group_size()).Do(
totalFees.store(totalFees.load() + Gtxn[i.load()].fee()),
i.store(i.load() + Int(1))
)
])
Looping: For
Note
This expression is only available in program version 4 or higher.
Similar to While
, the For
expression can also be used to create loops in PyTeal. The
syntax of For
is:
For(loop-start, loop-condition, loop-step).Do(loop-body)
The loop-start
, loop-step
, and loop-body
expressions must evaluate to
TealType.none
, and the the loop-condition
expression must evaluate to TealType.uint64
.
When a For
expression is executed, loop-start
is executed first. Then the
expressions loop-condition
, loop-body
, and loop-step
will continue to
execute in order as long as loop-condition
produces a true value (> 0).
For example, the following code uses ScratchVar
to iterate through every transaction in the
current group and sum up all of their fees. The code here is functionally equivalent to the
While
loop example above.
totalFees = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
totalFees.store(Int(0)),
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
totalFees.store(totalFees.load() + Gtxn[i.load()].fee())
)
])
Exiting Loops: Continue
and Break
The expressions Continue
and Break
can be used to exit While
and For
loops in different ways.
When Continue
is present in the loop body, it instructs the program to skip the remainder
of the loop body. The loop may continue to execute as long as its condition remains true.
For example, the code below iterates though every transaction in the current group and counts how
many are payments, using the Continue
expression.
numPayments = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
numPayments.store(Int(0)),
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() != TxnType.Payment)
.Then(Continue()),
numPayments.store(numPayments.load() + Int(1))
)
])
When Break
is present in the loop body, it instructs the program to completely exit the
current loop. The loop will not continue to execute, even if its condition remains true.
For example, the code below finds the index of the first payment transaction in the current group,
using the Break
expression.
firstPaymentIndex = ScratchVar(TealType.uint64)
i = ScratchVar(TealType.uint64)
Seq([
# store a default value in case no payment transactions are found
firstPaymentIndex.store(Global.group_size()),
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() == TxnType.Payment)
.Then(
firstPaymentIndex.store(i.load()),
Break()
)
),
# assert that a payment was found
Assert(firstPaymentIndex.load() < Global.group_size())
])
Subroutines
Note
Subroutines are only available in program version 4 or higher.
A subroutine is section of code that can be called multiple times from within a program. Subroutines are PyTeal’s equivalent to functions. Subroutine constraints include:
Subroutines accept any number of arguments.
Subroutine argument types can be any Expr (PyTeal expression) or strictly ScratchVar (no subclasses allowed). PyTeal applies pass-by-value semantics to Expr and pass-by-reference to ScratchVar.
Subroutines return a single value, or no value.
Creating Subroutines
To create a subroutine, apply the Subroutine
function decorator to a Python function which
implements the subroutine. This decorator takes one argument, which is the return type of the subroutine.
TealType.none
indicates that the subroutine does not return a value, and any other type
(e.g. TealType.uint64
or TealType.bytes
) indicates the return type of the single value
the subroutine returns.
For example,
@Subroutine(TealType.uint64)
def isEven(i):
return i % Int(2) == Int(0)
PyTeal applies these parameter type annotation constraints when compiling subroutine definitions:
ScratchVar
parameters require a type annotation.Expr
parameters do not require a type annotation. PyTeal implicitly declares unannotated parameter types asExpr
.
Here’s an example illustrating ScratchVar parameter declaration with parameter type annotations:
@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()),
)
Calling Subroutines
To call a subroutine, simply call it like a normal Python function and pass in its arguments. For example,
App.globalPut(Bytes("value_is_even"), isEven(Int(10)))
Recursion
Recursion with subroutines is also possible. For example, the subroutine below also checks if its argument is even, but uses recursion to do so.
@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)))
)
Recursion and ScratchVar’s
Recursion with parameters of type ScratchVar is disallowed. For example, the following subroutine is considered illegal and attempting compilation will result in a TealInputError:
@Subroutine(TealType.none)
def ILLEGAL_recursion(i: ScratchVar):
return (
If(i.load() == Int(0))
.Then(i.store(Int(1)))
.ElseIf(i.load() == Int(1))
.Then(i.store(Int(0)))
.Else(i.store(i.load() - Int(2)), ILLEGAL_recursion(i))
)
Exiting Subroutines
The Return
expression can be used to explicitly return from a subroutine.
If the subroutine does not return a value, Return
should be called with no arguments. For
example, the subroutine below asserts that the first payment transaction in the current group has a
fee of 0:
@Subroutine(TealType.none)
def assertFirstPaymentHasZeroFee():
i = ScratchVar(TealType.uint64)
return Seq([
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() == TxnType.Payment)
.Then(
Assert(Gtxn[i.load()].fee() == Int(0)),
Return()
)
),
# no payments found
Err()
])
Otherwise if the subroutine does return a value, that value should be the argument to the Return
expression. For example, the subroutine below checks whether the current group contains a payment
transaction:
@Subroutine(TealType.uint64)
def hasPayment():
i = ScratchVar(TealType.uint64)
return Seq([
For(i.store(Int(0)), i.load() < Global.group_size(), i.store(i.load() + Int(1))).Do(
If(Gtxn[i.load()].type_enum() == TxnType.Payment)
.Then(Return(Int(1)))
),
Return(Int(0))
])
Return
can also be called from the main program. In this case, a single integer argument
should be provided, which is the success value for the current execution. A true value (> 0)
is equivalent to Approve
, and a false value is equivalent to Reject
.