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)
}

Errors

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
}

Also see the Try-Catch Error Handling section for more information.

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
*/

Channels

Risor supports Go-style channels for concurrent communication:

// Create a buffered channel with capacity 1
c := chan(1)
 
// Send a value to the channel
c <- "hello"
 
// Receive a value from the channel
x := <-c
 
// Close a channel
close(c)

Risor channels behave like Go channels in many ways, however they support sending and receiving any Risor object type. Also when ranging over a channel, in Risor you receive both the index and value of the current item.

func work(count) {
    mychan := chan(5)
    go func() {
        for i := 0; i < count; i++ {
            mychan <- rand.float()
        }
        close(mychan)
    }()
    return mychan
}
 
// Different from Go: ranging over a channel yields both the index and value
for i, value := range work(5) {
    print(i, value)
}

Goroutines

Risor supports launching goroutines using the go keyword:

// Launch a goroutine
go func() {
    // Do some work
}()
 
// Use a channel for communication
c := chan(1)
go func() {
    c <- 42
}()
x := <-c

Spawn

The spawn function spawns a new goroutine and returns a thread object. The thread object can be used to wait for the goroutine to finish and retrieve its return value.

// Spawn a function and wait for its result
result := spawn(func(x) { return x * 2 }, 21).wait()
print(result)  // 42
 
// Spawn multiple functions
threads := []
for i := 0; i < 5; i++ {
    threads.append(spawn(func(x) { return x ** 2 }, i))
}
results := threads.map(func(t) { t.wait() })
print(results)  // [0, 1, 4, 9, 16]

Defer

The defer statement allows you to schedule a function call to be run after the current function completes:

func example() {
    defer print("This will be printed last")
    print("This will be printed first")
}
 
example()

Try-Catch Error Handling

Risor provides a try function for error handling:

result := try(
    func() {
        // Code that might raise an error
        error("Something went wrong")
    },
    func(err) {
        // Error handler
        print("Caught error:", err)
        return "fallback value"
    }
)

See the try built-in for more information.