Process

metronom

Sequences of actions, such as the body of a group, of a whenever, of a loop, etc., are the specification of a thread of execution1 owning its own temporal properties and managed independently by the run-time. An Antescofo process simply abstracts this notion by parameterizing a sequence of actions. A process can be instantiated multiple times by giving a value to the parameters (aka process call).

Processes are similar to functions: after its definition, a function can be called to compute a value from its body (an expression). After its definition, a process can be called and run from its body which is a group of actions. This group is called the instantiation of the process. Notice that there can be several instantiations of the same process that run in parallel.

Processes can be defined using the @proc_def construct. For instance,

          @proc_def ::trace($note, $d)
          {
                 print begin $note
              $d print end $note
          }
The name of the process denotes a proc value (see Proc Values), and is used to call the process.

Calling a Process

A process call is a mechanism similar to a function call: its parameters are expressions listed between parentheses and their values are bound to the arguments before running the body.

A process can be called as an action or as an expression.

Calling a Process as an Action

Here, calling a process is an action:

          NOTE C4 1.3
            ::trace("C4", 1.3)
            ; more actions
          NOTE D3 0.5
            ::trace("D3", 0.5)

In the previous code, the process is referred to through its name, a ::-identifier, so it is apparent that a process is called. But the call can also be indirect, as in:

          NOTE C4 1.3
            $x := ::trace
            :: $x ("C4", 1.3)  ; calling the process assigned to $x

the process ::trace is a value (of type proc) assigned to variable $x. To call the process refered by $x, we use the syntax :: expression (...) where expression must evaluated to a proc.

A process call is a compound action equivalent to the insertion of the process body at the place of call as a group. So the previous code fragments launch the following behavior:

          NOTE C4 1.3
             group trace_body1 {
                   print begin "C4"
               1.3 print end "C4"
             }
             ; more actions
          NOTE D3 0.5
              group trace_body2 {
                   print begin "D3"
               0.5 print end "D3"
             }

Calling a Process as an Expression

A process can also be called in an expression with the same syntax. The instanciation mechanism is similar: a group starts and runs in parallel.

Calling a process in an expression does not require the :: marker, that is the expression: $x(1, 2, 3) can be either a process call or a function call, depending on the value of $x. It causes no trouble. The :: keyword is mandatory only in actions, because allowing the syntax of function calls would lead to ambiguities in the specification of actions.

In an expression, a process call returns an exec value as the result of the process instanciation. See section exec value. This value refers to the running group lauched by the process instanciation and can be used in the computation of the surrounding expression. Exec values are ordinary values that can be stored in a map or in a tab, be passed as an argument to a function or to another process, etc. w The exec value corresponding to a running instance of a process can also be retrieved from within the process body itself through a special variable

          $MYSELF

This variable is read-only and is managed by the run-time. It has a different value for each instance of a process.

Aborting a Process

The actions spanned by a process call constitute a group. It is possible to abort all groups spanned by the calls to a given process using the process name:

          abort ::P

will abort all the active instances of ::P. The active instances of the process are the instantiation of the process body that are still alive.

It is possible to kill a specific instance of the process using its exec value:

          $p1 := ::P()
          $p2 := ::P()
          $p3 := ::P()
          ; ...
          abort $p2  ; abort only the second instance
          abort ::P  ; abort all remaining instances

Using the special variable $MYSELF , it is possible to implement self-destruction on a specific condition e:

          @proc_def ::Q()
          {
                @local $PID
                $PID := $MYSELF
                ; ...
                whenever (e) { abort $PID }
                ; ...
          }

The value of $MYSELF is stored in the local variable $PID because the value of $MYSELF is the exec value of the immediatly surrounding group. If we replace$PID with $MYSELF in the whenever, we kill the running instance of the whenever body, not the process instance.

Process as Values

A process definition is a proc value and can be the argument of another process. For example:

          @Proc_def ::Tic($x) {
               $x print TIC
          }

          @proc_def ::Toc($x) {
               $x print TOC
          }

          @proc_def ::Clock($p, $q) {
               :: $p(1)
               :: $q(2)
             2 ::Clock($p, $q)
          }

A call ::Clock(::Tic, ::Toc) will print TIC one beat after the call, then TOC one beat after the latter, and then again at date 3, at date 4, etc.

Processes and (local) Variables

Processes are defined at the top-level. So, the body of the process can only refer to global variables and to local variables introduced in the body. The process parameters are implicit local variables but other local variables can be introduced explicitly using the @local statement.

Process parameters are local variables

The parameters of a process are local variables that are initialized with the value of the arguments given in the process call. These variables are local to the process instance. These variables can bet set in the process body, as in:

         @proc_def ::P($x)
         {
               whenever ($x == $x) {
                   print "$x = " $x
               }
               ; ...
               1 $x := $x + 1
         }

One beat after the call ::P(0), the text $x= 1 appears on the console: the variable $x local to this process instance has been incremented by one which has triggered the whenever outputting the message. We stress that values are passed to the process, not variables or expressions, which is sometime a cause of pitfalls, see paragraph What to Choose Between Macro, Functions and Processes.

Other than the initialization, there is no difference at all between the process parameters and a local variable introduced explicitly using @local statement.

