Guards
Guards are a construct that enables asserting properties of variables in a given context. Code is conditionally pruned based on the guard condition being satisfied or not.
Guard blocks
Guarded blocks have a main and an else branch. All guarded variables must be used inside the main branch. No restrictions are applied to the else branch. For the main branch to be selected, all guarded variables must satisfy guard clause. Otherwise, else branch is selected. No assertions about variable properties are made inside else branch.
All type and access right checks are applied for all branches of guard blocks. This prevents unexpected errors that depend on the context of component usage.
Arbitrary nesting of guard blocks is allowed. Inner blocks inherit assertions from outer blocks. Nesting of opposite guard blocks can cause dead code, and a warning is raised in such cases. Errors for terminal access are not reported for unreachable guard blocks.
Guard context is inherited on function calls. This means that calling the same function can be allowed in one guard context, but not in the other.
Exists guard
While writing generic components using ? and ?: operators is advised, there are cases
in which specific logic is required depending on the concrete terminal configuration.
Exists guard comes in two opposite flavors: exists and not exists.
General form of exists guard is:
<not>? exists a, b, c:
<statements>
end
<not>? exists a, b, c:
<statements>
else:
<statements>
end
Following examples show usage of exists guard with and without else block:
fn test():
exists t.in:
t.out = t.in + 5
end
end
fn test():
exists t.in:
t.out = t.in + 5
else:
t.out1 = 10
t.out2 = 15
end
end
fn output_fnc():
exists t.opt_in:
test() # valid: 'exists' guard context is inherited
end
test() # error: unguarded access to optional
end
fn test():
x = t.opt_in + 1 # no error: errors are raised on calls from
# entry functions and global scope
end
Following examples show usage of not exists guard with and without else block:
fn test():
not exists t.in:
t.out1 = 10
t.out2 = 15
end
end
fn test():
not exists t.in:
t.out1 = 10
t.out2 = 15
else:
t.out = t.in + 5
end
end
fn output_fnc():
not exists t.opt_in:
test() # error: 'not exists' guard context is inherited
# and function is accessing t.opt_in without 'exists' guard
end
test() # error: unguarded access to optional
end
fn test():
x = t.opt_in + 1 # no error: errors are raised on calls from
# entry functions and global scope
end
Feedthrough guard
Depending on dynamically configurable feethrough property of terminals, code might need
to either be pruned or placed dynamically inside output_fnc or update_fnc.
To support this, TML has a guard block similar to exists which is called
feedthrough guard.
Feedthrough guard comes in two opposite flavors: feedthrough and not feedthrough.
Unlike exists guard which supports guarding all variables, only terminals can be guarded
using feedthrough guard, as only terminals have feedthrough property.
General form of feedthrough guard is:
<not>? feedthrough t.in1, t.in2:
<statements>
end
<not>? feedthrough t.in1, t.in2:
<statements>
else:
<statements>
end
Following examples show usage of feedthrough guard with and without else block:
fn test():
feedthrough t.in:
t.out = t.in + 5
end
end
fn test():
feedthrough t.in:
t.out = t.in + 5
else:
t.out1 = 10
t.out2 = 15
end
end
Following examples show usage of not feedthrough guard with and without else block:
fn test():
not feedthrough t.in:
t.out = t.in + 5
end
end
fn test():
not feedthrough t.in:
t.out = t.in + 5
else:
t.out1 = 10
t.out2 = 15
end
end
If same piece of code should be moved dynamically between output_fnc or update_fnc,
it is advised to separate common logic to a different function and call it from
corresponding feedthrough guard blocks.
x = 0
fn output_fnc():
feedthrough t.in:
move()
end
end
fn update_fnc():
not feedthrough t.in:
move()
end
end
fn move():
x = t.in + 5
end
Inline guards
Inline guards are a more concise way to guard specific simple, but common use patterns. All inline guards have equivalent guard blocks. Note that the opposite does not hold, as guard blocks are a more general mechanism. While guard blocks allow for pruning arbitrary statements, inline guards are restricted to simple variable expressions and assignment statements.
As with guard blocks, inline guards allow arbitrary nesting. Nesting inline guards inside guards blocks, and vice versa (possible only via function calls) is allowed. Warnings are raised for redundant inline guards and unreachable code.
Variables
Optionally present variables can be guarded using inline guard operator ?.
This is equivalent to using exists/else, but more flexible when possibly different
combinations of terminals are not present. Only optional variables can be guarded using
inline guard operator ?. If not present, value of variable subexpression
is treated as neutral element for the outer expression.
If outer expressions support both left and right neutral elements, and both subexpressions are missing, value of outer expression is treated as neutral element in it's enclosing outer expression.
Only certain binary expressions support neutral elements. Optionally present expressions
are not assignable in any context (i.e. cannot be assigned to variables, used as arugment
value of in return statement, etc.).
Following examples show usage of inline guard operator ?.
x = a? + b? # Incorrect, final value is optional
y = (a? + b?) ?: c # Correct, final value is given through elvis operator
z = a? + b? + c # Correct, final value is given through c which
# is always present, so expression is defined
Elvis operator
Sometimes, treating missing variables and expressions as neutral elements is not desirable or not possible. Examples would be needing a concrete fallback default value or using optional expressions in assignment contexts (which is not allowed without using other guard mechanisms, mainly elvis operator).
Elvis operator is a special variant of binary operator. Left expression must be optional, while right expression must not be optional. If left expression is present, it is selected and right branch is pruned. Otherwise, right branch is selected and left branch is pruned. Elvis operator always returns value that is present in all contexts.
Left expression can either be expression of optional values or a single variable.
If left expression consist of only a single variable, using inline guard ? on
variable is redundant.
Elvis operator is equivalent to inverted not exists/else guard block.
Right branch is eqiuvalent to not exists, while left branch is equivalent to else.
Following examples show usage of elvis operator ?:.
x = a ?: b # Note: a must be optional
x = a? ?: b # Note: ? is redundant
y = (a? + b?) ?: c # Correct
z = (a? + b? + c) ?: 0 # Incorrect: left expression is not optional
z = (a? + b?) ?: c? # Incorrect: right expression is optional
Assignments
Common use pattern of exists guard is assigning a value to an optionally present
variable. For better code readability, it is advised to use inline guarded assignment
statement, if only single assignment statement is guarded.
Inline guarded assignment is equivalent to exists, but applied only to the
assignment statement.
Following examples show usage of inline guard operator ? in assignments.
x? = 5 # Correct
y? = a? + b? # Incorrect: value must be defined regardless
# of inline guard of assignment
Declarations
Declaring variables with type derived from terminals is allowed. As terminals can be optionally present, derived variables can be optionally present as well. While derived type does not need be directly equal to type of the terminal (i.e. derived type can be nested inside custom tensor type or nested type can be unwrapped), optional presence of a variable is directly inherited.
If source variable type is guarded using inline guard operator, it is assumed that derived variable is optional as well. Is source variable type is not inline guarded, it is assumed that it will be guarded in the outer context and that derived type is always present and defined.
If variable type is derived from inline guarded variable, inline guarding declaration statement is required for clarity as well. Behavior is equivalent to inline guarded assignment statement.
Following examples show usage of inline guard operator ? in declarations.
t.in?.type a? = 1 # Correct
t.in.type a = 2 # Correct
t.in.type a? = 1 # Incorrect: variable name is inline guarded,
# while type is not inline guarded
t.in?.type a = 1 # Incorrect: type is inline guarded,
# while variable name is not inline guarded