Behavior specification
The Composition operators also specify behavior.
do
Define the behavior of a scenario
Directive
do <scenario-invocation>
See Scenario invocation for a description of <scenario-invocation>.
do is required within a scenario declaration or extension in order to define scenario behavior. Invoking a scenario causes its do member to be activated.
You define a scenario's behavior by invoking Foretify's built-in Composition operators, behavioral directives, library scenarios, and user-defined scenarios:
- Foretify's composition operators, actions and behavioral directives perform tasks common to all scenarios, such as implementing serial or parallel execution mode, adding information to a log file, or implementing time-related actions such as wait.
- Library scenarios describe relatively complex behavior, such as the vehicle.drive scenario and scenario modifiers, that let you control speed, distance between other vehicles and so on.
- By calling these scenarios in a user-defined scenario, you can describe more complex behavior, such as a vehicle approaching a yield sign or another vehicle moving at a specified speed.
In this manner, complex behavior is described by a hierarchy of scenario invocations.
Two operator scenarios commonly used to define scenario behavior are serial and parallel. See Composition operators for a description of these and other operators.
Within a scenario declaration or extension, use do once and only once in a scenario declaration or extension. Do not use do when invoking any nested scenario. For example, do is used below to invoke serial, but omitted when invoking turn and yield:
scenario vehicle.zip:
do serial():
t: turn()
y: yield()
See complete example.
To execute scenario zip, you need to extend top.main, again with do:
extend top.main:
car1: vehicle
do z: car1.zip()
See complete example.
However, if you extend vehicle.zip with do as in the following example, the earlier do statement in the vehicle.zip scenario is overridden. So, in this example, only intercept will execute.
extend vehicle.zip:
do top.intercept()
See complete example.
To execute a previously defined do block, you can use previous_do().
To make your code easily readable, create an explicit, meaningful label for the scenario invoked by do as well as all nested scenario invocations. Invocations without an explicit label are labeled automatically.
In the following example, there is an explicit label for the serial invocation at the root of the tree. The remaining invocations are labeled automatically. See Automatic label computation for how automatic labels (implicit labels) are computed.
extend top.main:
car1: vehicle
do a: serial(): # explicit label "a"
car1.drive() with:
s1: speed(0kph, at: start)
s2: speed(10kph, at: end)
See complete example.
previous_do()
Execute (and extend) a scenario's previously defined behavior
Scenario invocation
previous_do()
In the do block of a scenario extension, you can execute the previously defined do block using previous_do(). The result is that the do block of the previous extension is executed as if it were written in place.
If multiple extensions are loaded, the do block in the most recently loaded extension is the one executed by previous_do().
Any user defined labels in the previous do block are accessible inside the current do block. Therefore, labels defined in the current do block should not use the same name as labels in the previous one.
The following uses of previous_do() result in an error:
- Invoking previous_do() multiple times in a single do block.
- Invoking previous_do() outside of a do block.
- Invoking previous_do() when no previous behavior has been defined.
Note
If you want to replace the previously defined behavior, simply define the new behavior in a scenario extension without using previous_do().
In this example, the previous behavior of scenario3 is executed first. Then new behavior is added.
You can of course define new behavior first and then call previous_do() or you can use parallel to execute new behavior in parallel with the previous behavior.
scenario top.scenario1:
flag: bool
do log("Scenario 1 is executing")
extend top.scenario1:
do serial:
previous_do()
log("Extending scenario 1 behavior")
extend top.main:
do serial:
scenario1()
Here are the results of executing this example:
Foretify> run
Checking license...
Running the test ...
Loading configuration ... done.
[0.000] [MAIN] Executing plan for top.all@1 (top__all)
[0.000] [MAIN] Scenario 1 is executing
[0.000] [MAIN] Extending scenario 1 behavior
[0.020] [MAIN] Run finished
[0.020] [MAIN] Ending the run
[0.020] [SIMULATOR] stop_run()
Run completed
Scenario invocation
A scenario can only be invoked within an operator scenario or within a do clause of a scenario declaration.
Invoke a scenario
Scenario invocation
[<label-name>:] <scenario-name>(<param>*) [<with-block>]
<label-name>- (Optional) Is an identifier that has to be unique within the scenario declaration. If a label is not specified, an automatic label is created. See Automatic labels for an explanation of how automatic labels are computed.
<scenario-name>-
(Required) Is the name of the scenario you want to invoke, optionally including the path to the scenario. See Path operator and name resolution for an explanation of how names without explicit paths are resolved.
Note
Invoking the generic form of the scenario, such as vehicle.drive() is not allowed.
For more information on invocation parameters, see Invocation parameters.
<with-block>-
(Optional) <with-block> is a list of one or more keep() constraints or scenario modifiers, where the members are listed on separate lines as a block or on the same line as with: and separated by semi-colons. For example, the following two scenario invocations are the same:
OSC2 code: 'with' block syntax example 1ss2: some_scenario(z: 4) with: keep(x==3) keep(y==5)OSC2 code: 'with' block syntax example 2ss2: some_scenario(z: 4) with: keep(x==3); keep(y==5)Note
cover() definitions are not allowed in scenario invocations.
This example shows the declaration of a scenario called traffic.two_phases. do is required to define the behavior of two_phases, and it invokes the serial operator scenario. The nested scenario, car1.drive, whose behavior is defined as a library scenario, is invoked without do.
scenario traffic.two_phases: # Scenario name
# Define the cars with specific attributes
car1: vehicle with:
keep(it.color == green)
keep(it.vehicle_category == box_truck)
# Define the behavior
do serial():
phase1: car1.drive() with:
spd1: speed(0kph, at: start)
spd2: speed(10kph, at: end)
phase2: car1.drive() with:
speed([10..15]kph)
See complete example.
Invocation parameters
The Scenario invocation, Scenario modifier application, and Watcher invocation receive a list parameters.
The parameters are received in the form ([<param> [, <param>]*]). Here, each <param> is in the form [<field-name|event-name>:] [default] [<value>|<qualified-event>]. The descriptions of the fields within the <param> are provided below:
<value>is an expression representing a single value, range, or a path to an event.<qualified-event>is a qualified event.- The optional default keyword indicates that the field assignment can be overridden later using a constraint.
The list must be separated by a comma and enclosed in parentheses. It can be any of the following:
- Name-based (<field-name>: [default]
,…) - Order-based ([default] <value>,…)
- Mixed name-based and order-based
In order-based lists, the first value is assigned to the first field in the scenario, and so on.
In the list, a name-based parameter can follow an order-based parameter, or vice-versa. Thus, the following are valid and assign the same values:
- turn(x: 3, y: 5)
- turn(3, y: 5)
- turn(3, 5)
- turn(x: 3, 5)
When invoking an operator scenario, parentheses are allowed but not required. When invoking all other scenarios, parentheses are required.
Note
Passing an argument to a var field is not allowed and will result in a compilation error.
You can pass a qualified event as an argument to an event using the syntax <event-name>: <qualified-event>. The event parameters must be name-based. The default is not applicable to event parameters. <label>: <scenario-path>(<event-name>: <qualified-event>) is equivalent to on <qualified-event>: emit <label>.<event-name>.
call
Call a method
Directive
call <method>(<param>*)
<method>- (Required) Is the name of a declared method. If the method is not in the current context, the name must be specified as <path-name>.<method-name>
<param>*- (Required) Is a comma-separated list of method parameters.
extend top.main:
on @c.phase_essence_slow.start:
call sut.car.calculate_dist_to_other_car(c.cut_in_vehicle)
do c: sut.vehicle_cut_in_and_slow()
See complete example.
See also Call (invoke) a method.
emit
Emit an event in zero-time
Directive
emit <event-path>[(<param>+)]
<event-path>- (Required) Has the format [<path-expression>.]<event-name>.
<param>+- (Optional) Is a comma-separated list of one or more parameters defined in the event declaration in the form <param-name>: <value>. Passing parameters by position is not allowed.
extend top.main:
event second_after_pass_by
do parallel(start_to_start:[0..0]s, overlap:any):
onc: sut.oncoming()
smp: serial():
wait @onc.oncoming_vehicle.car_passing_by
wait elapsed(1s)
log_info("Sampling dut speed: $(sut.car.state.speed)")
emit second_after_pass_by
var dut_speed:=sample(sut.car.state.speed,
@second_after_pass_by) with:
cover(dut_speed_second_after_pass_by, expression: it,
unit:kph,range:[0..200], every:10)
See complete example.
on
Execute actions when an event occurs. on blocks can be used as scenario or modifier members.
Directive
on <qualified-event>[ with]:
<procedural-code>
<qualified-event>- (Required) See Qualified event for a description.
<procedural-code>- (Optional) Any procedural code that can be defined in a native method is also legal here. See Native methods for more information. When the on is defined with a with clause, the parameter it is available to the procedural code. That it parameter represents the event data.
scenario top.scenario1:
car1: vehicle
event near_collision
var near_collisions_count := 0
on @near_collision:
near_collisions_count = near_collisions_count + 1
logger.log_info("Near collision occurred.")
do serial:
car1.drive()
See complete example.
watcher and checker declarations
watcher
Instantiate a watcher that will emit intervals.
Watcher
watcher <name>[(<watcher-data>)] is <watcher-type>(<param>*)
<name>-
(Required) The instance name of the watcher.
<watcher-data>-
(Optional) The watcher instance will have a data field of the type watcher-data. The watcher-data has to be a legitimate watcher data type (derived from any_watcher_data). If the watcher-type already has watcher data defined for it, the expressed watcher-data has to be derived from it.
<watcher-type>-
(Required) The type of the watcher, defined by the watcher modifier statement. See watcher type for details.
<param>*-
(Optional) A list of zero or more parameters of the form [<field-name>:] <value> that pass values to the watcher fields. See the scenario-invocation parameters for more information.
For more information on invocation parameters, see Invocation parameters.
Instantiate a watcher of type <watcher-type> with the provided parameters. This watcher will emit intervals according to the <watcher-type>. If <watcher-data> is provided, the intervals will be of that type.
For more details, see Defining watchers and checkers.
The following example defines a watcher that creates intervals that record the maximal speed when the speed of the SUT vehicle exceeds some speed limit during the scenario.
# Define custom watcher data that records the maximum speed during
# an interval
struct speed_above_data inherits any_watcher_data:
max_speed: speed
record(max_speed, unit: kph)
# Define a watcher type that create intervals
# when the speed of a vehicle exceeds some speed limit,
# and records the maximal exceeding speed
watcher modifier speed_above(speed_above_data):
v: vehicle
limit: speed
on @top.w_clk:
if(data == null):
if(v.state.speed > limit):
start_interval()
else:
if(v.state.speed <= limit):
end_interval()
on @i_clock:
if(data.max_speed < v.state.speed):
data.max_speed = v.state.speed
global modifier vehicle.speed_watchers:
# Instantiate the watcher for all the vehicle actors
watcher speed_above_w is speed_above(actor, 30kph)
See complete example.
The following example defines an on-the-fly watcher that creates intervals that record the maximal speed when the speed of the SUT vehicle exceeds some speed limit during the scenario.
# Define watcher data that records a speed
struct speed_watcher_data inherits any_watcher_data:
speed: speed
record(speed, unit: kph)
extend top.main:
watcher above_speed(speed_watcher_data) is any_watcher_behaviour() # Instantiate the watcher
on @top.clk:
if(above_speed.data == null):
if(sut.car.state.speed > 30kph): # when the speed of the vehicle exceeds 30kph start the interval
above_speed.start_interval()
else:
if(sut.car.state.speed <= 30kph): # when the speed of the vehicle is less than 30kph end the interval
above_speed.end_interval()
on @above_speed.i_clock: # during the interval record the maximal speed
if(above_speed.data.speed < sut.car.state.speed): above_speed.data.speed = sut.car.state.speed
See complete example.
checker
Instantiate a checker that will emit violation intervals
Checker
checker <name>[(<watcher-data>)] is <watcher-type>(<param>*)
<name>-
(Required) The instance name of the checker.
<watcher-data>-
(Optional) The checker instance will have a data field of the type watcher-data. The watcher-data has to be a legitimate watcher data type (derived from any_watcher_data). If the watcher-type already has watcher data defined for it, the expressed watcher-data has to be derived from it.
<watcher-type>-
(Required) The type of the checker, defined by the watcher modifier statement. See watcher type for details.
<param>*-
(Optional) A list of zero or more parameters of the form [<field-name>:]
that pass values to the checker fields. See the scenario-invocation parameters for more information.
Instantiate a checker of type <watcher-type> with the provided parameters. This checker will emit intervals according to the <watcher-type>. If <watcher-data> is provided, the intervals will be of that type. The checker is used to validate the correct behavior of a part of the system. If that part does not behave as expected, the checker can emit an issue that will be captured by the error mechanisms of the system. The checker declaration exposes issue-related methods that are not available for watchers.
Checkers follow the same logic as watchers. You need to start the interval with start_interval() and end it with end_interval(). While the interval is active, you can call the checker methods to create an issue associated with that interval. When the interval is active, you can override the issue by calling subsequent calls to the checker methods. The issue will be released to the system only after the end_interval() is called. Once the interval ends, the checker issue goes through the checker verdict analysis followed by the general verdict analysis where the issue may be altered according to the analysis. The checker verdict analysis is utilized by the set_issue() modifier.
The checker API
The checker follows the same API as the watcher. In addition, there are specific methods to create and manipulate issues for the checker.
The checker methods use common parameters as follows:
severity: issue_severity- The severity of the issue.
`kind: issue_kind- The kind of the issue.
category: issue_category- The category of the issue.
details: string- The message of the issue.
normalized_details: string- Provide a canonical string for clustering issues. If not provided,
normalized_detailsis deduced automatically from the issue message.
sut_issue()
sut_issue(severity: issue_severity, kind: issue_kind, details: string, normalized_details: string = "")-
Issue an SUT issue with the provided severity.
sut_error()
sut_error(kind: issue_kind, details: string, normalized_details: string = "")-
Issue an SUT error.
sut_warning()
sut_warning(kind: issue_kind, details: string, normalized_details: string = "")-
Issue an SUT warning.
scenario_completion_error()
scenario_completion_error(kind: issue_kind, details: string, normalized_details: string = "")-
Issue a scenario completion error.
scenario_completion_warning()
scenario_completion_warning(kind: issue_kind, details: string, normalized_details: string = "")-
Issue a scenario completion warning.
other_issue()
other_issue(severity: issue_severity, kind: issue_kind, details: string, normalized_details: string = "")-
Issue an issue of category other.
other_error()
other_error(kind: issue_kind, details: string, normalized_details: string = "")-
Issue an error of category other.
other_warning()
other_warning(kind: issue_kind, details: string, normalized_details: string = "")-
Issue a warning of category other.
issue()
issue(category: issue_category, severity: issue_severity, kind: issue_kind, details: string, normalized_details: string = "")-
Issue a generalized issue.
message()
message(details: string, normalized_details: string = "")-
Set the message of the issue to a specific string.
The following example defines a checker that creates issues when the speed of an SUT vehicle exceeds some speed limit. The issues describes the maximal speed reached.
extend issue_kind: [too_fast] #define a new issue kind
# Define a watcher type that create intervals
# when the speed of a vehicle exceeds some speed limit
watcher modifier speed_above:
v: vehicle
limit: speed
var max_speed: speed
on @top.w_clk:
if(data == null):
if(v.state.speed > limit):
start_interval()
else:
if(v.state.speed <= limit):
end_interval()
on @i_clock:
if(max_speed < v.state.speed):
max_speed = v.state.speed
global modifier sut_vehicle.speed_checkers:
checker check_speed_above is speed_above(actor, 30kph) with:
it.sut_error(kind: too_fast,
details: "Vehicle $(it.v) exceeded maximal speed $(it.limit) reaching up to $(it.max_speed)")
See complete example.
set_issue
Perform verdict analysis on intervals
Modifier
set_issue(target_checker, condition, category, severity, kind, message)
target_checker-
(Required) The checker path you want to perform verdict analysis on. When no target_checker is provided, the change is performed on all checker issues that meet the condition.
condition-
(Required) A condition to be checked when the issue is handled. When the condition is not met, the set_issue() is ignored.
category: issue_category-
(Required) Change the issue category to this one. When no category is provided, the issue category is not changed.
severity: issue_severity-
(Required) Change the issue severity to this one. When no severity is provided, the issue severity is not changed.
kind: issue_kind-
(Required) Change the issue kind to this one. When no kind is provided, the issue kind is not changed.
message-
(Required) Change the issue message to this one. When no message is provided, the issue message is not changed.
The set_issue() modifier lets you modify issues coming from checkers. If target_checker is provided, only the issues coming from that checker are modified. If target_checker is not provided, the modification is performed on all checker-associated issues.
The modification is activated for issues only if the condition is met.
The following example changes a checker called s to issue an informative interval rather than an error.
extend top.main:
set_issue(target_checker: sut.car.speed_checkers.check_speed_above, severity: info)
See complete example.
synchronize
Synchronize the timing of two sub-invocations.
Directive
synchronize (slave: <inv-event1>
, master: <inv-event2>
[, offset: <time-exp>])
slave: <inv-event1>, master: <inv-event2>-
(Required) <inv-event1> is a scenario invocation event that you want to synchronize with <inv-event2> (another scenario invocation event).
An invocation event has the form <invocation-label>.<event>, where <invocation-label> is the label of some scenario invocation in the current scope.
offset: <time-exp>- (Optional) Is an expression of type time. It signifies how much time should pass from the master event to the slave event. If negative, the slave event should happen before the master event.
The synchronize() modifier accepts two events. You can synchronize more events by using multiple statements.
The synchronize() modifier is mainly used under parallel() to sync between scenarios within the top-level scenarios, and not for the top-level scenarios themselves.
until
End an invoked scenario when an event occurs.
Directive
until(<qualified-event>)
<qualified-event>- (Required) See Qualified event for a description.
# Example 1
do serial:
phase1: car1.drive() with:
speed(40kph)
until(@e1)
phase2: car1.drive() with:
speed(80kph)
until(@e1)
wait
Delay action until the qualified event occurs
Directive
wait <qualified-event>
<qualified-event>- (Required) See Qualified event for a description.
Note
Any scenario has these predefined events: start, end, fail.
do serial:
w1: wait @top.my_event
w2: wait @top.my_event if a > b
w3: wait (a > b)
See complete example.
do serial:
w1: wait elapsed([3..5]second)
# max is a field defined with type time and constrained to a value
w2: wait elapsed(max)
See complete example.
For additional guidelines on how to use wait, see Tips for the proper use of wait as an event-based constructs.
Methods
Foretify supports three types of methods:
- Expression methods simply assign the value of any legal OSC2 expression to the variable.
- Native methods are written in OSC2 and can manipulate lists, perform certain mathematical calculations and so on.
- External methods can access more complex functions written in an external language such as C++.
Describe and implement the behavior of an OSC2 object
Struct, actor, or scenario member
def <osc-method-name> (<param>*) [-> <return-type>] <method-implementation>
<osc-method-name>- (Required) Is an identifier that is unique in the current OSC2 context.
<param>*- (Optional) Is a list composed of zero or more arguments separated by commas of the following form. The parentheses are required even if the parameter list is empty.
<param-name>: <param-type> [= <default-exp>] <return-type>- (Optional) Specifies the type of the return parameter. Methods without a declared return type can only be invoked with the call directive in behavior specifications. They cannot be invoked in other expressions.
<method-implementation>-
(Required) Is one of:
- is [<qualifier>] undefined
- is [<qualifier>] expression <expression>
- is [<qualifier>] <procedural-code>
- is [<qualifier>] <bind-exp>
<qualifier> specifies an extension to a previously declared method. The method signature must be the same as the previous definition. <qualifier> is one of the following:
- also appends the specified method to the previously declared method(s).
- first prepends the specified method to the previously declared method(s).
- only replaces the previously declared method(s) with the specified method.
Undefined methods
The syntax for undefined methods is:
def <osc-method-name> (<param>*) [-> <return-type>] is [<qualifier>] undefined
undefined declares but does not implement a method. If an undefined method is called, it causes a runtime error.
def calculate_distance() is undefined
Expression methods
The syntax for methods that evaluate an expression is :
def <osc-method-name> (<param>*) [-> <return-type>] is [<qualifier>] expression <expression>
The <expression> to be evaluated can reference the named arguments of the method as well as all other fields that are in scope.
This expression method returns true if an integer is even.
def is_even(i: int) -> bool is expression (i % 2 == 0)
External methods
Foretify provides an interface for invoking OSC2 methods defined in C++.
The syntax for external methods is :
def <osc-method-name> (<param>*) [-> <return-type>] is [<qualifier>] external cpp([[name:] <c++-method-name>,] <shared-object-name>)
<c++-method-name>- (Required) Specifies the name for the C++ method. The name must be unique within the current context. When not set, the C++ method name is the same as <osc-method-name>.
<shared-object-name>- (Required) Specifies the shared object that contains the implementation of the C++ method. The name should include only the file name; the full library path is detected according to the operating system conventions. For example, in Linux, libraries are searched for in paths defined by the environment variable LD_LIBRARY_PATH.
This example shows how to declare an external method that accesses data stored in an external (not OSC2) entity, in this case, a C++ class. Fields of the external_data type cannot be generated, so they must be declared as variables with the keyword var.
extend vehicle:
var states: external_data
def set_states_on_lane_change(states: external_data)-> external_data is \
external cpp("set_states_on_lane_change", "car_states.so")
Call (invoke) a method
You can call methods using the call directive. If a method returns a value, you can use it in any place where an expression can be used, such as in constraint expressions, in qualified event expressions, in coverage computations and so on.
When called, these methods execute immediately in zero simulated time.
This example calls the external sut.car.calculate_dist_to_other_car() method at the start of the slow phase of cut_in_and_slow().
extend top.main:
on @c.phase_essence_slow.start:
call sut.car.calculate_dist_to_other_car(c.cut_in_vehicle)
do c: sut.vehicle_cut_in_and_slow()
See complete example.
Native methods
Native methods are implemented in OSC2 and do not refer to an external language implementation.
The syntax for defining a native method is:
def <name>(<param>*)[-> <return-type>] is [also|first|only]:
<procedural-code>
Note
The following language constructs can be used in any procedural code such as native methods or within an on blocks, within the init() method or the post_plan() method.
Declaring local variables
You can declare a typed variable that can hold values. There are two variants for variable declaration:
-
For an explicit type, use this syntax:
OSC2 code: explicit var declaration syntaxvar name: type[ = value] -
For an implicit type, use this syntax:
OSC2 code: implicit var declaration syntaxvar name := value
Local variables can be defined anywhere in the procedural code:
- A variable is known from the declaration until the end of that block.
- Only one variable with the same name can be defined in each block.
- Variables defined in an internal block hide other variables with the same name defined in external blocks.
- The top-most block of a method holds the parameters of the method as local variables. The body of the method opens an internal block, possibly containing local variables.
Using set to assign variables or fields
You can set a value for a local variable, for a global field or for a field in a struct instance that was passed to the method.
[set] <pathname> = <value>
Allocating objects with new
You can allocate structs or actors using the new operator in a variable declaration or when setting a value for a field.
-
In a variable declaration
OSC2 code: allocate object syntax in a variable declarationvar x: type = new -
Assigning a value to a field:
OSC2 code: allocate object syntax for assigning a value to a field[set] <pathname> = new
struct my_ints:
x: int
y: int
extend top.main:
def new_ints() -> my_ints is:
var w:= new my_ints
return w
Calling a method that does not return a value
[call]<method-pathname>
call logger.log_info("It's too late!")
Adding conditional flow control
You can add conditional flow control using this syntax:
if(expression):
...
[elif:
...]
[else:
...]
Note
Note that conditional flow can be used for triggering events, call functions and non-generatable field assignments. It cannot be used for generatable field assignments, since these are assigned only once at the beginning of a simulation cycle.
if(speed < 10kph):
slow_speed(speed)
if(speed > 50kph):
fast_speed(speed)
else:
car1.slow_speed = speed
if(t < 10s): call fast_time()
Return from the method
return [<exp>]
Result field
If the method has a return value, a field named result is defined, and its type is the same as the return value. At any point in the method, you can access the variable like any other variable (set it, include it in an expression, and more). If a method does not reach a return action, the value of result is returned. The result gets a default value according to its type.
Note
The method extension remembers the returned value from previously executed extensions and uses it as the initial value of the result field in the current method extension.
def simple() -> int is:
if(result > 10):
result = 16
def simple() -> int is first:
result = 12
Repeat loops
You can use a repeat to repeat a block of one or more actions using this syntax:
repeat:
<block>
To stop the repeat use keyword break.
def simple_repeat(iterations: uint) -> uint is:
var current := 0
var total := 0
repeat:
if current == iterations:
break
set current = current + 1
set total = total + 1
return total
For loops
You can use a for loop to set the values of items in a list using this syntax:
for <item-var>[, <index-var>] in <list-exp>
<block>
<item-var> is a variable defined inside the loop representing the current list item.
<index-var> is a variable defined inside the loop representing the current index of the item in the list.
The block is executed for each item in the list. The <item-var> and <index-var> are updated with the corresponding values.
var l: list of int = [1,2,3]
for curr_item in l:
if curr_item % 2 == 0:
logger.log_info("$(curr_item)")
Emitting events
You can emit events using the same syntax as the emit directive:
emit <event-path>[(<param>+)
See emit for details and an example.