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 Jul 7, 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 [alpha-version]

Deployment definitions 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.

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

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.
  • runtime_name() : Shorthand for runtime name only in a node_attr_match

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

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         '|'
         UNOT        '~'
         FALSE      'false'
         TRUE       'true'
         NULL       'null,
         AT         '@'

N.B. Keywords are not allowed as identifiers.

script                  ::= opt_constdefs opt_compdefs opt_program

empty                   ::= 

opt_constdefs           ::= constdefs
                          | empty

constdefs               ::= constdefs constdef
                          | constdef

constdef                ::= DEFINE identifier EQ argument

opt_compdefs            ::= compdefs
                          | empty

compdefs                ::= compdefs compdef
                          | compdef

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

docstring               ::= DOCSTRING
                          | empty

comp_statements         ::= comp_statements comp_statement
                          | comp_statement

comp_statement          ::= assignment
                          | port_property
                          | internal_port_property
                          | link

opt_program             ::= program
                          | empty

program                 ::= program statement
                          | statement

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

assignment              ::= IDENTIFIER COLON qualified_name LPAREN named_args RPAREN

opt_direction           ::= LBRACK IDENTIFIER RBRACK
                          | empty

port_property           ::= qualified_port opt_direction LPAREN named_args RPAREN

internal_port_property  ::= unqualified_port opt_direction LPAREN named_args RPAREN

link                    ::= void GT void

link                    ::= real_outport GT void
                          | void GT real_inport_list
                          | real_outport GT inport_list
                          | implicit_outport GT inport_list
                          | internal_outport GT inport_list

void                    ::= VOIDPORT

inport_list             ::= inport_list COMMA inport
                          | inport

real_inport_list        ::= inport_list COMMA real_inport
                          | real_inport

inport                  ::= real_or_internal_inport
                          | transformed_inport

transformed_inport      ::= port_transform real_or_internal_inport

implicit_outport        ::= argument
                          | label argument

real_or_internal_inport ::= real_inport
                          | internal_inport

opt_tag                 ::= AT tag_value
                          | empty

tag_value               ::= NUMBER
                          | STRING

real_inport             ::= opt_tag qualified_port

real_outport            ::= qualified_port

internal_inport         ::= unqualified_port

internal_outport        ::= unqualified_port

port_transform          ::= SLASH argument SLASH
                          | SLASH label argument SLASH

qualified_port          ::= IDENTIFIER DOT IDENTIFIER

unqualified_port        ::= DOT IDENTIFIER

label                   ::= COLON identifier

named_args              ::= named_args named_arg COMMA
                          | named_args named_arg
                          | empty

named_arg               ::= identifier EQ argument

argument                ::= value
                          | identifier

opt_argnames            ::= argnames
                          | empty

argnames                ::= argnames COMMA IDENTIFIER
                          | IDENTIFIER

identifiers             ::= identifiers COMMA identifier
                          | identifier

identifier              ::= IDENTIFIER

string                  ::= STRING
                          | string STRING

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

bool                    ::= TRUE
                          | FALSE

null                    ::= NULL

dictionary              ::= LBRACE members RBRACE

members                 ::= members member COMMA
                          | members member
                          | empty

member                  ::= string COLON value

values                  ::= values value COMMA
                          | values value
                          | empty

array                   ::= LBRACK values RBRACK

qualified_name          ::= qualified_name DOT IDENTIFIER
                          | IDENTIFIER

define_rule             ::= RULE identifier COLON rule

rule                    ::= rule AND rule
                          | rule OR rule

rule                    ::= UNOT rule

rule                    ::= LPAREN rule RPAREN

rule                    ::= identifier
                          | predicate

predicate               ::= identifier LPAREN named_args RPAREN

apply                   ::= APPLY identifiers COLON rule

group                   ::= GROUP identifier COLON identifiers