Language Guide

Program Structure

A Tealish program has the following structure:

#pragma version 6

{Comments}

{Struct Definitions}

{Constant Declarations}

{Statements}

{Exit Statement}

{Blocks}

{Function Definitions}

Comments

All comments begin on a new line and start with #:

# A comment

Literals

Literal bytes:

"abc"

Literal Integers (unsigned):

42

Constants

Constants must use UPPERCASE names:

const int FOO = 1
const bytes BAR = "ABC"

Declaration & Assignment

Variable names must begin with a lowercase letter: [a-z][a-zA-Z0-9_]*.

Variables can be declared separately from assignment:

int x
bytes b
x = 1
b = "abc"

Or declared and assigned together:

int x = 1
bytes b = "abc"

Assignments must be of the form: {name} = {expression}. The spacing around the = operator is mandatory.

Some opcodes return multiple values. In this case the variables must be declared first:

int exists
bytes asset_unit_name
exists, asset_unit_name = asset_params_get(AssetUnitName, asset_id)

Sometimes you will want to ignore one of the return values:

_, asset_unit_name = asset_params_get(AssetUnitName, asset_id)

Expressions

Expressions are pieces of Tealish that evaluate to a value. They are not valid statements on their own but are used on the right side of assignments, in conditions and arguments.

Note

Statements vs Expressions

A Tealish statement is a valid standalone line (or lines) of Tealish. An expression is something that evaluates to a value. It must be used inside a statement.

Expressions are composed of the following components:
  • Values

  • Function Calls

  • Math/Logic

  • Expression Groups

Values

Values are composed of:
  • Literals

  • Constants

  • Variables

  • Fields

  • Builtins

Function Calls

Function calls take the form of {name}({expression}, {expression}...). Functions can take 0, 1 or more arguments. Examples:

sqrt(x)
app_local_get(address, key)

Function calls are expressions so they are valid arguments to other functions:

sqrt(sqrt(x))

The same function call syntax is used for opcodes, user defined functions and Tealish builtins. Functions that do not return values cannot be used as expressions. They can only be used as statements.

OpCodes

Most opcodes (AVM Functions) can be used in a function call style. The syntax is of the following form: {op_name}({expression_A}, {expression_B}...). For example app_local_get is described in the docs as local state of the key B in the current application in account A. In Tealish this is written as app_local_get(account, key).

Some opcodes expect “immediate args”. For example substring takes two immediate arguments s (start), e (end). In Tealish this is written as substring(0, 5). Some opcodes expect both immediate and stack arguments: e.g asset_params_get(AssetUnitName, asset_id). It is important to note that immediate arguments cannot be expressions and therefore must be literals.

See Functions for the full list of supported function opcodes.

Some opcodes are defined for use as mathematical or logical operators. These are used in the form discussed below.

Math/Logic

Math & Logic are most naturally written with infix notation. Nearly all math & logic expressions are binary expressions of the form {expression} {operator} {expression}. Spacing around the operator is required.

Examples:

1 + 2
2 * a
b / 2
1 == 2
2 != a
x > y

As math & logic expressions are all binary expressions it is necessary to use “groups” rather than chaining:

1 + 2 + 3 # Invalid!
(1 + 2) + 3 # Valid
1 + (2 + 3) # Valid

This requirement exists to ensure math and logic is written obviously and unambiguously (from a human reader perspective).

The exception to the above rule is Unary expressions:

!x # valid
x || !y # valid

Note

Logical operators || (or) and && (and) in Tealish have AVM stack semantics which differ from some other languages. There is no short circuiting with these operators. For example x && f(x) will still evaluate both x and f(x) before evaluating the && even if x is 0.

See Operators for the full list of supported math & logic operator opcodes.

Note

Operators are not handled the same way as other opcodes in Tealish so the langspec opcode support mechanisms do not apply.

Fields

The AVM provides readonly access to fields in a number of namespaces through opcodes. These include global, txn, txna, gtxn, gtxnas, itxn, and many more. Tealish provides a specific syntax for accessing fields to differentiate these from function calls and make the code more readable. The base form of this syntax is {Namespace}.{FieldName}. The namespace is always the Capitalized version of the corresponding (base) opcode; e..g Txn, Gtxn, Itxn, Global. Fieldnames are also always Capitalized.

For array fields indexing is used to access specific elements: {Namespace}.{FieldName}[{Expression}]. In this case the base opcode is still used as the namespace (e.g Txn instead of Txna).

