Reference

This section describes the basic syntax of Roto scripts. This is written in a reference-style. It is mostly meant as a cheatsheet, not as an introduction to the language.

Comments

Comments in Roto start with a # and continue until the end of a line. They can be inserted anywhere in the script and are ignored.

# this is a comment

Block comments are not supported. To create a multiline comment, prefix every line with a #.

# one comment line
# another comment line

Literals

Roto supports literals for primitive types:

  • 0, 1, 34, -10, 0xFF etc. for integers

  • true and false for booleans

  • 0.0.0.0, 2345:0425:2CA1:0000:0000:0567:5673:23b5, 0::, etc. for IP addresses

  • 0.0.0.0/10 for prefixes

  • AS1234 for AS numbers

  • "Hello" for strings

Primitive types

There are several types at Roto’s core, which can be expressed as literals.

There are many more types available that have more to do with BGP. These are described elsewhere. Note that Roto is case sensitive; writing the Asn type as ASN or asn won’t work.

Integers

As the previous section indicates, there are several types for integers in Roto. This might be familiar to users of languages such as C and Rust, but not for users of Python and similar languages which only have one integer type.

Roto is a compiled language and as such needs to know how many bytes to use for a given integer. Hence, the number of bits are included in the type. The prefix u is used for unsigned (i.e. non-negative) numbers and i for signed integers.

Below is a table of all available integer types.

Type

Bits

Signed

Min

Max

u8

8

No

0

255

u16

16

No

0

65,535

u32

32

No

0

4,294,967,295

u64

64

No

0

18,446,744,073,709,551,615

i8

8

Yes

-128

127

i16

16

Yes

-32768

65,535

i32

32

Yes

-2147483648

4,294,967,295

i64

64

Yes

-9,223,372,036,854,775,808

9,223,372,036,854,775,807

Arithmetic operators

There are mathematical operators for common operations:

+

addition

-

subtraction

*

multiplication

/

division

These operators follow the conventional PEMDAS rule for precedence. The order is

  • Parentheses

  • Multiplication and division

  • Addition and subtraction

Parentheses can always be used to force a certain order of operations. For example, this expression:

1 + 2 * 3    # evaluates to 7

is interpreted as

1 + (2 * 3)  # evaluates to 7

and not as

(1 + 2) * 3  # evaluates to 9

Comparison operators

In addition to arithmetic operators, there are operators to compare values. Comparison operators have a lower precedence than arithmetic operators. The script won’t compile if the operands have different types.

==

Equals

!=

Does not equal

>

Greater than

>=

Greater than or equal

<

Less than

<=

Less than or equal

Examples:

5 > 10      # evaluates to false
10 > 5      # evaluates to true
5 == 5      # evaluates to true
5 == true   # compile error!
1 < x < 10  # compile error!

Logical operators

Operators to combine boolean values are called logical operators. They have a lower precedence than comparison operators. These are the logical operators in Roto:

&&

Logical and

||

Logical or

not

Negation

Now that we have all the rules for precendence, here is an example using all types of operators (arithmetic, comparison and logical):

1 + x * 3 == 5 && y < 10

This is equivalent to:

((1 + (x * 3)) == 5) && (y < 10)

Strings

Strings are enclosed in double quotes like so:

"This is a string!"

Strings can be concatenated with +:

"race" + "car" # yields the string "racecar"

It also has some methods such as String.contains that can be very useful. See the documentation for the String type for more information.

If-else

To conditionally execute some code, use an if block. The braces in the example below are required. The condition does not require parentheses. The condition must evaluate to a boolean.

if x > 0 {
    # if the condition is true
}

An else-clause can optionally follow the if-block. The if-else construct is an expression and therefore evaluates to a value.

if x > 0 {
    # if the condition is true
} else {
    # if the condition is false
}

Functions

Functions can be defined with the function keyword, followed by the name and parameters of the function. It is required to specify the types of the parameters. The return type is specified with ->. A function without a return type does not return anything.

function add_one(x: u64) -> u64 {
    x + 1
}

This function can then be called like so:

add_one(10)

A function can contain multiple expressions. The last expression is returned if it is not terminated by a ;. The return can also be made explicit with the return keyword. This function is equivalent to the previous example.

function add_one(x: u64) -> u64 {
    return x + 1;
}

The following function uses multiple statements to return 0 if the input is 0 and subtract 1 otherwise.

function subtract_one(x: u64) -> u64 {
    if x == 0 {
        return 0;
    }
    x - 1
}

This function does not return anything:

function returns_nothing(x: u64) {
    x + 1;
}

The return keyword can still be used in functions that don’t return a value to exit the function early.

When a function becomes more complex, intermediate results can be stored in local variables with let.

