Terminals
Terminals provide a mechanism for a component to receive input and emit output.
Input and output terminals of different components are linked together to form a model
of a system.
Terminals are represented as variables belonging to the built-in namespace t.
As with variables, all typing rules described in Type system
and Broadcasting sections are applied.
Unlike user defined variables, terminals can be optionally present, depending on the context of component usage. Additional field that describes terminals is their feedthrough. Terminal access rules are defined in the following table.
| variable type |
feedthrough | init_fncread |
init_fncwrite |
update_fncread |
update_fncwrite |
output_fncread |
output_fncwrite |
|---|---|---|---|---|---|---|---|
| input | ✓ | ✓ | ✓ | ✓ | |||
| input | ✓ | ✓ | |||||
| output | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | |
| output | ✓ | ✓ | ✓ | ✓ | ✓ |
Callee functions inherit terminal access rules of the context in which they
are called. This means that calling a function might be possible inside output_fnc,
but not inside update_fnc. This also means that calling a function might be allowed
inside one guard block, but not inside another one. Access right inheritance is
shown in the following examples.
x = 0
fn output_fnc():
test() # unguarded call might raise an error if
# t.out is a non-feedthrough terminal
feedthrough t.out:
test() # this code cannot raise an error,
# as feedthrough property is assumed
end
end
fn update_fnc():
test() # unguarded call might raise an error if
# t.out is a feedthrough terminal
not feedthrough t.out:
test() # this code cannot raise an error,
# as not-feedthrough property is assumed
end
end
fn test():
t.out = x + 1
end
x = 0
fn output_fnc():
test() # this call is allowed!
end
fn update_fnc():
test() # this call is not allowed!
end
fn test():
t.feedthrough_out = x + 1
end
Dynamic terminals
Configuration of terminals can depend on the context of component usage. Terminals can be allowed to be optionally present and number of their instances can vary, but TML has mechanisms for writing generic components and handling different configurations.
Optional terminals
Optional terminals can be present depending on the context. Optional presence of terminals
is explicitly specified in model configuration. Access to optional terminals must be
guarded using ? and ?: operators or exists guard.
Both single terminals and terminal groups can be optionally present.
Terminal groups
Terminal groups represent a named array of elements of the same name. Terminal groups are used for implementing configurable components with variable width of input and output (ex. adder with variable number of operands). Length of the array represents the number of terminal instances. Terminal groups allow writing generic components independent of the total number of single terminal instances. TML supports uniform and non-uniform terminal groups.
Elements of a uniform terminal group must be of types that can be broadcast to a common type. Uniform group is an array of elements of a common type created by broadcasting. For more details, see section Broadcasting.
Not all components require or expect their terminal types to be altered by broadcasting.
In such components, using non-uniform terminal groups is preferred. Non-uniform terminals
are accessed using group name and a static index. Static index can either be a literal
or an index from macro for. For more details, see section Macro system.
Note
Currently, only internal library components can use terminal groups. TML function will support terminal groups in the future.
Terminal initialization
Using a variable that is not initialized is a common source of bugs. To prevent this, TML does not allow declaration of variables without value. To further prevent bugs, TML analyses code to determine if a terminal was initialized in current simulation cycle prior to reading it, and to determine if all output terminals were initialized at the end of simulation cycle.
This is achieved by analyzing program's control flow graph (CFG). This is a graph that
represents all possible execution paths of a program. Guard blocks are selected and
pruned based on current component configuration. Dynamic control flow statement
(if, for and while) behavior is approximated by considering all theoretically
possible paths, not regarding expression values.
Warning
Different terminal configurations can yield different errors and warnings due to static pruning of guard blocks. Special care and testing is needed if terminal initialization depends on guarded code.
General rule is that a terminal is considered initialized if it is initialized on all incoming paths to a point in code. Terminal initialization state can be none, partial or full.
Variable assignment (x = ...) marks terminal as fully initialized.
If a terminal is a tensor, tensor assignment(x[index] = ...) marks it as partially
initialized, but only if it was previously uninitialized.
If index access broadcasting is applied,
tensor assignment markse terminal as fully initialized. Shorthand assignment
(x += ...) does not alter initialization state, as it depends on the terminal itself.
When joining separate paths, smaller value is chosen. If terminal is not initialized only on a single path, it is considered as not initialized. Also, if a terminal is partially initialized only on one path and fully on all other paths, it is considered as partially initialized. Once initialized, terminal stays initialized until a point in which multiple paths meet.
Errors are raised if a terminal is not initialized at the end of simulation cycle or if reading a terminal that is not initialized is attempted.
Warnings are raised if a terminal is partially initialized at the end of simulation cycle or if reading a terminal that is partially is attempted.
Terminal initialization analysis behaves according to the following rules:
- All terminals are considered uninitialized at the beggining of a simulation cycle, not regarding values from the previous cycle
- Once a terminal is initialized, it stays initialized until the end of the statement block that it belongs to, or until the first jump statement
- After a jump statement, a terminal is initialized if it was initialized on all paths from which the point is reachable
- After
ifstatement, a terminal is initialized if it was initialized prior to the statement or is initialized in all branches andifcontains finalelsestatement - After
forloop proven to be executed at least once, a terminal is initialized if it was initialized prior to the statement or it is initialized on all possible loop exits forloop is proven to be executed at least once if range expression contains only number literals, namespace variables, constant properties or attributes as direct subexpressions (i.e.0:t.out.len, not(1 + 2):t.out.len) and range is properly formed (i.e. start value is smaller than end value)continue,breakand the last statement in loop's statement block (ifcontinueandbreakare omitted or if otherwise reachable) are considered as possible loop exits- After
forloop not proven to be executed at least once, a terminal is initialized only if was initialized before the statement - After
whileloop, a terminal is initialized only if was initialized before the statement - Every function call is a separate context that can both observe and alter different terminal initialization state
- After a function call, a terminal is initialized if it was initialized prior to the function call or if it is initialized on all possible function exits
- Explicit
returnstatements and the last statement in function (ifreturnis optional and omitted or if otherwise reachable) are considered as function exits - In expressions, subexpressions that follow a function call in evaluation order see terminal initialization state as a side-effect
Warning
A common pattern of usage is that all terminals in an array are initialized in a for
loop. This was the reason for introducing partial initialization and loop execution
check. Such permissiveness can cause bugs if used without special care.
TML users must pay special attention or resort to using full initialization.
Following examples show how terminal initialization analysis works.
fn output_fnc():
e = t.out + 1 # t.out not initialized,
# error is raised
t.out[1] = 1
a = t.out + 2 # t.out partially initialized,
# only warning is raised
t.out = 2
b = t.out + 3 # t.out fully initialized
end
fn output_fnc():
if a < b:
t.out = 1
end
e = t.out + 1 # t.out not initialized,
# error is raised
end
fn output_fnc():
if a < b:
t.out = 1
else:
t.out = 2
end
e = t.out + 1 # t.out fully initialized
end
fn output_fnc():
# static range
for i=0:10:
t.out[i] = 1
end
a = t.out + 1 # t.out partially initialized,
# a warning is raised
# static range
for i=0:10:
t.out = 1
end
b = t.out + 1 # t.out fully initialized
end
fn output_fnc():
# dynamic range
for i=a:b:
t.out[i] = 1
end
b = t.out + 1 # t.out not initialized,
# error is raised
end
fn output_fnc():
while a < b:
t.out = 1
end
e = t.out + 1 # t.out not initialized,
# error is raised
end