Fields from other transactions in the same Group can be accessed with Gtxn[{GroupIndex}].{FieldName}[{Expression}]. GroupIndex can be an expression or a signed literal (e.g +1, -2). Signed literals are used for relative indexing.

Examples:

Global.Round
Txn.Sender
Itxn.Fee
Txn.ApplicationArgs[0]
Txn.ApplicationArgs[x + 2]
Gtxn[0].Sender
Gtxn[payment_txn_index].Receiver
Gtxn[+1].ApplicationArgs[0]
Gtxn[-2].Sender

See Fields for the full list of fields.

If/Elif/Else

Structure:

if {condition_Expression}:
    {Statements}
elif {condition_Expression}:
    {Statements}
else:
    {Statements}
end

Examples:

if x < 1:
    result = 1
elif x < 10:
    result = 2
else:
    error()
end

if x == y:
    result = 1
end

if x < 1:
    result = 0
else:
    result = 1
end

If statements can be nested:

if x:
    if y:
        error()
    end
end

While Loop

Structure:

while {condition_Expression}:
    {Statements}
end

Examples:

int i = 0
while i <= 10:
    result = result + Txn.ApplicationArgs[i]
    i = i + 1
end

For Loop

Structure:

for {name} in {start}:{end}
    {Statements}
end

start and end can be Literals or Variables but not Expressions (for readability).

Examples:

for i in 0:10:
    result = result + Txn.ApplicationArgs[i]
end

int start = 0
int end = start + 10
for i in start:end:
    result = result + Txn.ApplicationArgs[i]
end

# if the loop variable is not used in the body then ``_`` should be used instead.
for _ in 0:10:
    result = result + "*"
end

Inline Teal

Structure:

teal:
    {Statements}
end

Examples:

pushint(6)
teal:
    int 7
    *
    int 10
    *
end
int result = pop()

Inner Transactions

Tealish has a special syntax for Inner Transactions:

inner_txn:
    {FieldName}: {Expression}
    {FieldName}: {Expression}
    ...
end

Example:

inner_txn:
    TypeEnum: Pay
    Receiver: Txn.Sender
    Amount: 1000
    Fee: 0
end

Inner transactions are evaluated immediately so there is no separate submit function.

Inner transactions can be grouped in inner groups:

inner_group:
    inner_txn:
        TypeEnum: Pay
        Receiver: Txn.Sender
        Amount: 1000
        Fee: 0
    end
    inner_txn:
        TypeEnum: Axfer
        AssetReceiver: Txn.Sender
        AssetAmount: 1000
        Index: 5
        Fee: 0
    end
end

Functions

Functions can be defined in Tealish in the following forms:

func {func_name}({arg1_name}: {type}, {arg2_name}: {type}) {return_type}:
    {Statements}
    return {Expression}
end

# No return value
func {func_name}({arg1_name}: {type}, {arg2_name}: {type}):
    {Statements}
    return
end

# No return value or arguments
func {func_name}():
    {Statements}
    return
end


# Multiple return values
func {func_name}() {return_type}, {return_type}:
    {Statements}
    return {Expression}, {Expression}
end

# Returns in if & else statements
func {func_name}() {return_type}, {return_type}:
    {Statements}
    if {Statements}:
        return {Statements}
    else:
        return {Statements}
    end

    # Return is mandatory just before the end statement of the function
    return
end

Notes:

  • Function names must be lowercase.

  • Argument names must be lowercase.

  • Types must be int or bytes.

  • Functions must have return just before end.

  • Functions must be defined at the end of programs or Blocks. There can be no other statements after function definitions apart from other function definitions.

Examples:

func get_balance(account_idx: int, asset_id: int) int:
    int balance
    if asset_id == 0:
        balance = balance(account_idx) - min_balance(account_idx)
    else:
        _, balance = asset_holding_get(AssetBalance, account_idx, asset_id)
    end
    return balance
end

func checks():
    assert(app_local_get(0, "x") > 7)
    assert(app_local_get(0, "y") > 6)
end

Router

A common pattern in AVM contracts is to execute different functions based on the first application argument (Txn.ApplicationArgs[0]). Tealish includes a Router to make this common pattern easier to write. The router calls functions where the first application argument matches the function name. The remaining application arguments are passed to the function as parameters.

The router is defined in Tealish in the following form:

router:
    {function_name_1}
    {function_name_2}
    {function_name_3}
    ...
end

