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 filter-map 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.

filter-map 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.

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

A filter-map 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.

filter-map example_filter_map() {
    define {
        x = { foo: 5 };
    }
    apply {
        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
}

Next steps

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