Skip to content
This repository has been archived by the owner on Aug 13, 2020. It is now read-only.

CalvinScript

Per Persson edited this page Mar 2, 2017 · 14 revisions

CalvinScript syntax

For a formal definition, see Appendix A

Basically, an application consists of actor instances and connections between the ports of the actor instances, forming a data flow graph. To allow reuse and clean scripts, it is also possible to define components. An application script can also contain deployment rules.

Data flow graph

The data flow graph is in all respects the application. In the figure below, the flow graph of a simple temperature sensing application is shown, followed by the corresponding CalvinScript source code.

A simple flow graph

# Actors
trigger : std.Trigger(tick=1, data=null)
sense : sensor.Temperature()
print : io.Print()

/* Connections */
trigger.data > sense.measure
sense.centigrade > print.token

The three first lines creates three actor instances, and the following two lines connects the actor ports into a flow graph. Also illustrated are the two types of comments supported; line comments (#) and multi-line comments (/* */).

Actor instantiation

trigger : std.Trigger(tick=1, data=null)
^         ^   ^       ^    ^
|         |   |       |    |
|         |   |       |    +-- argument
|         |   |       +------- argument identifier
|         |   +--------------- actor type
|         +------------------- namespace (required, except for components, see below)
+----------------------------- instance name

Actor names must start with a letter, followed by any number of letters, digits, and underscores. Arguments must be a valid value, and the type of values accepted is defined in the documentation for each actor.

Connection setup

trigger.data > sense.measure
^       ^    ^ ^     ^
|       |    | |     |
|       |    | |     +-- destination actor inport
|       |    | +-------- destination actor instance
|       |    +---------- flow marker (left-to-right)
|       +--------------- source actor outport
+----------------------- source actor instance

Constant definitions

At the beginning of a script constants can be defined:

define FOO = <value>

where value is any valid value, and constants can be used whereever a value is expected. Constant names are, by convention, written in upper case, but the convention is not strictly enforced. These constructs are sometimes referred to as implicit actors. Constant definitions have file scope.

Convenience syntax

There are a number of syntax constructs simplifying writing scripts and increasing the readability of the code. Be aware though, that in the actual flow graph the constructs will be represented by actors and connections as necessary.

Literal values on inports

CalvinsScript allows the use of literal values on inports as a convenience. In practice, that means that instead of writing:

src : std.Constant(data={"foo":1, "bar":2})
snk : io.Print()
src.token > snk.token

you can write:

snk : io.Print()
{"foo":1, "bar":2} > snk.token

Token value replacement

Sometimes you are only interested in the presence of a token, e.g. a token signaling an error condition, and want to convert it into a meaningful message. Instead of writing

...
cfy : std.Constantify(data="Hello!")

foo.token > cfy.in
cfy.out > print.token

you can write

foo.token > /"Hello!"/ print.token

where the value of tokens passed from the foo.token port will be (unconditionally) replaced by the string "Hello!" before it reaches the destination port.

Labels

When using literal values on inports or token value replacement actors will be autogenerated during deployment and given names that are unique, but unaccessible from the script. If you need to refer to an implicit actor, you can specify a label that will be used as the name for that actor. Just like actor names, labels must be unique to the current scope.

A label starts with a colon (:) which is followed by a valid actor name, and immediately preceeds the implicit actor construct.

 ...
 :greeting "Hello!" > print.token
 greeting.token > log.token

Port lists

By default fan-out from ports is allowed (as opposed to fan-in, except where excplicitly stated in an actor's documentation, see Fan-out and Fan-in). Instead of stating each fan-out connection on a separate line, it is allowed to list the inports on the right hand side of a connection.

42 > print.token, send.token, log.token

Components

It is possible to group actor instances and connections together in a component. A component behaves just like an actor, and allows for easy reuse and hiding of implementation details as well as an extension mechanism for Calvin.

Component definition

A component is defined as follows:

 # Delay will pass token unchanged from in to out after a given delay

 component Delay(delay) in -> out { <data flow graph> }
 ^         ^     ^       ^    ^      ^
 |         |     |       |    |      |
 |         |     |       |    |      +-- implementation, see below
 |         |     |       |    +--------- output port declarations (comma separated list, possibly empty)
 |         |     |       +-------------- input port declarations (comma separated list, possibly empty)
 |         |     +---------------------- argument identifier (comma separated list, possibly empty)
 |         +---------------------------- component type
 +-------------------------------------- keyword (component definition start)

Notice the lack of namespace in the definition: until a component is added to an actor store it is local to the script file.

To actually make the component useful, we have to define its internal data flow graph and its relation to the ports declared by the component. The internal data flow is described exactly as above, with the exception for the connections to/from the component ports.

component Delay(seconds) in -> out {
    """
    Documentation goes here…
    """
    delay : std.Delay(delay=seconds)

    .in > delay.token
    delay.token > .out
}

First thing to note is that actor names are scoped inside the component, i.e. there is no conflict with an actor named delay outside of the component.

Instantiation of actors is the same as before, but the connections are slightly to and from the component are different in that they do not follow an 'actor.port' pattern, but have unqualified ports, starting with a dot (.) followed by the name of a. The scope of the actor instances and argument identifiers used in the component is limited by the curly braces.

.in > delay.token
^     ^
|     |
|     +-- qualified port
+-------- unqualified port

Whenever an unqualified port is encountered, it is taken to refer to the ports declared by the component.

So, to use the component, we could write the following script:

component Delay(seconds) in -> out {
    """
    Documentation goes here…
    """
    delay : std.Delay(delay=seconds)

    .in > delay.token
    delay.token > .out
}

foo : Delay(seconds=0.1)
print : io.Print()
1 > foo.in
foo.out > print.token

One point to notice here is how arguments are propagated from the component to its constituent actors. The component definition defines the argument seconds which is passed as argument to the std.Delay instance, and its value is defined in when instantiating the Delay (foo : Delay(seconds=0.1)).

Once defined, a component can be used anywhere exactly as if it was an actor, including other component definitions.

Installing components

Components that are generally useful can be installed in the ActorStore under a suitable namespace using the csmanage tool. If the above component was installed under e.g. namespace custom we would be able to use it as follows:

foo : custom.Delay(seconds=0.1)
print : io.Print()
1 > foo.in
foo.out > print.token

Documentation for installed components will be available in the DocStore, and the description will be extracted from the docstring (triple-quoted string following the opening curly brace). The documentation for the input and output ports will be automatically derived from the "real" ports of the actors in the component definition.

Deployment definitions

The deployment definitions expresses rules for actor placement, replication, and scaling. While it is possible to keep all the rules with the script, only rules related to application behaviour should be kept in the script, and the rules pertaining to a specific deployment should be kept in a separate DeployScript file. In that way, the application can easily be reused in different deployments without change.

Deployment rules [alpha version]

Deployment rules is a feature in early development, and consequently the support is limited and the error handling is insufficient. In particular, the way that requirement are expressed is subject to change.

A simple rule can be defined as follows:

rule myrule: node_attr_match(index=["node_name", {"organization": "com.ericsson"}])
^    ^       ^               ^     ^
|    |       |               |     |
|    |       |               |     +-- argument
|    |       |               +-------- argument identifier
|    |       +------------------------ requirement expression
|    +-------------------------------- rule name
+------------------------------------- rule keyword

Rules can also be expressed as a combination of previously defined rules and requirement expressions using the & (and) and | (or) operators for intersection and union, respectively. The ~ (not) operator can be used in front of a requirement expression to obtain the inverted sub-set (this should be used with care). The resulting rule can then be combined in other rules and maintain the union grouping.

Requirement Expressions

The requirement expression types currently available are:

  • all_nodes() : The set of every runtime that we are allowed to access.
  • current_node() : The runtime the rule is evaluated on.
  • device_scaling() : Activate replication. Optional args max and min number of instances.
  • performance_scaling() : Activate parallelization. Optional args max and min number of instances.
  • node_attr_match() : The set of runtimes matching the mandatory index argument, which should be an attribute specification.

Defining groups

If rules are shared by a number of actors, it is convenient to define groups of actors (or other groups).

group my_group: an_actor, another_group
^     ^         ^
|     |         |
|     |         +------------ comma separated list of actors and/or groups
|     +---------------------- name of group
+---------------------------- group keyword

Applying rules [alpha]

The rules defined can be applied to actor and component instances or groups.

apply src, snk: myrule
^     ^         ^
|     |         |
|     |         +--------------- rule or requirement expressions
|     +------------------------- comma separated list of actor/component instance names or groups
+------------------------------- apply keyword

Replication rules

Replication refers to the ability to using an actor as a "template" and create copies of that actor in multiple locations, typically sensors for a particular quantity (not necessarily of the same kind or make) located at different physical sites within a building or installation.

trigger : std.Trigger(tick=60, data=null)
sense : sensor.Temperature()
collect : std.Collect()
print : io.Print()

trigger.data > sense.measure
sense.centigrade > collect.token
collect.token > print.token

rule ericsson: node_attr_match(index=["owner", {"organization": "com.ericsson"}])
rule lund_office: node_attr_match(index=["address", {"country": "Sweden", "locality": "Lund", "street": "Mobilvägen", "streetNumber": "12"}])
rule cover_building: ericsson & lund_office & device_scaling()

apply sense: cover_building

The application will now instatiate a temperature sensing actor on every temperature sensing device in Ericsson's office in Lund, Sweden.

Note the use of a std.Collect actor, that allows fan-in, between the sense and the print actors. Without it, the replication rule would not have any effect.

Also note that there is no need to specify a rule that sense should be replicated on runtimes with temparature sensing capabilities since that is an implicit requirement posed by the use of the sensor.Temperature() actor.

Scaling rules

Scaling refers to splitting a data path in multiple parallel paths in order to increase throughput. In the below example, the process actor will be cloned and the clones will be instantiated on runtimes other than the origin since the Calvin runtimes are single threaded and it would be pointless to instantiate more process actors on the same runtime.

source : custom.DataSource()
process : custom.Process()
collect : std.Collect()
print : io.Print()

source.data > process.in
process.out > collect.token
collect.token > print.token

rule ericsson: node_attr_match(index=["owner", {"organization": "com.ericsson"}])
rule parallelize: ericsson & performance_scaling(max=20)

apply process: parallelize

Note that we limit the potential runtimes used in scaling out to those owned by Ericsson, and that we limit the maximum number of parallel path to 20.

CalvinScript values

Finally, the types of arguments that CalvinScript understands are a superset of JSON:

  • number : integer or float, e.g. 42, -3.14.
  • string : double quoted text, e.g. "foo in the bar", consecutive strings are concatenated which is useful for preformatted sections of text.
  • literal string : string preceded by ! means no interpretation of escapes, e.g. !"([a-zA-Z][a-zA-Z0-9_]*\n)" is done.
  • list : a bracketed list, e.g. [1, 2, 3, "foo"]
  • dictionaries : a curly bracketed list with key/value pairs, e.g. {"key":"value", "foo":[1, 2, 3]}. Keys must be strings.
  • boolean : true or false
  • null : null
  • port reference : an expression &<actor>.<port>[<dir>], e.g. &collect.token[in] or &sense.centigrade, where the direction specifier is only needed if there is an ambiguity (in- and outports have the same name).

Appendix A. Formal syntax

keywords : component, define, voidport, rule, group, apply

N.B. The list of keywords may grow

tokens : COMMENT    (/\*(.|\n)*?\*/)|(\#.*)
         IDENTIFIER [a-zA-Z][a-zA-Z0-9_]*
         STRING     !?".*?"
         NUMBER     -?\d+(?:\.\d*)?
         DOCSTRING  """(.|\n)*?"""
         LPAREN     '('
         RPAREN     ')'
         LBRACE     '{'
         RBRACE     '}'
         LBRACK     '['
         RBRACK     ']'
         DOT        '.'
         COMMA      ','
         COLON      ':'
         GT         '>'
         EQ         '='
         RARROW     '->'
         SLASH      '/'
         AND        '&'
         OR         '|'
         NOT        '~'
         STAR       '*'
         FALSE      'false'
         TRUE       'true'
         NULL       'null

N.B. Keywords are not allowed as identifiers.

script                 ::= opt_constdefs opt_compdefs opt_program

opt_constdefs          ::=
                         | constdefs

constdefs              ::= constdefs constdef
                         | constdef

constdef               ::= DEFINE identifier EQ argument

opt_compdefs           ::=
                         | compdefs

compdefs               ::= compdefs compdef
                         | compdef

compdef                ::= COMPONENT qualified_name LPAREN identifiers RPAREN identifiers RARROW identifiers LBRACE docstring comp_statements RBRACE

docstring              ::=
                         | DOCSTRING

comp_statements        ::= comp_statements comp_statement
                         | comp_statement

comp_statement         ::= assignment
                         | port_property
                         | internal_port_property
                         | link

opt_program            ::=
                         | program

program                ::= program statement
                         | statement

statement              ::= assignment
                         | port_property
                         | link
                         | rule
                         | group
                         | apply

group                  ::= GROUP identifier COLON ident_list

ident_list             ::=
                         | ident_list identifier COMMA
                         | ident_list identifier

rule                   ::= RULE identifier COLON expression

expression             ::= expression predicate
                         | first_predicate

first_predicate        ::= identifier
                         | NOT identifier
                         | identifier LPAREN named_args RPAREN
                         | NOT identifier LPAREN named_args RPAREN

predicate              ::= setop identifier
                         | setop identifier LPAREN named_args RPAREN

setop                  ::= AND
                         | OR
                         | AND NOT
                         | OR NOT

apply                  ::= APPLY ident_list COLON expression
                         | APPLY STAR ident_list COLON expression

assignment             ::= IDENTIFIER COLON qualified_name LPAREN named_args RPAREN

opt_direction          ::=
                         | LBRACK IDENTIFIER RBRACK

port_property          ::= IDENTIFIER DOT IDENTIFIER opt_direction LPAREN named_args RPAREN

internal_port_property ::= DOT IDENTIFIER opt_direction LPAREN named_args RPAREN

link                   ::= outport GT port
                         | outport GT portlist
                         | outport GT void
                         | implicit_port GT port
                         | implicit_port GT portlist
                         | internal_outport GT inport
                         | internal_outport GT inportlist
                         | void GT inport
                         | void GT inportlist

link                   ::= internal_outport GT internal_inport

void                   ::= VOIDPORT

portlist               ::= portlist COMMA port
                         | port COMMA port

inportlist             ::= inportlist COMMA inport
                         | inport COMMA inport

port                   ::= inport
                         | internal_inport
                         | transformed_inport

transformed_inport     ::= SLASH argument SLASH port
                         | SLASH COLON identifier argument SLASH port

implicit_port          ::= argument
                         | COLON identifier argument

inport                 ::= IDENTIFIER DOT IDENTIFIER

outport                ::= IDENTIFIER DOT IDENTIFIER

internal_inport        ::= DOT IDENTIFIER

internal_outport       ::= DOT IDENTIFIER

named_args             ::=
                         | named_args named_arg COMMA
                         | named_args named_arg

named_arg              ::= identifier EQ argument

argument               ::= value
                         | identifier

identifier             ::= IDENTIFIER

string                 ::= STRING
                         | string STRING

value                  ::= dictionary
                         | array
                         | bool
                         | null
                         | NUMBER
                         | string
                         | portref

portref                ::= AND IDENTIFIER DOT IDENTIFIER opt_direction
                         | AND DOT IDENTIFIER opt_direction

bool                   ::= TRUE
                         | FALSE

null                   ::= NULL

dictionary             ::= LBRACE members RBRACE

members                ::=
                         | members member COMMA
                         | members member

member                 ::= string COLON value

values                 ::=
                         | values value COMMA
                         | values value

array                  ::= LBRACK values RBRACK

identifiers            ::=
                         | identifiers IDENTIFIER COMMA
                         | identifiers IDENTIFIER

qualified_name         ::= qualified_name DOT IDENTIFIER
                         | IDENTIFIER