Notes:

  • Functions must be decorated with @public(...) to be included in the router.

  • The router is a type of exit statement. Execution will not continue after it.

  • If Txn.ApplicationArgs[0] does not match one of the included function names an error will be raised.

Examples:

#pragma version 9

router:
    method_a
    method_b
    update_app
    delete_app
end

@public(OnCompletion=UpdateApplication)
func update_app():
    assert(Txn.Sender == Global.CreatorAddress)
    return
end

@public(OnCompletion=DeleteApplication)
func delete_app():
    assert(Txn.Sender == Global.CreatorAddress)
    return
end

# a public decorator with the default OnCompletion=NoOp
@public()
func method_a(user_address: bytes[32], amount: int):
    # some statements here
    return
end

@public()
func method_b() int:
    # this value will be logged by the router as an arc4 style return value
    return 42
end

The generated Teal for the above code would be as follows:

#pragma version 9

// tl:3: router:
pushbytes "method_a"
pushbytes "method_b"
pushbytes "update_app"
pushbytes "delete_app"
txna ApplicationArgs 0
match route_method_a route_method_b route_update_app route_delete_app
err                                                         // unexpected value
route_method_a:
    txn OnCompletion; pushint 0; ==; assert                 // assert OnCompletion == NoOp
    txna ApplicationArgs 1; dup; len; pushint 32; ==; assert// Bytes Size Assertion: 32 bytes
    txna ApplicationArgs 2; btoi 
    callsub __func__method_a
    pushint 1; return
route_method_b:
    txn OnCompletion; pushint 0; ==; assert                 // assert OnCompletion == NoOp
    callsub __func__method_b
    // return int
    // uncover 0 int
    itob
    pushbytes 0x151f7c75; swap; concat; log                 // arc4 return log
    pushint 1; return
route_update_app:
    txn OnCompletion; pushint 4; ==; assert                 // assert OnCompletion == UpdateApplication
    callsub __func__update_app
    pushint 1; return
route_delete_app:
    txn OnCompletion; pushint 5; ==; assert                 // assert OnCompletion == DeleteApplication
    callsub __func__delete_app
    pushint 1; return

// tl:11: func update_app():
__func__update_app:
    // tl:12: assert(Txn.Sender == Global.CreatorAddress)
    txn Sender
    global CreatorAddress
    ==
    assert
    // tl:13: return
    retsub

// tl:17: func delete_app():
__func__delete_app:
    // tl:18: assert(Txn.Sender == Global.CreatorAddress)
    txn Sender
    global CreatorAddress
    ==
    assert
    // tl:19: return
    retsub

// a public decorator with the default OnCompletion=NoOp
// tl:24: func method_a(user_address: bytes[32], amount: int):
__func__method_a:
    store 1                                                 // amount [int]
    store 2                                                 // user_address [bytes[32]]
    // some statements here
    // tl:26: return
    retsub

// tl:30: func method_b() int:
__func__method_b:
    // this value will be logged by the router as an arc4 style return value
    // tl:32: return 42
    pushint 42
    retsub

Blocks

Blocks are low level scoping and labeling constructs. Execution can jump between blocks using jump or switch statements.

Note

Blocks are a low level feature that are less commonly used now that the Function Router is available. Most previous uses of blocks can now be more cleanly written with the router and functions.

Blocks can be defined in Tealish in the following forms:

block {block_name}:
{Statements}
end

Notes:

  • Variables are scoped by blocks and functions.

  • Blocks should end with an exit statement.

Examples:

block main:
    int sender = Txn.Sender
    pay_to_user()
    exit(1)

    func pay_to_user(amount: int):
        # Function can use variables defined in the outer blocks.
        pay(sender, amount)
        return
    end
    exit(0)
end

func pay(receiver: bytes, amount: int):
    inner_txn:
        TypeEnum: Pay
        Receiver: receiver
        Amount: amount
        Fee: 0
    end
    return
end

Switch

A switch statement is used to conditionally branch to blocks of a program. Structure:

switch {condition_Expression}:
    {Value}: {Block_Name}
    {Value}: {Block_Name}
    {Value}: {Block_Name}
    ...
    else: {Block_Name}
end

Notes:

  • If none if the values match the expression then the optional else target is used if specified, otherwise an error is raised.

  • Any code after a switch is unreachable.

Example:

switch Txn.OnCompletion:
    NoOp: main
    OptIn: opt_in
    CloseOut: close_out
    UpdateApplication: update_app
    DeleteApplication: delete_app
