PK {_O,ud d storyscript-stable/index.html
Welcome to Storyscript’s development documentation. These docs are intended for those who want to contribute to Storyscript itself or those working on an execution engine for the language.
Storyscript is a domain-specific language that uses EBNF for the grammar definition and a LALR parser. The abstract tree is then compiled to JSON, which will be used by an execution engine to perform the operations described in the original story.
Unlike other languages, compiling and execution in Storyscript are separated. The services are defined only at run-time at the engine’s discretion. In a way, you could say that what Storyscript really does is to convert a story in a machine-friendly format.
For example, the Asyncy engine uses services as docker containers, so when a service is encountered “docker run service-name” is executed by the engine.
EBNF stands for “Extended Backus-Naur form”, and is a language that can define the syntax for other languages. It looks like this:
boolean: TRUE | FALSE
number: INT | FLOAT
TRUE: "true"
FALSE: "false"
INT.2: "0".."9"+
FLOAT.2: INT "." INT? | "." INT
If you are curious, you can have at look at Storyscript’s full EBNF definition with the grammar command:
storyscript grammar
LALR stands for “Look-Ahead LR” parser and is what actually parses a story into its tree.
Fork the main repository from https://github.com/asyncy/storyscript
Storyscript is a Python project. The suggested installation is inside a virtual environment:
virtualenv --python=python3.6 storyscript
Change directory and activate the virtual environment:
cd storyscript
source bin/activate
Clone your fork:
git clone git@github.com/asyncy/storyscript.git
Cd in storyscript, and install it:
python setup.py install
Check the installation:
storyscript --version
You have succesfully installed Storyscript! You might need to install development dependencies as well:
pip install tox pytest pytest-mock
Check you can run the tests:
pytest
tox
You are now ready to start contributing to Storyscript!
A command line interface is provided.
The grammar command provides a simple way to check the current EBNF grammar:
> storyscript grammar
...
boolean: TRUE| FALSE
null: NULL
number: INT| FLOAT
...
The lex command print a list of all the tokens in a story:
> storyscript lex hello.story
...tokens list
The parse command returns the internal representation of the abstract syntax tree (AST) after the parsing phase:
> storyscript lex hello.story
...tokens list
The compile command compiles stories:
> storyscript parse hello.story
Script syntax passed!
A JSON output of the compilation is available:
> storyscript parse -j hello.story
{
"stories": {
"one.story": {
"tree": {
...
It’s possible to specify an EBNF file, instead of using the generated one. This is particularly useful for debugging:
> storyscript parse --ebnf-file grammar.ebnf hello.story
Reference for the current syntax
Strings can be declared with single or double quotes:
color = 'blue'
color = "blue"
Escaping is done with backslashes:
funky = ".\"."
Strings can reference existing variables with templating. In curly brace blocks, variables can be referenced:
where = "Amsterdam"
message = "Hello, {where}!"
Lists are a set of elements with guaranteed order:
colours = ["blue", "red"]
A list can be defined over more lines:
colours = [
"blue",
"red",
]
Elements are accessed by index:
blue = colours[0]
An unordered collection of elements, accessable by key:
colours = {'red': '#f00', 'blue': '#00f'}
Keys can be variables:
colour = 'red'
colours = {colour: '#f00'} # equal to {'red': '#f00'}
Objects can be access with dot notation or by key index:
colours.red
colours['red']
Regular expressions are defined with slashes:
pattern = /^foo/
Flags are supported:
pattern = /^foo/i
An arithmetical operation between values. Addition (+), subtraction (-), multiplication (*), divison (/), power (^), modulus (%) are supported:
a = 1 + 2
a = 1 - 2
a = 1 * 2
a = 1 / 2
a = 1 % 2
A logical operation between values. The logical and, or and the unary negation (!) are supported:
c = a and b
c = a or b
c = !a
A comparison between values. The equal (==), not equal (!=), greater (>), greater or equal (>=), less (<) and less or equal (<=) relation are supported:
foo == bar
foo != bar
foo > bar
foo >= bar
foo < bar
foo <= bar
Program flow can be controlled with if conditional blocks:
if foo
bar = foo
else if foo > bar
bar = foo
else
bar = foo
Iteration over lists can be done with foreach:
foreach items as item
# ..
And over objects:
foreach object as key, value
# ...
Functions allow to write repeatable sub procedures:
function sum a:int b:int returns int
x = a + b
return x
The output declaration is optional:
function sum a:int b:int
# ...
Calling a function requires parentheses:
sum (a:1 b:2)
Services can be called with a <service-name> <command-name> <arguments>* expression:
result = service command key:value foo:bar
Arguments with the value equal to the argument name can be shortened:
# instead of: service command argument:argument
service command :argument
When a service provides a stream, the service+when syntax can be used. This could be an http stream, a stream of events or a generator-like result:
service command key:value as client
when client event name:'some_name' as data
# ...
Exceptions can be handled with try:
try
x = 0 / 0
Exceptions can be caught:
try
x = 0 / 0
catch as error
alpine echo message:"caught!"
Finally can be used to specify instructions that are always executed, regardless of the try’s outcomet:
try
x = 0 / 0
finally
a = 1
Inline expressions are a shorthand to have on the same line something that would normally be on its own line:
service command argument:(service2 command)
To import another story and have access to its functions:
import 'colours.story' as Colours
The compiler takes care of transforming the tree to a dictionary, line by line. Additional metadata is added for ease of execution: the Storyscript version and the list of services used by each story:
{
"stories": {
"hello.story": {
"tree": {...}
"services": ["alpine"],
"functions": {
"name": "1"
},
"version": "0.5.0"
},
"foo.story": {
"tree": {...},
"services": ["twitter"],
"version": "0.5.0"
}
},
"services": [
"alpine",
"twitter"
],
"entrypoint": "hello.story"
}
The compiled tree uses a similar structure for every line:
{
"tree": {
"line number": {
"method": "operation type",
"ln": "line number",
"output": "if an output was defined (as in services or functions)",
"name": ["if assigning a variable, its name"],
"service": "the name of the service or null",
"command": "the command or null",
"function": "the name of the function or null",
"args": [
"additional arguments about the line"
],
"enter": "if defining a block (if, foreach), the first child line",
"exit": "used in if and elseif to identify the next line when a condition is not met",
"parent": "if inside the block, the line number of the parent",
"next": "the next line to be executed"
}
}
}
The operation described by the line.
The line number.
Next refers to the next line to execute. It acts as an helper, since the original story might have comments or blank lines that are not in the tree, the next line is not always the current line + 1
The parent property identifies nested lines. It can be used to identify all the lines inside a block. Care must be taken for further nested blocks.
Objects are seen in the args of a line. They can be variable names, function arguments, string or numeric values:
{
"args": [
{
"$OBJECT": "<objectype>",
"objectype": "value"
}
]
}
String objects have a string property. For example, “hello” would evaluate to:
{
"$OBJECT": "string",
"string": "hello",
}
If they are string templates, they will also have a values list, indicating the variables to use when compiling the string. For example, “hello, {path} would evaluate to:
{
"$OBJECT": "string",
"string": "hello, {}",
"values": [
{
"$OBJECT": "path",
"paths": [
"name"
]
}
]
}
Declares a list. Items will be a list of other objects. For example, [1, 2, 3] would evaluate to:
{
"$OBJECT": "list",
"items": [1, 2]
}
However, note that for other types the object types needs to be passed too. For example, [“hello”, “world”] would evaluate to:
{
"$OBJECT": "list",
"items": [
{
"$OBJECT": "string",
"string": "hello"
},
{
"$OBJECT": "string",
"string": "world"
}
]
}
Declares an object:: For example, [“key”: “value”] would evaluate to:
{
"$OBJECT": "dict",
"items": [
[
{
"$OBJECT": "string",
"string": "key"
},
{
"$OBJECT": "string",
"string": "value"
}
]
]
}
Declares a regular expression. For example, /^foo/g would evaluate to:
{
"$OBJECT": "regexp"
"regexp": "/^foo/",
"flags": "g"
}
A path is a reference to an existing variable:
{
"args": [
{
"$OBJECT": "path",
"paths": [
"<varname>"
]
}
]
}
Is more than one paths member given, this implies object access to the referenced variable. For example, a.b would evaluate to:
{
"args": [
{
"$OBJECT": "path",
"paths": [
"a", "b"
]
}
]
}
Expression have an expression property indicating the type of expression and a values array with one (unary) or two (binary) expression values. Values can be`paths` or values objects:: For example, a <type> b would like similar to:
{
"$OBJECT": "expression",
"expression": "<type>",
"values": [
{
"$OBJECT": "path",
"paths": [
"foo"
]
},
1
]
}
Storyscript engines must support the following unary and binary expression types.
Argument objects are used in function definition, function calls and services to declare arguments:
{
"$OBJECT": "argument",
"name": "id",
"argument": {
"$OBJECT": "type",
"type": "int"
}
}
Mutation objects are used for mutations on values, and are found only as arguments in expression methods. They are always preceded by another object, that can be any kind of value or a path:
{
"$OBJECT": "string",
"string": "hello"
},
{
"$OBJECT": "mutation",
"mutation": "uppercase",
"arguments": []
}
Mutations arguments follow the same syntax for service arguments and can be found in the arguments list:
{
"$OBJECT": "mutation",
"mutation": "slice",
"arguments": [
{
"$OBJECT": "argument",
"name": "at",
"argument": 2
}
]
}
Used for expression lines, like sums, multiplications and so on. For example:
1 + 1
Compiles to:
{
"method": "expression",
"ln": "1",
"output": null,
"service": null,
"command": null,
"function": null,
"args": [
{
"$OBJECT": "expression",
"expression": "sum",
"values": [
1,
1
]
}
],
"enter": null,
"exit": null,
"parent": null
}
When declaring a variable, or assigning a value to a property the name field will be set. For example, a story like:
x = "hello"
Will result in:
{
"1": {
"method": "expression",
"ln": "1",
"name": ["a"],
"args": [
1
],
"next": "<next line>"
}
}
Args can be a path, an expression object or a pure value. When part of block of conditionals, the exit property will refer to the next else if or else.
For example, if color would evaluate to:
{
"method": "if",
"ln": "1",
"output": null,
"service": null,
"command": null,
"function": null,
"args": [
{
"$OBJECT": "path",
"paths": [
"color"
]
}
],
"enter": "2",
"exit": null,
"parent": null,
"next": "2"
}
Similar to if. For example, elif a == 1 would evaluate to:
{
"method": "elif",
"ln": "3",
"output": null,
"service": null,
"command": null,
"function": null,
"args": [
{
"$OBJECT": "expression",
"expression": "equals",
"values": [
{
"$OBJECT": "path",
"paths": [
"a"
]
},
1
]
}
],
"enter": "4",
"exit": null,
"parent": null,
"next": "4"
}
Similar to if and elif, but exit is always null and no args are available:
{
"method": "else",
"ln": "5",
"output": null,
"service": null,
"command": null,
"function": null,
"args": [],
"enter": "6",
"exit": null,
"parent": null,
"next": "6"
}
Declares the following child block as a try block. Errors during runtime inside that block should not terminate the engine:
{
"method": "try",
"ln": "1",
"next": "2",
"name": null,
"function": null,
"output": null,
"args": null,
"command": null,
"service": null,
"parent": null,
"enter": "2",
"exit": null
}
Declares the following child block as a catch block that would be executed in case the previous try block failed:
{
"method": "catch",
"ln": "3",
"output": [
"error"
],
"name": null,
"function": null,
"args": null,
"command": null,
"service": null,
"parent": null,
"enter": "4",
"next": "4",
"exit": "line"
}
Declares the following child block as finally block that is always executed regardless of the previous try outcome:
{
"method": "finally",
"ln": "5",
"name": null,
"function": null,
"output": null,
"args": null,
"command": null,
"service": null,
"parent": null,
"enter": "6",
"next": "6",
"exit": null
}
Foreach ### Declares a for iteration. For example foreach items as item would evaluate to:
{
"method": "for",
"ln": "1",
"output": [
"item"
],
"service": null,
"command": null,
"function": null,
"args": [
{
"$OBJECT": "path",
"paths": [
"items"
]
}
],
"enter": "2",
"exit": null,
"parent": null,
"next": "2"
}
Used for services. Service arguments will be in args. For example, alpine echo message: “text” would evaluate to:
{
"method": "execute",
"ln": "1",
"output": [],
"name": [],
"service": "alpine",
"command": "echo",
"function": null,
"args": [
{
"$OBJECT": "argument",
"name": "message",
"argument": {
"$OBJECT": "string",
"string": "text"
}
}
],
"enter": null,
"exit": null,
"parent": null
}
Declares a function. Output maybe null. For example, function sum a:int b: int returns int would evaluate to:
{
"method": "function",
"ln": "1",
"output": [
"int"
],
"service": null,
"command": null,
"function": "sum",
"args": [
{
"$OBJECT": "argument",
"name": "a",
"argument": {
"$OBJECT": "type",
"type": "int"
}
},
{
"$OBJECT": "argument",
"name": "b",
"argument": {
"$OBJECT": "type",
"type": "int"
}
}
],
"enter": "2",
"exit": null,
"parent": null,
"next": "2"
}
Declares a return statement. Can be used only inside a function, thus will always have a parent. For example, return x would evaluate to:
{
"method": "return",
"ln": "2",
"output": null,
"service": null,
"command": null,
"function": null,
"args": [
{
"$OBJECT": "path",
"paths": [
"x"
]
}
],
"enter": null,
"exit": null,
"parent": "1"
}
Declares a function call, but otherwise identical to the execute method. For example, sum(a: 1, b:2) would evaluate to:
{
"method": "call",
"ln": "4",
"output": [],
"service": "sum",
"command": null,
"function": null,
"args": [
{
"$OBJECT": "argument",
"name": "a",
"argument": 1
},
{
"$OBJECT": "argument",
"name": "b",
"argument": 2
}
],
"enter": null,
"exit": null,
"parent": null
}