Skip to content
Docs
Syntax

Syntax

The overall idea for Risor syntax is that it is like Go, but aims to be more concise and expressive for scripting. Variables are dynamically typed and all Risor object types implement an object.Object interface defined in Go.

Variable Declarations

Variables are declared using the := operator. Unlike in Go, all variables are dynamically typed.

// Declare a variable `x` and assign the integer value 10
x := 10
 
// Declare a variable `y` and assign the string value "hello"
y := "hello"
 
// Declare a variable `array` and assign an array of integers
array := [1, 2, 3]

Constant declarations are also supported:

const url = "https://example.com"

Variable Assignment

Existing variables can be set using the = operator:

// Declare `x`
x := 10
 
// Update `x` to 20
x = 20

Functions

Functions in Risor are declared using the func keyword. Type declarations are not supported.

func last(data) {
    if len(data) == 0 {
        return nil
    }
    return data[-1]
}

Default parameter values are supported, with allowed types including strings, integers, floats, and booleans:

func greet(name='world') {
    return 'Hello, ' + name
}

Unlike in Go, functions can only return a single value. You can, however, return a list and unpack it:

func swap(a, b) {
    return [b, a]
}
 
x, y := swap(10, 20)
print(x, y)  // 20 10

Functions may be assigned to a variable and passed as values:

add := func(a, b) {
    return a + b
}
 
result := add(10, 20) // 30

Control Flow

Risor supports most forms of control flow present in Go.

x := 10
threshold := 5
if x > threshold {
    print("greater")
} else if x == threshold {
    print("equal")
} else {
    print("lesser")
}

One difference with Risor is that if-else and switch are expressions in Risor, meaning they evaluate to a value.

x := if 10 > 5 {
    "greater"
} else {
    "lesser"
}
 
print(x)  # "greater"
name := "Alice"
x := switch name {
    case "Joe":
        "Hello, Joe"
    default:
        "Hello, stranger"
}
print(x)  # "Hello, Joe"

Ternary expressions are supported:

x := 10 > 5 ? 1 : 0
print(x) // 1

Loops

Risor supports for loops and range loops in the same way as Go.

for i := 0; i < 5; i++ {
    print(i)
}
i := 0
for i < 5 {
    print(i)
    i++
}
array := [1, 2, 3]
for i, value := range array {
    print(i, value)
}
for {
    print("infinite loop")
    time.sleep(1)
}

break and continue keywords are also supported.

for i := 0; i < 10; i++ {
    if i == 5 {
        break
    }
    print(i)
}
for i := 0; i < 10; i++ {
    if i == 5 {
        continue
    }
    print(i)
}

Concurrency

Risor leverages Go's concurrency primitives and supports spawning goroutines and communicating via channels. Channels can be used to send any Risor object type.

func work(count) {
    mychan := chan(5)
    go func() {
        for i := 0; i < count; i++ {
            mychan <- rand.float()
        }
        close(mychan)
    }()
    return mychan
}
 
for i, value := range work(5) {
    print(i, value)
}

Error Handling

Risor errors are similar to exceptions in languages like Python and Javascript. This makes Risor more concise and suitable for scripts, as compared to Go. A built-in try function is provided to attempt an operation and fallback to a default value if it fails. try accepts any number of functions, and the first one that does not raise an error has its result returned. If a non-function value is reached, it is returned immediately.

result := try(func() { error("kaboom") }, "default value")
print(result)  # "default value"

Errors can be raised using the error function:

func divide(a, b) {
    if b == 0 {
        error("bad idea: division by zero")
    }
    return a / b
}

Pipe Expressions

Risor supports pipe expressions, which allow you to chain function calls together. The pipe operator | takes the result of the expression on its left and passes it as the first argument to the function on its right.

result := "hello" | strings.to_upper
print(result)  # "HELLO"

Importing Modules

Modules are imported using the import keyword.

import mymod
 
import mymod as mod

By default the Risor CLI will look for modules as .risor files in the current working directory.

You can also import specific attributes from a module using the from keyword.

from mymod import myfunc
 
from mymod import myfunc as f

Strings

Simple strings are declared using double quotes, as in Go:

s := "hello"

Multiline, raw strings are also supported using backticks:

s := `
"one"
"two"
`

Templated strings are defined using single quotes, and variables are interpolated between curly braces:

name := "Joe"
s := 'Hello, {name}'

Data Structures

Risor supports lists, maps, and sets.

// Values contained in data structures can hold mixed types
mylist := [1, "two", 3.0]
 
// Map keys are always strings
mymap := {
    "one": 1,
    "two": 2,
    "three": 3,
}
 
othermap := {
    foo: "bar" // keys can be unquoted if they are valid identifiers
}
 
myset := {1, 2, 3}

Membership tests are performed using the in operator:

mylist := [1, 2, 3]
print(1 in mylist)  // true

Comments

Single-line comments are defined using // or #, and multiline comments are defined using /* and */.

// This is a single-line comment
 
# This is also a single-line comment
 
/* 
This is a multiline comment
*/