end

block opt_in:
    # Handle Opt In
    # some statements here
    exit(1)
end

block update_app:
    # Handle Update App
    # Example: Only allow the Creator to update the app
    exit(Txn.Sender == Global.CreatorAddress)
end

Jump

A jump statement is used to unconditionally branch to a block of a program.

Structure:

jump({Block_Name})

Notes:

  • The block name is a literal, not an expression.

  • Any code after a jump is unreachable.

Structs

Structs are used to define structure for byte strings.

Structs can be defined in Tealish in the following form:

struct {Struct_name}:
    {field_name}: {type}
end

Notes:

  • Structs must be defined at the top of the file.

  • Struct names must begin with a capital letter.

  • Field names must be lowercase.

  • Types may be either int or bytes[N]

Examples:

struct Item:
    x: int
    y: int
    name: bytes[10]
end

Item item = Cast(Txn.ApplicationArgs[0], Item)
log(item.name)
assert(item.x > 10)
item.y = 1

Note

There are no implicit runtime checks when assigning to a struct or struct field. Care must be taken to ensure field values are correctly sized.

Boxes

Boxes can be accessed and manipulated using the standard opcodes (box_put, box_get, box_extract, etc).

Tealish also supports a higher level syntax that makes it easier to deal with structured data in boxes using structs. A typed box reference can created with the following forms:

box<{Struct_name}> {box_name} = CreateBox("{box_key}") # asserts box does not already exist
box<{Struct_name}> {box_name} = OpenBox("{box_key}")   # asserts box does already exist and has the correct size for the struct
box<{Struct_name}> {box_name} = Box("{box_key}")       # makes no assertions about the box

A box field can be set or accessed just like a struct field:

{box_name}.{field_name} = {value}

log({box_name}.{field_name})

Examples:

struct Item:
    id: int
    foo: int
    name: bytes[10]
end

box<Item> item1 = CreateBox("a")

item1.id = 1
item1.foo = 111
item1.name = "Tealish   "

log(item1.name)

Warning

There are no implicit runtime checks when assigning to a box field. Care must be taken to ensure field values are correctly sized.

Types

The AVM has two types; bytes and int. Bytes are bytestrings up to 4096 bytes in length. Ints are unsigned 64bit integers in the range 0 - (2^64 - 1). Ints always take up 8 bytes.

Tealish introduces fixed sized byte strings (bytes[size]), and custom types with structs.

The Tealish compiler checks types at compile time for assignments and function call arguments. If the types do not match an error will be raised.

Example 1:

Error: Incorrect type uint8 for arg 0 of log. Expected bytes at line 19
    log(42)

In this case the compiler is informing us that log expects bytes but we have given an int. We must change our code somehow to make this work.

Example 2:

$ tealish compile source/language/structs.tl
Compiling source/language/structs.tl to source/language/build/structs.teal
Error: Incorrect type for assignment. Expected Item, got bytes at line 7.
Perhaps Cast or padding is required?
- Item item = Txn.ApplicationArgs[0]
+ Item item = Cast(Txn.ApplicationArgs[0], Item)

In this case we want to treat a bytestring from the application args as an Item struct. However the compiler can’t know if the bytestring is the correct size for this struct so we need to convince it. We ourselves can’t be sure the arg will be the right size so the safest way forward is to use Cast as suggested which will tell the compiler the value is the right size but also add a runtime assertion to ensure it is.

Item item = Cast(Txn.ApplicationArgs[0], Item)

Example 3:

transfer(app_global_get("MANAGER"))
...

func transfer(user: bytes[32]):
    return
end

Error: Incorrect type any for arg 0 of transfer. Expected bytes[32] at line 18
    transfer(app_global_get("MANAGER"))

In this case we have a user defined function with an argument with a fixed size byte string (bytes[32]). Again the compiler doesn’t know the size of the value we retrieve from global state so we should use a Cast here again.

If we can be certain that the value retrieved from global state is of the required type we can skip the runtime type assertion introduced by Cast and use UncheckedCast instead. This tells the compiler the value is known to have a certain type. This should be used with caution and only when a logical proof of type is possible and when the cost of the additional assertion is unacceptable.

# use UncheckedCast with caution!
transfer(UncheckedCast(app_global_get("MANAGER"), bytes[32]))

Example 4:

transfer(Txn.Sender)
...

The above line does not raise an error or require a Cast however because the compiler knows that Txn.Sender is always a bytes[32].