function greater_than_square(x: i32, y: i32) {
    let y_squared = y * y;
    x > y_squared
}

Filter-map

A filtermap is a function that filters and transforms some incoming value.

Filter-maps resemble functions but they don’t return. Instead they either accept or reject, which determines what happens to the value. Generally, as accepted value is stored or fed to some other component and a reject value is dropped.

filtermap reject_zeros(input: IpAddr) {
    if input == 0.0.0.0 {
        reject
    } else {
        accept
    }
}

This describes a filter which takes in an IP address and accepts it if it is not equal to 0.0.0.0.

Like with functions, intermediate results can be stored in variables with let bindings.

filtermap reject_zeros(input: IpAddr) {
    let zeros = 0.0.0.0;
    if input == zeros {
        reject
    } else {
        accept
    }
}

A filtermap can also accept or reject with a value.

Anonymous records

Multiple values can be grouped into records. A record is constructed with {} and contains key-value pairs.

{ foo: 5, bar: 10 }

These records are statically typed, which means that records with different field names or different field types are separate types. For example, this is a type checking error:

if x {
    { foo: 5, bar: 10 }
} else {
    { foo: 5 }  # error!
}

Note that this makes records significantly different from dictionaries in Python and objects in JavaScript, which resemble hash-maps and are far more dynamic.

Fields of records can be accessed with the . operator.

filtermap example_filter_map() {
    let x = { foo: 5 };
    accept x.foo
}

Named records

Named records provide a more principled approach to grouping values which will yield more readable type checking errors.

type SomeRecord {
    foo: i32,
    bar: bool,
}

# ...

x = SomeRecord { foo: 3, bar: false }

Roto checks that all declared values are provided and are of the same type.

There is an automatic coercion from anonymous records to named records:

function foo(int: i32) -> SomeRecord {
    { foo: int, bar: false }  # implicitly coerced to SomeRecord
}

Modules

A Roto script can be split over multiple files. To do this, we have to create a folder with the name of the script and create a Roto file directly in it called pkg.roto. This file is the root of our script. The contents of pkg.roto will form the pkg module. No other files in the directory can be called pkg.roto.

Files adjacent to pkg.roto are submodules of pkg. For example, a file called foo.roto will define the module pkg.foo.

A directory next to pkg.roto will also be a submodule if it contains a file called lib.roto. A file foo/lib.roto is therefore equivalent to foo.roto and defines the module called pkg.foo. We can do this recursively, so we can define the module pkg.foo.bar with either a file called foo/bar.roto or foo/bar/lib.roto and so forth.

The files foo.roto and foo/lib.roto cannot both exist and only foo/lib.roto can have submodules.

An item such as a function, filtermap or type can be used from other modules in a couple of ways. To access them, we must know the path, which is the period-separated list of identifiers to follow to get to the item, starting with the module name and ending with the name of the item.

For the following examples, we will work with the following files:

pkg.roto
foo.roto
bar/lib.roto
bar/baz.roto

These define the following modules:

pkg
pkg.foo
pkg.bar
pkg.bar.baz

Now assume that foo.roto contains a function called square, this function can be referenced in any of the other files with the absolute path pkg.foo.square. For example:

function add_and_square(x: i32, y: i32) -> i32 {
    pkg.foo.square(x + y)
}

We can also use the relative path, which is different for each file. We can use the super keyword in a path to reference the parent module of the current module. Multiple super keywords can appear at the start of a path.

# in pkg.roto
foo.square

# in foo.roto
square

# in bar.roto
super.foo.square

# in bar/baz.roto
super.super.foo.square

There are 3 special identifiers that can only be used at the start of a path and automatically make the path an absolute path:

  • pkg for the current package

  • std for the Roto standard library

  • dep for dependencies (not implemented yet, but the identifier is reserved)

Imports

Of course, writing out the full path to anything you want to use can become quite tedious. We can import items from other modules into the current module with the import keyword. The import keyword is followed by a path. The item the path references will be available by name in the current scope.

import foo.square;

function fourth_power(x: i32) -> i32 {
    square(square(x))
}

We can also import entire modules. Imported modules are not available in other modules.

An import does not need to be at the top-level, they can be in any scope. We can rewrite the previous example as follows.

function fourth_power(x: i32) -> i32 {
    import foo.square;
    square(square(x))
}

Now the name square can only be used within the fourth_power function and not in any other functions we define. But we can define even more granular imports such as in the following example, where we use a function foo from either module A or B, depending on a boolean flag.

function use_foo(x: i32, choice: bool) -> i32 {
    if choice {
        import A.foo;
        foo(x)
    } else {
        import B.foo;
        foo(x)
    }
}

Next steps

You can learn more about Roto by looking at the documentation for the Standard Library.