-
Notifications
You must be signed in to change notification settings - Fork 91
CalvinScript
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.
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.
# 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 (/* */
).
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.
trigger.data > sense.measure
^ ^ ^ ^ ^
| | | | |
| | | | +-- destination actor inport
| | | +-------- destination actor instance
| | +---------- flow marker (left-to-right)
| +--------------- source actor outport
+----------------------- source actor instance
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.
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.
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
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.
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
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
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.
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.
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 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.
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.
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 argsmax
andmin
number of instances. -
performance_scaling()
: Activate parallelization. Optional argsmax
andmin
number of instances. -
node_attr_match()
: The set of runtimes matching the mandatoryindex
argument, which should be an attribute specification. -
runtime_name()
: Shorthand for runtime name only in anode_attr_match
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
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 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 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.
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
orfalse
- 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).
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