docs/lang/articles/reference/language_reference.md
This article describes the syntax and semantics of the Taichi programming language.
To users: If you have gone through the user tutorials and still feel uncertain about your program behavior, then you are in the right place. If you find the actual behavior different from what is described in this article, feel free to create an issue. You should not rely solely on this article since things unspecified are subject to changes.
To contributors: This article specifies what the language should be. That is, you should try to match the implementation of the Taichi compiler with this article. You can clearly determine a certain behavior is correct, buggy, or undefined from this article.
Taichi is a domain-specific language embedded in Python. Kernels and functions clearly defines the boundary between the Taichi language and the Python language - code in the Taichi scope is treated as the former, while code in the Python scope is treated as the latter. It should be emphasized that this article is about the Taichi language.
That said, because Taichi is embedded in Python, the syntax of Taichi is a subset of that of Python. To make life easier, this article is modeled after the Python language reference. The notation and lexical analysis parts exactly follow Python. Please familiarize yourself with them if they seem new.
Before detailing syntax and semantics in the next few chapters, many basic but important concepts and general evaluation principles specific to Taichi are listed here.
Like many other programming languages, each expression in Taichi will be evaluated to a value, and each value has a type. Because Taichi provides easy interaction with Python and meta-programming support, there are actually two kinds of evaluation: compile-time evaluation and runtime evaluation. There are also two kinds of values: Python values and Taichi values.
:::note For readers familiar with programming language terms, such behavior is inspired by multi-stage programming or partial evaluation. :::
A Python value is simply a Python object, which directly comes from the following sources:
ti.template()Furthermore, as long as all the operands of an operation are Python values,
compile-time evaluation will take place, producing a result Python value. For
meta-programming purposes, Taichi provides an advanced environment for
compile-time evaluation: ti.static(), where more operations are supported.
A Python value only exists at compile time. After compile-time evaluation, all the remaining expressions will be evaluated to Taichi values at runtime.
A Taichi value has a Taichi type, which is one of the following:
:::note An informal quick summary of evaluation rules:
A variable contains a name, a type and a value. In Taichi, a variable can be defined in the following ways:
Taichi is statically-typed. That is, you cannot change the type of a variable after its definition. However, you can change the value of a variable if there is another assignment statement after its definition.
Taichi adopts lexical scope. Therefore, if a variable is defined in a block, it is invisible outside that block.
Following the Values and types section, if both operands of a binary operation are Python values, compile-time evaluation is triggered and a result Python value is produced. If only one operand is a Python value, it is first turned into a Taichi value with default type. Now the only remaining case is that both operands are Taichi values.
Binary operations can happen between Taichi values of either primitive type or compound type. There are three cases in total:
The section explains the syntax and semantics of expressions in Taichi.
Atoms are the most basic elements of expressions. The simplest atoms are identifiers or literals. Forms enclosed in parentheses, brackets or braces are also categorized syntactically as atoms.
atom ::= identifier | literal | enclosure
enclosure ::= parenth_form | list_display | dict_display
Lexical definition of identifiers (also referred to as names) in Taichi follows Python.
There are three cases during evaluation:
TaichiNameError is thrown.Taichi supports integer and floating-point literals, whose lexical definitions follow Python.
literal ::= integer | floatnumber
Literals are evaluated to Python values at compile time.
parenth_form ::= "(" [expression_list] ")"
A parenthesized expression list is evaluated to whatever the expression list is evaluated to. An empty pair of parentheses is evaluated to an empty tuple at compile time.
Taichi supports displays for container (list and dictionary only) construction. Like in Python, a display is one of:
list_display ::= "[" [expression_list | list_comprehension] "]"
list_comprehension ::= assignment_expression comp_for
dict_display ::= "{" [key_datum_list | dict_comprehension] "}"
key_datum_list ::= key_datum ("," key_datum)* [","]
key_datum ::= expression ":" expression
dict_comprehension ::= key_datum comp_for
comp_for ::= "for" target_list "in" or_test [comp_iter]
comp_iter ::= comp_for | comp_if
comp_if ::= "if" or_test [comp_iter]
The semantics of list and dict displays in Taichi mainly follow Python. Note
that they are evaluated at compile time, so all expressions in comp_for,
as well as keys in key_datum, are required to be evaluated to Python values.
For example, in the following code snippet, a can be successfully defined
while b cannot because p cannot be evaluated to a Python value at compile
time.
@ti.kernel
def test(p: ti.i32):
a = ti.Matrix([i * p for i in range(10)]) # valid
b = ti.Matrix([i * p for i in range(p)]) # compile error
Primaries represent the most tightly bound operations.
primary ::= atom | attributeref | subscription | slicing | call
attributeref ::= primary "." identifier
Attribute references are evaluated at compile time. The primary must be
evaluated to a Python value with an attribute named identifier. Common use
cases in Taichi include metadata queries of
field and
matrices.
subscription ::= primary "[" expression_list "]"
If primary is evaluated to a Python value (e.g., a list or a dictionary),
then all expressions in expression_list are required to be evaluated to
Python values, and the subscription is evaluated at compile time following
Python.
Otherwise, primary has a Taichi type. All Taichi types excluding primitive
types support subscriptions. You can refer to documentation of these types
for subscription usage.
slicing ::= primary "[" slice_list "]"
slice_list ::= slice_item ("," slice_item)* [","]
slice_item ::= expression | proper_slice
proper_slice ::= [expression] ":" [expression] [ ":" [expression] ]
Currently, slicings are only supported when primary has a Taichi matrix type,
and the evaluation happens at compile time.
When slice_item is in the form of:
expression: it is required to be evaluated to a Python value
unless ti.init(dynamic_index=True) is set.proper_slice: all expressions (the lower bound, the upper bound, and the
stride) inside have to be evaluated to Python values.call ::= primary "(" [positional_arguments] ")"
positional_arguments ::= positional_item ("," positional_item)*
positional_item ::= assignment_expression | "*" expression
The primary must be evaluated to one of:
positional_arguments must only
contain one item. If the item is evaluated to a Python value, then the
primitive type serves as a type annotation for a literal, and the Python value
will be turned into a Taichi value with that annotated type. Otherwise, the
primitive type serves as a syntax sugar for ti.cast(), but the item cannot
have a compound type.power ::= primary ["**" u_expr]
The power operator has the same semantics as the builtin pow() function.
u_expr ::= power | "-" power | "+" power | "~" power
Similar to rules for binary operations, if the operand is a Python value, compile-time evaluation is triggered and a result Python value is produced. Now the remaining case is that the operand is a Taichi value:
See arithmetic operators and
bitwise operators for operator details.
Note that ~ can only be used with integer type values.
m_expr ::= u_expr | m_expr "*" u_expr | m_expr "@" m_expr | m_expr "//" u_expr | m_expr "/" u_expr | m_expr "%" u_expr
a_expr ::= m_expr | a_expr "+" m_expr | a_expr "-" m_expr
See common rules for binary operations,
implicit type casting in binary operations,
and arithmetic operators. Note that
the @ operator is for matrix multiplication and only operates on matrix type
arguments.
shift_expr::= a_expr | shift_expr ( "<<" | ">>" ) a_expr
See common rules for binary operations, implicit type casting in binary operations, and bitwise operators. Note that both operands are required to have integer types.
and_expr ::= shift_expr | and_expr "&" shift_expr
xor_expr ::= and_expr | xor_expr "^" and_expr
or_expr ::= xor_expr | or_expr "|" xor_expr
See common rules for binary operations, implicit type casting in binary operations, and bitwise operators. Note that both operands are required to have integer types.
comparison ::= or_expr (comp_operator or_expr)*
comp_operator ::= "<" | ">" | "==" | ">=" | "<=" | "!=" | ["not"] "in"
Comparisons can be chained arbitrarily, e.g., x < y <= z is equivalent to (x < y) & (y <= z).
See common rules for binary operations, implicit type casting in binary operations, and comparison operators.
The semantics of membership test operations follow Python, but they are only supported in static expressions.
or_test ::= and_test | or_test "or" and_test
and_test ::= not_test | and_test "and" not_test
not_test ::= comparison | "not" not_test
When the operator is inside a static expression,
the evaluation rule of the operator follows Python.
Otherwise, the behavior depends on the short_circuit_operators option of ti.init():
short_circuit_operators is False (default), a logical and will be
treated as a bitwise AND, and a logical or will be treated as a bitwise
OR. See binary bitwise operations for details.short_circuit_operators is True, the normal short circuiting behavior
is adopted, and the operands are required to be boolean values. Since Taichi
does not have boolean type yet, ti.i32 is served as a temporary alternative.
A ti.i32 value is considered False if and only if the value is evaluated to 0.assignment_expression ::= [identifier ":="] expression
An assignment expression assigns an expression to an identifier (see assignment statements for more details), while also returning the value of the expression.
Example:
@ti.kernel
def foo() -> ti.i32:
b = 2 + (a := 5)
b += a
return b
# the return value should be 12
:::note This operator is supported since Python 3.8. :::
conditional_expression ::= or_test ["if" or_test "else" expression]
expression ::= conditional_expression
The expression x if C else y first evaluates the condition, C rather than x.
If C is True (the meaning of True and False has been mentioned at boolean operations), x is evaluated and its value is returned; otherwise,y is evaluated and its value is returned.
static_expression ::= "ti.static(" positional_arguments ")"
Static expressions are expressions that are wrapped by a call to ti.static().
The positional_arguments is evaluated at compile time, and the items inside must be evaluated to Python values.
ti.static() receives one or more arguments.
The static expressions work as a mechanism to trigger many metaprogramming functions in Taichi, such as compile-time loop unrolling and compile-time branching.
The static expressions can also be used to create aliases for Taichi fields and Taichi functions.
expression_list ::= expression ("," expression)* [","]
Except when part of a list display, an expression list containing at least one comma is evaluated to a tuple at compile time. The component expressions are evaluated from left to right.
The trailing comma is required only to create a tuple with length 1; it is optional in all other cases. A single expression without a trailing comma is evaluated to the value of that expression.
This section explains the syntax and semantics of compound statements in Taichi. A simple statement is comprised within a single logical line. Several simple statements may occur on a single line separated by semicolons.
simple_stmt ::= expression_stmt
| assert_stmt
| assignment_stmt
| augmented_assignment_stmt
| annotated_assignment_stmt
| pass_stmt
| return_stmt
| break_stmt
| continue_stmt
expression_stmt ::= expression_list
An expression statement evaluates the expression list (which may be a single expression).
assignment_stmt ::= (target_list "=")+ expression_list
target_list ::= target ("," target)* [","]
target ::= identifier
| "(" [target_list] ")"
| "[" [target_list] "]"
| attributeref
| subscription
The recursive definition of an assignment statement basically follows Python, with the following points to notice:
augmented_assignment_stmt ::= augtarget augop expression_list
augtarget ::= identifier | attributeref | subscription
augop ::= "+=" | "-=" | "*=" | "/=" | "//=" | "%=" |
"**="| ">>=" | "<<=" | "&=" | "^=" | "|="
Different from Python, some augmented assignments (e.g., x[i] += 1) are automatically atomic in Taichi.
annotated_assignment_stmt ::= identifier ":" expression "=" expression
The differences from normal assignment statements are:
assert statementAssert statements are a convenient way to insert debugging assertions into a program:
assert_stmt ::= "assert" expression ["," expression]
Assert statements are currently supported on the CPU, CUDA, and Metal backends.
Assert statements only work in debug mode (when debug=True is set in the arguments of ti.init()),
otherwise they are equivalent to no-op.
The simple form, assert expression, raises TaichiAssertionError (which is a subclass of AssertionError)
when expression is equal to False, with the code of expression as the error message.
The extended form, assert expression1, expression2, raises TaichiAssertionError when expression1 is equal to False,
with expression2 as the error message. expression2 must be a constant or a formatted string. The variables in the
formatted string must be scalars.
pass statementpass_stmt ::= "pass"
pass is a null operation — when it is executed, nothing happens.
It is useful as a placeholder when a statement is required syntactically, but no code needs to be executed.
return statementreturn_stmt ::= "return" [expression_list]
The return statement may only occur once in a Taichi kernel or a Taichi function, and it must be at the bottom of the function body. Note that this is subject to change, and Taichi might relax it in the future.
If a Taichi kernel or Taichi function has a return type hint,
it must have a return statement that returns a value other than None.
If a Taichi kernel has a return statement that returns a value other than None, it must have a return type hint.
The return type hint for Taichi function is optional but recommended.
Note that this is subject to change, and Taichi might enforce it in the future.
A kernel can have at most one return value, which can be a scalar, ti.Matrix, or ti.Vector,
and the number of elements in the return value must not exceed 30.
Note that this number is an implementation detail, and Taichi might relax it in the future.
A Taichi function can have multiple return values in a return statement,
and the return values can be scalar, ti.Vector, ti.Matrix, ti.Struct, and more.
break statementbreak_stmt ::= "break"
The break statement may only occur syntactically nested in a for or while loop, and it terminates the nearest enclosing loop.
Break statement is not allowed when the nearest enclosing loop is a parallel range/ndrange for loop, a struct for loop, or a mesh for loop.
continue statementcontinue_stmt ::= "continue"
The continue statement may only occur syntactically nested in a for or while loop, and it continues with the next cycle of the nearest enclosing loop.
This section explains the syntax and semantics of compound statements in Taichi.
A compound statement consists of one or more clauses. A clause consists of a header and a suite. The clause headers of a particular compound statement are all at the same indentation level. Each clause header begins with a uniquely identifying keyword and ends with a colon. A suite is a group of statements controlled by a clause.
compound_stmt ::= if_stmt | while_stmt | for_stmt
suite ::= stmt_list NEWLINE | NEWLINE INDENT statement+ DEDENT
statement ::= stmt_list NEWLINE | compound_stmt
stmt_list ::= simple_stmt (";" simple_stmt)* [";"]
The difference between the compound statements in Taichi and Python is that Taichi introduces compile time evaluation. If the expression in the clause header is a static expression, Taichi replaces the compound statement at compile time according to the evaluation result of the expression.
if statementThe if statement is used for conditional execution:
if_stmt ::= "if" (static_expression | assignment_expression) ":" suite
("elif" (static_expression | assignment_expression) ":" suite)*
["else" ":" suite]
The elif clause is a syntax sugar for a if statement inside a else clause.
For example:
if cond_a:
body_a
elif cond_b:
body_b
elif cond_c:
body_c
else:
body_d
is equivalent to
if cond_a:
body_a
else:
if cond_b:
body_b
else:
if cond_c:
body_c
else:
body_d
Taichi first transforms elif clause as above, and then deal with the if statement with only an if clause and possibly an else clause as below.
If the expression of the if clause is found to be true (see section Boolean operations for the definition of true and false),
the suite of the if clause is executed. Otherwise, the suite of the else clause, if present, is executed.
An if statement whose expression is a static expression is called a static if statement.
The expression of a static if clause is evaluated at compile time, and it replaces the compound statement as below at compile time.
if clause replaces the static if statement.else clause, the suite of the else clause replaces the static if statement.else clause, a pass statement replaces the static if statement.while statementThe while statement is used for repeated execution as long as an expression is true:
while_stmt ::= "while" assignment_expression ":" suite
This repeatedly tests the expression and, if it is true, executes the suite; if the expression is false (which may be the first time it is tested) the loop terminates.
A break statement executed in the suite terminates the loop.
A continue statement executed in the suite skips the rest of the suite and
goes back to testing the expression.
for statementThe for statement in Taichi is used to iterate over a range of numbers, multidimensional ranges, or the indices of elements in a field.
for_stmt ::= "for" target_list "in" iter_expression ":" suite
iter_expression ::= static_expression | expression
Taichi does not support else clause in for statements.
The for loops can iterate in parallel if they are in the outermost scope.
When a for loop is parallelized, the order of iteration is not determined,
and it cannot be terminated by break statements.
Taichi uses ti.loop_config function to set directives for the loop right after it.
You can write ti.loop_config(serialize=True) before a range/ndrange for loop to let it run serially,
then it can be terminated by break statements.
There are four kinds of for statements:
for statementfor statementfor statementfor statementfor statementThe range for statement iterates over a range of numbers.
The iter_expression of range for statement must be like range(start, stop) or range(stop),
and they mean the same as the Python range function,
except that the step argument is not supported.
The target_list of range for statement must be an identifier which
is not occupied in the current scope.
The range for loops are by default parallelized when the loops are in the outermost scope.
for statementThe ndrange for iterates over multidimensional ranges.
The iter_expression of ndrange for statement must be a call to ti.ndrange() or a nested call to ti.grouped(ti.ndrange()).
iter_expression is a call to ti.range(), it is a normal ndrange for.iter_expression is a call to ti.grouped(ti.range()), it is a grouped ndrange for.You can use grouped for loops to write dimensionality-independent programs.
ti.ndrange receives arbitrary numbers of arguments.
The k-th argument represents the iteration range of the k-th dimension,
and the loop iterates over the direct product of the iteration range of each dimension.
Every argument must be an integer or a tuple of two integers.
stop, the range of the k-th dimension
is equivalent to the range of range(stop) in Python.(start, stop), the range of the k-th dimension
is equivalent to the range of range(start, stop) in Python.The target_list of an n-dimensional normal ndrange for statement must be n different identifiers which
are not occupied in the current scope, and the k-th identifier is assigned an integer which is the loop variable of the k-th dimension.
The target_list of an n-dimensional grouped ndrange for statement must be one identifier which
is not occupied in the current scope, and the identifier is assigned a ti.Vector with length n, which contains the loop variables of all n dimensions.
The ndrange for loops are by default parallelized when the loops are in the outermost scope.
for statementThe struct for statement iterates over every active elements in a Taichi field.
The iter_expression of a struct for statement must be a Taichi field or a call to ti.grouped(x) where x is a Taichi field.
iter_expression is a Taichi field, it is a normal struct for.iter_expression is a call to ti.grouped(x) where x is a Taichi field, it is a grouped struct for.The target_list of a normal struct for statement on an n-dimensional field must be n different identifiers which
are not occupied in the current scope, and the k-th identifier is assigned an integer which is the loop variable of the k-th dimension.
The target_list of a grouped struct for statement on an n-dimensional field must be one identifier which
is not occupied in the current scope, and the identifier is assigned a ti.Vector with length n, which contains the loop variables of all n dimensions.
The struct for statement must be at the outermost scope of the kernel,
and it cannot be terminated by a break statement even when it is run serially.
for statementThe static for statement unrolls a range/ndrange for loop at compile time.
If the iter_expression of the for statement is a static_expression,
the for statement is a static for statement.
The positional_arguments of the static_expression must meet the requirement on
iter_expression of the range/ndrange for.
For example,
for i in ti.static(range(5)):
print(i)
is unrolled to
print(0)
print(1)
print(2)
print(3)
print(4)
at compile time.