Skip to content

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_fnc
read
init_fnc
write
update_fnc
read
update_fnc
write
output_fnc
read
output_fnc
write
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:

  1. All terminals are considered uninitialized at the beggining of a simulation cycle, not regarding values from the previous cycle
  2. 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
  3. After a jump statement, a terminal is initialized if it was initialized on all paths from which the point is reachable
  4. After if statement, a terminal is initialized if it was initialized prior to the statement or is initialized in all branches and if contains final else statement
  5. After for loop 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
  6. for loop 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)
  7. continue, break and the last statement in loop's statement block (if continue and break are omitted or if otherwise reachable) are considered as possible loop exits
  8. After for loop not proven to be executed at least once, a terminal is initialized only if was initialized before the statement
  9. After while loop, a terminal is initialized only if was initialized before the statement
  10. Every function call is a separate context that can both observe and alter different terminal initialization state
  11. 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
  12. Explicit return statements and the last statement in function (if return is optional and omitted or if otherwise reachable) are considered as function exits
  13. 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