Expressions
Arithmetic operators
Arithmetic operations can be applied on numeric scalars and tensors. Broadcasting is applied if needed per arithmetic broadcasting rules. See the Broadcasting section for more information about when broadcast is applicable. All operators in TML behave as element-wise operators.
Warning
At the moment, TML does not support the following:
- Operators:
\(left division)
TML currently supports the following in-place assignments on scalars and vectors:
+=-=*=/=%=
The assignments using the operator %= can be applied on scalars only.
Examples of using arithmetic expression.
m1 = [1, 2; 3, 4]
m2 = [5, 6; 7, 8]
#
# Addition
#
x = m1 + m2
#> [6, 8; 10, 12]
#
# Subtraction
#
x = m1 - m2
#> [-4, -4; -4, -4]
#
# Element-wise multiplication
#
x = m1 * m2
#> [5, 12; 21, 32]
#
# Element-wise right division
#
x = m1 / m2
#> [0.2, 0.33; 0.4286, 0.5]
#
# Element-wise left division
#
x = m1 \ m2
#> [5.00, 3.00; 2.33, 2.00]
#
# Power
#
# scalar
a = 2
b = 5
x = a ** b
#> 32
# matrix and scalar
x = m1 ** 2
#> [7, 10; 15, 22]
# matrix and matrix
x = m1 ** m2
#> [1, 64; 2187, 65536]
#
# Negation
#
x = -m1
#> [-1, -2; -3, -4]
# Transpose
x = m1'
#> [1, 3; 2, 4]
Dot product
Dot product operation is supported for scalars (T), vectors (tensor<T, x>)
and matrices (tensor<T, x, y>). Dot product is not supported for nested tensors.
Dot product returns either a scalar or a tensor as the result.
Scalar part of the result type is inferred from operand base scalar types,
like in ordinary binary operations. Full result type depends on the combination
of operand types on which the operation is applied.
Operation a .* b is defined by the following rules:
- If both
aandbare vectors, the result is inner product of vectors - Vector dot product is a commutative operation
- Both vectors must be of the same length
- Result is sum of products of corresponding vector elements
(i.e.
a1 * b1 + ... + an * bn) - Result is a scalar
- If both
aandbare matrices, the result is matrix multiplication - Matrix multiplication is not a commutative operation
- Inner dimensions of matrices must be equal (i.e. matrix multiplication is defined
only for matrices of dimensions
A x BandB x C) - Result type is a matrix of dimensions
A x C - If one operand is a scalar, and the other is either a vector or a matrix, the result is element-wise multiplication by the scalar value
- This is not equivalent to element-wise multiplication of scalars and tensors, because nested tensors and tensors of arbitrary dimensions are forbidden
- If
ais a vector andbis a matrix, the result is sum product of vector and matrix elements per column - Length of vector must be equal to the number of matrix rows
- If
ais of lengthA,bmust be of dimensionsA x B - If
ais a matrix andbis a vector, the result is sum product of vector and matrix elements per row - Length of vector must be equal to the number of matrix columns
- If
bis of dimensionsA x B,amust be of lengthB
Following examples show usage od dot product:
a = 2
b = [1, 2, 3]
c = [
1, 2, 3;
4, 5, 6;
7, 8, 9
]
d = [
9, 8, 7;
6, 5, 4
]
r1 = a .* a # r1 = 4
r2 = a .* b # r2 = [2, 4, 6]
r3 = a .* c # r3 = [
# 2, 4, 6;
# 8, 10, 12;
# 14, 16, 18
# ]
r4 = b .* c # r4 = [30, 36, 42]
r5 = c .* b # r5 = [14, 32, 50]
r6 = d .* b # r6 = [46, 28]
r7 = d .* c # r7 = [
# 90, 114, 138;
# 54, 69, 84
# ]
Note
Rules 4. and 5. present simplified and specialized versions of Numpy rules 4. and 5. present in numpy.dot documentation. We found Numpy rules confusing for users and decided to implement dot product which covers currently needed use cases.
Range Expressions
Ranges present a special built-in syntax for describing bounded and unbounded sets
of values, with optional step for spacing. If step is omitted, elements are spaced by 1.
Ranges can be only of int type.
Ranges can be used in for loops and in tensor indexing. Support for using ranges for initializing arrays will be added in the future.
TML supports following range types:
| Name | Syntax | Example | Range | Note |
|---|---|---|---|---|
| Range from to | <expr>:<expr> |
1:10 |
[1, 10) | Can be used in for loop and indices |
| Range from | <expr>: |
1: |
[1, max) | Can be used only as an index for slicing |
| Range to | :<expr> |
:10 |
[0, 10) | Can be used only as an index for slicing |
| Range from step to | <expr>:<expr>:<expr> |
1:10:2 |
[1, 10), spaced by 2 | Can be used in for loop and indices |
| Range all | : |
a[1, :, :] |
Can be used only as an index for slicing |
Following example shows range expression usage in for loop:
for i = 1:10:
x = i + 1
end
for i = 1:10:2:
x = i + 1
end
Index Expression
Indexing expression allows referencing elements of a tensor. Number of indices must be equal to the number of tensor dimensions. Index expressions can be applied recursively to obtain elements of nested tensors. Index expressions can be nested arbitrarily.
When a scalar value is used as index, dimension is removed from an object (i.e. resulting objects present a hyperplane extracted from n-D space). When a range expression is used as index, dimension is retained while the length of the dimension is adjusted.
If a tensor has more than one dimension, indexing it using only one index as a plain
array of values is possible. Indexing a multidimensional tensor is possible only
using either the same number of indices or using one index.
If single index is used, tensors are represented as an array holding elements from
the left to the right (e.g. [1, 2, 3; 4, 5, 6] can be viewed as [1, 2, 3, 4, 5, 6]).
Note
Index expression can be ignored by the compiler if a scalar is indexed. As ignoring expressions with side effects would alter program execution semantics, having side effects in index expressions is not allowed.
Using such code would not be useful in practical terms, and enforcing such restrictions is necessary for avoiding subtle bugs and having predictable behavior.
Following example shows usage of index expression for basic indexing:
array = [1, 2, 3]
a = array[0] # a = 1
matrix = [
1, 2, 3;
4, 5, 6;
7, 8, 9
]
b = matrix[0, 0] # b = 1
b = matrix[5] # b = 6
nested_array = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9]
]
c = nested_array[0] # c = [1, 2, 3]
d = nested_array[0][0] # d = 1
Note
Index expression results in a slice of the original supported tensor. Slice is just a view to the supported vector allocated by the TML compiler.
Note
Octave-like append, insert and delete operations are not supported.
Arrays are static by nature.
Warning
Vectors cannot be used for indexing like in Octave (for now).
Indexing Terminal Groups
TML supports simple indexing terminals and groups in the following way:
a = t.in[0] # a is equal to the first input terminal
Indexing strings
Strings can be indexed in a similar way to how tensors are indexed.
Indexing str type returns char. Char is treated like a scalar and infinite
indexing is allowed (and ignored).
# x == "asd"
# x.type == str
# x[].type == char
# x[][][].type == char
if x == "asd":
a = 0
end
if x[0] == "a":
a = 0
end
if x[0][1][2] == "a":
a = 0
end
# y == "a"
# y.type == char
# y[].type == char
# y[][][].type == char
if y == "a":
b = 0
end
# indexing is ignored!
if y[0] == "a":
b = 0
end
# indexing is ignored!
if y[0][1][2] == "a":
b = 0
end
end
Relational operators
| Operator | Usage | Description |
|---|---|---|
> |
x > y |
true if x is greater than y |
>= |
x >= y |
true if x is greater than or equal toy |
== |
x == y |
true if x is equal toy |
< |
x < y |
true if x is less than y |
<= |
x <= y |
true if x is less than or equal to y |
!= |
x != y |
true if x is not equal to y |
Relational operations can be applied on boolean scalars and tensors. Broadcasting is applied if needed per logical and relational broadcasting rules. See the Broadcasting section for more information about when broadcast is applicable. Relational operators behave as element-wise operators.
# scalar
m1 = [1, 2; 3, 4]
x = m1 > 1
#> [0, 1; 1, 1]
# matrices
m2 = [5, 3; 1, 5]
x = m2 > m1
#> [1, 1; 0, 1]
m3 = [1, 1; 4, 4]
x = m1 == m3
#> [1, 0; 0, 1]
# vector
v1 = [1, 2]
x = m1 > v1
#> [0, 0; 1, 1]
If an expression results in a boolean tensor,
using all or any is necessary in selection statements.
fn foo() int:
a = [1, 2, 3]
b = [-3, -4, 5]
c = [-1, 2, 0]
if all(a > b):
# Not all elements of `a` are greater than elements of `b`.
# Thus this branch is skipped.
res = 1
elseif any(a > c):
# At least one element of `a` is greater than the corresponding
# element of `c`. Thus this branch is executed.
res = 2
else:
res = 3
end
return res # res is 2
end
Logical operators
Logical operations can be applied on boolean scalars and tensors. Broadcasting is applied if needed per logical, relational and bitwise broadcasting rules. See the Broadcasting section for more information about when broadcast is applicable. Logical operators in TML behave as element-wise operators.
TML supports the following logic operators that behaves as element-wise operators assuming implicit broadcasting of smaller operands.
| Operator | Usage | Description |
|---|---|---|
and |
x and y |
Applies logical-and on corresponding pair of elements of x and y |
or |
x or y |
Applies logical-or on corresponding pair of elements of x and y |
not |
not x |
Applies logical-not on elements of x |
The following examples shows how logic operators behave on scalars:
a = True
b = False
c = a and b # c = 0
c = a or b # c = 1
c = not a # c = 0
The following examples shows how logic operators behave on tensors:
a = [True, False, True]
b = [False, True, False]
c = a and b # c = [0, 0, 0]
c = a or b # c = [1, 1, 1]
c = not a # c = [0, 1, 0]
Bitwise operators
Bitwise operations can be applied on uint scalars and tensors.
Broadcasting is applied if needed per logical, relational and bitwise broadcasting rules.
See the Broadcasting section for more information
about when broadcast is applicable. Bitwise operators in TML
behave as element-wise operators.
TML supports the following logic operators that behaves as element-wise operators assuming implicit broadcasting of smaller operands.
| Operator | Usage | Description |
|---|---|---|
<< |
x << y |
Applies bitwise left shift of x bits by y count |
>> |
x >> y |
Applies bitwise right shift of x bits by y count |
^ |
x ^ y |
Applies bitwise exclusive or (XOR) on bits of corresponding pair of elements of x and y |
& |
x & y |
Applies bitwise-and on bits of corresponding pair of elements of x and y |
\| |
x \| y |
Applies bitwise-or on bits of corresponding pair of elements of x and y |
~ |
~x |
Applies bitwise bit flip of bits on elements of x |
The following examples shows basic bitwise operator usage:
uint a = 0u
uint b = 1u
c1 = a << b
c2 = a >> b
c3 = a ^ b
c4 = a & b
c5 = a | b
c6 = ~a
Neutral Elements
TML allows treating inline guarded optionally present variables as neutral elements in binary arithmetic, logical and bitwise expressions. An expression is treated as neutral element if all subexpressions are optional and missing. Relational and all unary expressions do not have neutral element. The following table shows left and right neutral elements of binary arithmetic operations.
| Operator | left neutral | right neutral |
|---|---|---|
.* |
not supported | not supported |
** |
not supported | 1 |
/ |
not supported | 1 |
* |
1 |
1 |
- |
not supported | 0 |
+ |
0 |
0 |
and |
true |
true |
or |
false |
false |
^ |
all bits 0 |
all bits 0 |
& |
all bits 1 |
all bits 1 |
\| |
all bits 0 |
all bits 0 |
<< |
not supported | 0 |
>> |
not supported | 0 |
Note
Optional expressions and neutral elements are not implemented as binary operations in the compiler. They are implemented as pruning missing expressions from code.
Operator Precedence
The following table contains arithmetic expression that TML supports, with their respective priority and associativity. Higher number represents higher priority.
| Priority | Operator | Note | Associativity |
|---|---|---|---|
| 14 | .<attr> |
Attribute access (e.g. a.len) |
left |
| 14 | [] |
Indexing operator (e.g. a[1], a[1][2]) |
left |
| 14 | () |
Parenthesis (e.g. (1 + 2) * 3) |
left |
| 13 | ' |
Transpose (e.g. a') |
left |
| 13 | ** |
Exponentiation (e.g. 2 ** 4) |
left |
| 12 | +, - |
Unary operators (e.g. +a, -a) |
right |
| 12 | ~ |
Bitwise unary NOT (e.g. ~a) |
right |
| 11 | *, /, %, \\, .* |
Binary operators (e.g. a * b) |
left |
| 10 | +, - |
Binary operators (e.g. a + b) |
left |
| 9 | <<, >> |
Binary bitwise shift operators (e.g. a >> b) |
left |
| 8 | ?: |
Elvis operator (e.g. a ?: 0, (a? + b?) ?: 0) |
left |
| 7 | <, <=, >, >=, ==, != |
Comparison operators (e.g. a == b) |
left |
| 6 | & |
Bitwise binary AND (e.g. a & b) |
left |
| 5 | ^ |
Bitwise binary XOR (e.g. a ^ b) |
left |
| 4 | \| |
Bitwise binary OR (e.g. a \| b) |
left |
| 3 | not |
Logical unary NOT (e.g. not a) |
right |
| 2 | and |
Logical binary AND (e.g. a and b) |
left |
| 1 | or |
Logical binary OR (e.g. a or c) |
left |
Note
Priority and associativity represent properties defined in the grammar.
For binary operations that are mathematically both left and right associative (e.g. addition, multiplication), associativity represents the shape of AST after parsing.
Expression evaluation represents a depth first left-to-right traversal of AST, so associativity defined in the grammar alters the evaluation order.