Introduction
Workflow Definition Language (WDL) is a domain-specific language tailored for mobile robotics. Its semantics are based on the pi-calculus, introducing interesting features such as concurrent execution and synchronization channels. Moreover, it supports well-known concepts from other languages like JSON compatible values, global and local variables, functions, operators, and control structures like if-else statements and while loops.
In addition to its core functionality, WDL provides a comprehensive standard library. This library offers functions to perform physical actions like pickup and drop actions, accessing the web through HTTP calls, using regex for searching, finding, and replacing strings, and many more.
Example
The following is a simple example of how a station-to-station workflow in WDL would look:
global source = "mySource";
global destination = "myDestination";
actions {
action::pickup(
target: {
stations: [
source
]
},
events: {
no_station_left: order::cancel
}
);
action::drop(
target: {
stations: [
destination
]
}
);
}
Language
In this chapter, the syntax and semantics of the core features of the language are described.
Syntax
Notation
| Notation | Example | Description |
|---|---|---|
TypeWriter | if | The exact character(s) |
| Italic | Identifier | A syntactical production |
| x ::= y | Null ::= null | Definition of a syntactical production |
| x y | spawn Expression | Concatenation of x and y |
| x? | Expression? | The item x is optional |
| x* | Char* | 0 or more occurrences of x |
| x+ | Digit+ | 1 or more occurrences of x |
| x | y | true | false | Either x or y |
| [ x - y ] | [ 0 - 9 ] | One of the characters in the range |
| ( x y ) | ( Value , )* | Group multiple items |
Identifier
Identifiers used for variables, channels, object members, and function names comply with the Unicode Standard Annex #31.
Identifier ::= Start Continue*
Where Start is a character from the Unicode set XID_Start or the underscore character *, and Continue is a character of the set XID_Continue.
Example
var1
ladungsträger
_12
Furthermore, scoped identifiers are used to identify variables and functions from the standard library.
ScopedIdentifier ::= ( Identifier :: )* Identifier
Example
action::pickup
log::info
Value
Value can be one of:
| Syntax | Name |
|---|---|
null | Null |
true | false | Bool |
Digit+ (. Digit+)? | Number |
" Char* " | String |
[ ( Value , )* ] | Array |
{ ( ( Identifier | String ) : Value , )* } | Object |
Digit ::= [ 0 - 9 ]
Char can be any valid Unicode character, with the exception that " and \ must be escaped by a preceding \. Furthermore, we allow using \n inside strings for adding line breaks.
Expression
Expression can be one of:
| Syntax | Name |
|---|---|
| Value | Value |
| ScopedIdentifier | Variable |
<- Expression | Receive |
| UnaryOperator Expression | Unary |
| Expression BinaryOperator Expression | Binary |
( Expression ) | Group |
Expression [ Expression ] | Index |
Expression . Identifier | Member |
Expression ( ( ( Identifier : )? Expression , )* ) | Call |
spawn Expression | Spawn |
UnaryOperator ::= - | !
BinaryOperator ::= +| - | * | / | % | ?? | == | != | < | <= | > | >= | and | or
Statement
Statement can be one of:
| Syntax | Name |
|---|---|
Expression ; | Expression |
let Identifier = Expression | Declaration |
Identifier = Expression | Assignment |
Expression <- Expression | Send |
if Expression { Statement* } ( else { Statement* } | else If-else )? | If-else |
while Expression { Statement* } | While |
return Expression? ; | Return |
break ; | Break |
continue ; | Continue |
Workflow
Workflow ::= GlobalDeclaration* actions { Statement* } GlobalDeclaration*
GlobalDeclaration can be one of:
| Syntax | Name |
|---|---|
global Identifier = Expression ; | Global Variable |
function Identifier ( ( Identifier , )* ) { Statement* } | Function |
Comment
To document code, comments can be used. Comments can be placed anywhere in the code except within string literals. Two types of comments are supported. Firstly, single-line comments, which start with // and comment out the rest of the line. Secondly, multi-line comments, which begin with /* and end with */, commenting out everything between them. Nesting multi-line comments is not supported; thus, /* /* */ */ raises a syntax error because the comment ends with the first */, and the second occurrence is unexpected.
Values
The language provides five data types analogous to JSON: null, bool, number, string, array, and object.
Syntax
Null
Null values represent the absence of values.
Example:
null
Bool
Bool values represent truth values; they can be either true or false. Mostly used for conditions.
Example:
false
true
Number
Numbers are represented as IEEE 754 floating-point numbers with 64-bit precision.
Example:
0
23
34.5
-45.6
Strings
Strings can be used to save text; they fully support Unicode code points as UTF-8 encoded. Strings start and end with ". ' and ` are not allowed. Furthermore, escaped characters can be used: \\ to use a backslash \, \n to add a line break, and \" to use " inside strings. Multiline strings are also supported.
Example:
"text"
"first line\nsecond line"
"multi
line
string"
Array
Arrays are sequences of values with arbitrary length. The elements can be of any type. An array starts with [ and ends with ], the elements are separated by a ,.
Example:
[1, 2, 3]
["first", 2, null]
[
1,
[1, 2],
{
key: "value",
key2: 2
}
]
Object
Objects are collections of named values. They start with a { and end with a }, the key and the value are separated by a :, and the key can be either an identifier or a string. The key-value pairs are separated by a , like in arrays.
The key wdl_type is reserved for internal use. Using this key may lead to unintended behavior.
Example:
{
key: 23,
"my key": [
1,
2,
],
key3: {
inner_key: "text".
inner_key2: 34
}
}
Access Operators
Offset operator
To access a specific element of a string, array, or object, the offset operator [] can be used.
| Type | Example |
|---|---|
| string | "text"[1 + 1] == "x" |
| array | [1, 2, 3][1] == 2 |
| object | { "k 1": "v" }["k 1"] == "v" |
Offsets on strings and arrays are 0-based. So the first element is at offset 0 and the second element is at offset 1.
Member operator
The member operator is a special form of the offset operator used to access members of an object. It has the restriction that only values associated with an identifier can be accessed, not those associated with a string key.
Example:
{
key: "value",
"key 1": 23
}.key == "value"
To access "key 1", the offset operator has to be used.
Truth value
Each of the above values can be used as a condition, which means each of these values has a truth value true/false.
| Value | Truth value | Example |
|---|---|---|
| null | false | null == false |
| bool | false if false true if true | false == false true == true |
| number | false if 0 true otherwise | 0 == false 34 == true -0.5 == true |
| string | false if "" true otherwise | "" == false "text" == true |
| array | false if [] true otherwise | [] == false [1, 2] == true |
| object | false if {} true otherwise | {} == false { k: "v" } == true |
Variables
Variables are dynamically typed and can be used to store values, functions, and channels of any type for later reuse. The values of variables can be changed through an assignment operation. The language supports both global and local variables.
Global
Global variables can be used everywhere in the program, and their value can be set at the workflow start. To use a global variable, it needs to be declared in the outermost scope, for example, above the actions block.
Example:
global my_variable = 23;
actions {
my_variable = my_variable + 2;
// This raises an error because `global`
// is not allowed inside any scope.
global another_var = 3;
}
Local
Local variables can only be used within the scope in which they are declared. A local variable can be declared using the let keyword. They cannot be declared in the outermost scope.
Example:
// This raises an error because `let`
// is not allowed on the global scope.
let var1 = 23;
actions {
let var2 = 3;
if true {
var2 = var2 + 2;
let var3 = 45;
var3 = 23;
} // <- `var3` gets deleted at this `}`
var2 = 78;
// This raise an error because `var3`
// is not declared inside this scope.
var3 = 56;
} // <- `var2` gets deleted at this `}`
Functions
Functions can be used to reuse common code at multiple places within a workflow.
Declaration
Before a function can be used, it has to be declared in the global scope using the function keyword. A function can take an arbitrary number of input variables and can return at most one value using the return keyword.
Example:
actions {
// This raises an error because function
// declarations are only allowed at global scope.
function sub(left, right) {
return left - right;
}
}
function sum(left, right) {
return left + right;
}
Call
After declaring a function, it can be used through a function call. The return value can be used directly in an expression or can be assigned to a variable. Input variables can be either placed in the correct order or can be named. Positional and named arguments can be used in the same call, but first all positional arguments have to be placed, and after that, the named ones can be placed.
Example:
actions {
log::info(sub(5, 2)); // logs the value `3`
let my_sum = sub(right: 4, left: 9); // my_sum now has the value `5`
sub(7, right: 2);
// This raises an error because after the
// named argument `left`, is the positional argument `2`.
sub(left: 7, 2);
// Calling a function with too few or
// too many input variables raises an error.
sub(1);
sub(1, 2, 3);
}
function sub(left, right) {
return left - right;
}
Channels
In addition to variables that hold values permanently, channels can be used as synchronized queues that hold a value only until it is consumed. If the channel is empty, a read operation blocks the execution of the workflow until a value is sent over the channel. If the buffer is full, a send operation blocks until there is space for another element in the buffer.
Creation
To create a new channel, the channel::new function from the standard library can be used.
Example:
actions {
// This creates a channel with a buffer for 3 values.
let ch = channel::new(3);
}
Send
Before a value can be received from a channel, a value has to be sent over this channel. For that, the <- operator can be used.
actions {
let ch = channel::new(3);
ch <- 4;
ch <- 5;
// `ch` holds the values 4 and 5
}
Receive
To receive an already sent value from a channel, the <- operator can be used as a unary operator.
actions {
let ch = channel::new(3);
ch <- 4;
ch <- 5;
logs::info(<-ch); // logs `4`
logs::info(<-ch); // logs `5`
logs::info(<-ch); // blocks until another value is sent along `ch`
}
Operators
Arithmetic operators
The following table shows which operators are implemented for which types.
| Left type | Operator | Right type | Example |
|---|---|---|---|
| number | + | number | 1 + 2 == 3 |
| string | + | any | "text" + 3 == "text3" |
| array | + | array | [1, 2] + [3, 4] == [1, 2, 3, 4] |
| array | + | any | [1, 2] + 3 == [1, 2, 3] |
| number | - | number | 3 - 4 == -1 |
| number | * | number | 3 * -4 == -12 |
| number | / | number | 2 / 4 == 0.5 |
| number | % | number | 7 % 3 == 1 |
All other operator/type combinations raise an error and cancel the order.
Furthermore, if the right value of / and % is 0, an error is raised and the order gets canceled.
Logical operators
As logical operators and and or can be used. For that, the left and the right operand are converted to their truth values and the operators are evaluated as follows.
| Left | Operator | Right | Result |
|---|---|---|---|
| false | and | false | false |
| false | and | true | false |
| true | and | false | false |
| true | and | true | true |
| false | or | false | false |
| false | or | true | true |
| true | or | false | true |
| true | or | true | true |
Logical operators get short-circuited evaluated, which means if the operator is and and the left value is false, the right expression is not evaluated at all. Vice versa if the operator is or and the left value is true, the right expression is not evaluated.
That means that the following expression evaluates to true without canceling the order: true or order::cancel().
Relational operators
To compare values, two equality operators and four comparison operators are provided by the language.
Equality
| Expression | Meaning |
|---|---|
a == b | a is equal to b |
a != b | a is not equal to b |
a and b can be of any type, but the operators are type strict, which means that 2 == "2" is false. Arrays and objects are compared recursively.
Comparison
| Expression | Meaning |
|---|---|
a < b | a is less than b |
a <= b | a is less than or equal to b |
a > b | a is greater than b |
a > b | a is greater than or equal to b |
Comparison operators should only be used for numbers. Because if either a or b is not a number, the value of the expression is always false.
Unary operators
| Operator | Type | Example |
|---|---|---|
- | number | -3 == -3 |
! | any | !{ k: "v" } == false |
All other operator/type combinations raise an error and cancel the order.
Null coalescing operator
To set a default value if a value is null, the null coalescing operator ?? can be used. The default value can be of any type.
Example:
1 ?? 2 == 3
null ?? "default" == "default"
Precedence
- Logical:
and, andor - Relational:
==,!=,<,<=,>, and>= - Additive:
+, and- - Multiplicative:
*,/, and% - Unary:
-, and!,<- - Null coalescing:
?? - Offset, Member, Call:
[],., and() - Values and variables:
23,"test",var, …
Except unary operators, multiple operators on the same precedence level are evaluated left-associative. Unary operators are evaluated right-associative.
Example:
1 + 2 + 3 == ((1 + 2) + 3)
1 + 2 * 3 == (1 + (2 * 3))
1 + 2 == 3 or -4 >= 6 == (((1 + 2) == 3) or ((-4) <= 6))
-4 ?? "default" == (-(4 ?? "default"))
test()[2].key == ((((test)())[2]).key)
-<-var ?? 5 == (-(<-(var ?? 5)))
Control Structures
To skip and repeat actions, and perform tasks in the background, the language provides control structures.
Conditional Branches
The if-else structure allows selecting specific statements based on conditions.
Example:
actions {
let var = 3;
if var < 0 {
// ...
} else if var > 10 {
// ...
} else {
// ...
}
}
Loops
To repeat statements, the while loop can be used. The statements inside the loop body are executed as long as the condition holds.
Example:
actions {
let var = 5;
while var >= 0 {
var = var - 1;
// ...
}
}
Continue
To skip the current loop iteration, continue statements can be used.
Example:
actions {
let arr = [1, 101, 2, 3, 66, 4];
let sum = 0;
let idx = 0;
while idx < 6 {
if arr[idx] > 10 {
continue;
}
sum = sum + arr[idx];
idx = idx + 1;
}
// now sum has the value `10`
}
Break
To cancel the loop execution, break statements can be used.
Example:
actions {
let arr = [1, 2, 3, 66, 4];
let sum = 0;
let idx = 0;
while idx < 5 {
if arr[idx] > 10 {
break;
}
sum = sum + arr[idx];
idx = idx + 1;
}
// now sum has the value `6`
}
Concurrency
To run time-consuming tasks in the background, the spawn operator can be used. Calling the spawn operator moves the task on the right side to the background and returns a channel on which the result of the function can be received.
Example:
actions {
let res_ch = spawn http::get("http://example.org/get-target");
// do something else
let result = <-res_ch; // this blocks until the response arrives
// now `result` holds the HTTP response
}
Standard Library
In this chapter, the standard library is documented. Because the standard library is strictly typed, this chapter not only deals with the provided modules but also provides the type signatures of the input types and the returned types of these functions.
All functions which returns void can get another return type at any release.
All object return types can be extended by additional members at any release.
Furthermore, the return values of callback functions, which currently should return void, may be ignored for now, but that can change in any future release.
Modules
This section describes the modules provided by the standard library.
action
pickup
function pickup(target: Target, events?: Events) -> void
Example
action::pickup(
target: {
stations: [
"myStation"
]
},
events: {
no_station_left: order::cancel
}
)
drop
function drop(target: Target, events?: Events) -> void
Example
action::drop(
target: {
stationareas: [
"myArea"
],
not: {
stations: [
"myStation"
]
}
}
)
drive
function drive(target: Target, events?: Events) -> void
Example
action::drive(
target: {
stations: [
"myStation"
]
},
events: {
no_station_left: log::warn
}
)
order
done
function done() -> void
Example
order::done()
cancel
function cancel() -> void
Example
order::cancel()
http
get
function get(url: string) -> HttpResponse|null
Example
let response = http::get("http://example.org/");
if !response {
log::error("Request failed!");
order::cancel();
}
// use response
post
function post(url: string) -> HttpResponse|null
Example
let response = http::post("https://example.org/");
if response {
log::info(response.body);
}
regex
match
function match(regex: string, haystack: string) -> bool
Example
regex::match("\\d", "abc123") // returns `true`
regex::match("\\d", "abc") // returns `false`
find
function find(regex: string, haystack: string) -> [string]
Example
regex::find("a\\d", "a a3 c a45") // returns `["a3", "a4"]`
replace
function replace(regex: string, haystack: string, replace: string) -> string
Example
regex::replace("a\\d", "a a3 c a45", "b") // returns `"a b c b5"`
log
All messages (msg) are serialized to strings and truncated to 100 characters if longer.
The string representation of messages may change with any release; thus, it should not be utilized as input for other programs.
info
function info(msg: any) -> void
Example
log::info("Some useful information!")
warn
function warn(msg: any) -> void
Example
log::warn({ key: "val" })
error
function error(msg: any) -> void
Example
log::error(get_error_msg())
channel
new
function new(buffer: number) -> channel
Example
let ch = channel::new(3);
close
function close(channel: channel) -> void
Example
let ch = channel::new(3);
channel::close(ch);
time
sleep
function sleep(ms: number) -> void
Example
time::sleep(1000) // sleeps for 1 second
Types
This section describes the types used by the standard library.
Target
{
stations?: [string],
stationareas?: [string],
waypoints?: [{ x: number, y: number }],
not?: {
stations?: [string],
stationareas?: [string],
waypoints?: [{ x: number, y: number }],
}
}
Events
{
no_station_left?: |event: NoStationLeftEvent| -> void
}
HttpResponse
{
status: number,
headers: { string -> string },
body: any
}