Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

NotationExampleDescription
TypeWriterifThe exact character(s)
ItalicIdentifierA syntactical production
x ::= yNull ::= nullDefinition of a syntactical production
x yspawn ExpressionConcatenation 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 | ytrue | falseEither 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:

SyntaxName
nullNull
true | falseBool
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:

SyntaxName
ValueValue
ScopedIdentifierVariable
<- ExpressionReceive
UnaryOperator ExpressionUnary
Expression BinaryOperator ExpressionBinary
( Expression )Group
Expression [ Expression ]Index
Expression . IdentifierMember
Expression ( ( ( Identifier : )? Expression , )* )Call
spawn ExpressionSpawn

UnaryOperator ::= - | !

BinaryOperator ::= +| - | * | / | % | ?? | == | != | < | <= | > | >= | and | or

Statement

Statement can be one of:

SyntaxName
Expression ;Expression
let Identifier = ExpressionDeclaration
Identifier = ExpressionAssignment
Expression <- ExpressionSend
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:

SyntaxName
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.

Warning

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.

TypeExample
string"text"[1 + 1] == "x"
array[1, 2, 3][1] == 2
object{ "k 1": "v" }["k 1"] == "v"
Warning

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.

ValueTruth valueExample
nullfalsenull == false
boolfalse if false
true if true
false == false
true == true
numberfalse if 0
true otherwise
0 == false
34 == true
-0.5 == true
stringfalse if ""
true otherwise
"" == false
"text" == true
arrayfalse if []
true otherwise
[] == false
[1, 2] == true
objectfalse 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 typeOperatorRight typeExample
number+number1 + 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-number3 - 4 == -1
number*number3 * -4 == -12
number/number2 / 4 == 0.5
number%number7 % 3 == 1
Errors

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.

LeftOperatorRightResult
falseandfalsefalse
falseandtruefalse
trueandfalsefalse
trueandtruetrue
falseorfalsefalse
falseortruetrue
trueorfalsetrue
trueortruetrue
Warning

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

ExpressionMeaning
a == ba is equal to b
a != ba 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

ExpressionMeaning
a < ba is less than b
a <= ba is less than or equal to b
a > ba is greater than b
a > ba 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

OperatorTypeExample
-number-3 == -3
!any!{ k: "v" } == false
Error

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

  1. Logical: and, and or
  2. Relational: ==, !=, <, <=, >, and >=
  3. Additive: +, and -
  4. Multiplicative: *, /, and %
  5. Unary: -, and !, <-
  6. Null coalescing: ??
  7. Offset, Member, Call: [], ., and ()
  8. 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.

Return types

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

Message format

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(chan​nel: 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
}