Local variables

Variables that are defined @local to a process are defined per process instance: they are not shared with the other calls. One can access a local variable of a specific instance of a process through the exec value of this instance, using the dot notation:

          @proc_def DrunkenClock()
          {
               @local $tic
               $tic := 0
               Loop (1. + @random(0.5) - 0.25)
               { $tic := $tic + 1 }
          }
          $dclock := ::DrunkenClock()
          ; ...
          if ($dclock.$tic > 10)
          { print "Its 10 passed at DrunkenClock time" }

The left hand side of the infix operator . must be an expression whose value is an active exec. The right hand side is a variable local to the referred exec. If the left hand side refers to a dead exec (cf. exec), the evaluation of the dot expression raises an error.

In the previous example an instance of ::DrunkenClock is recorded in variable $dclock. This exec is then used to access to the variable $tic which is local to the process. This variable is incremented with a random period varying between 0.75 and 1.25.

Assignment using the dot notation

The reference of an instance of a process can be used to assign a variable local to a process from the outside, as for example in:

        @proc_def ::P()
        {
             @local $x
             whenever ($x} { print "$x has changed for the value " $x }
             ; ...
         }
         $p := ::P()
         ; ...
         $p.$x := 33

the last statement will change the value of the variable $x only for the instance of ::P launched at line 7 and this will trigger the whenever at line 4.

Notice that the features described here specifically for a process instance work for any exec (see sections Action As Expression and exec value).

Dynamically Scoped Variable

Accessing a local variable through the dot notation is a dynamic mechanism and the local variable is looked first in the instance referred by the exec, but if not found in this group, the variable is looked up in the context of the exec, i.e. in the instance of the group that has spanned the process call, and so on, climbing up the nesting structure (in case of recursive process, this structure is dynamic) until the variable is found. If the top-level context is reached without finding the variable, the <undef> value is returned and an error message is issued.

This mechanism is useful to dynamically access a variable defined in the scope of the call. For example:

        @proc_def ::Q($which) { print $which ": " ($MYSELF.$x) }

        $x := "x at top level"

        Group G1 {
              @local $x
              $x := "x local at G1"
              ::Q("Q in G1")
        }

        Group G2 {
              @local $x
              $x := "x local at G2"
              ::Q("Q in G2")
        }

        ::Q("Q at top level")

will print:

    Q in G1: x local at G1
    Q in G2: x local at G2
    Q at top level: x at top level

It is possible to specify that a process must always be launched in the top-level scope, even if the call appears in other contexts. The process definition must be labeled with the attribute @staticscope. With this attribute, only the global variables (in addition to the local variable of the process) can be accessed.

Recursive Process

A process may call other processes and can be recursive, calling itself directly or indirectly. For instance, an infinite loop

          Loop L 10
          {
               ; actions ...
          }

is equivalent to a call of the recursive process defined by:

          @proc_def ::L()
          {
               Group iterate { 10 ::L() }
               ; actions ...
          }

The group iterate is used to recursively launch the process without disturbing the timing of the actions in the loop body. In this example, the process has no arguments.

This example illustrate the idea of temporal recursion: the process calls itself with a delay. There is no base case (the base case in a classic recursive definition does not involve the recursive call) but a delay which postpones the computations. Without delay, the process loops infinitively by calling istelf on the same instant.

When a process is called, the new instance refers to the scope of the call, e.g. to retrieve its tempo (cf. next paragraph). It means that the scope surrounding the call must exists as long as the process is runing. In the case of a temporal recursion, it means that the whole chain of calls must be recorded. Beware there is a hard limit: only 15000 active processes are allowed simultaneously. This constraints restricts the possible use of temporal recursion. For instance, if a process launches itself every 100 ms, then it can run at max for 25 minutes (and actually less, because they are certainly other running processes).

However, if a process is tagged @staticscope, then its scope always refers to the top-level scope and the scope of the call site can be free without waiting the end of the launched process. In this case, the temporal recursion can run forever. The downside is that the launched process do not inherit the temporal scope of the call site (the default behavior).

Process, Tempo and Synchronization

The tempo of a process can be fixed at its definition using a tempo attribute:

          @proc_def ::P() @tempo := ...
          { ... }

In this case, every instance of ::P follows the specified tempo. If the tempo is not specified at the process definition, then the tempo of an instance is implicitly inherited from the call site (as if the body of the process was inserted as a group at the call site).

For example:

          Group G1 @tempo :=  60 { Clock(::Tic, ::Tic) }
          Group G2 @tempo := 120 { Clock(::Toc, ::Toc) }

will launch two clocks, one ticking every second, the other one tocking two times per second.

If not explicitly specified, the tempo of a process instance is inherited from the call site. This is also true for the other synchronization attributes (targets, strategies, etc.).

Actors

Actors are built on processes to provide autonomous objects that can react to external signals and able to answer requests initiated from other program parts. Actors do not belong to the Antescofo core: they do not add fundamental mechanisms, they rather provide syntactic sugar for the sake of the programmers and they are internally rewritten as processes. They are described in chapter actors.



  1. See the notion of exec and coroutine. A sequence of actions may span multiples threads, e.g. the body of a loop or of a whenever