Skip to content

The language

Functions

Functions are declared with fn, with a type on every parameter and the return type after ->. Arguments are passed by value; a function changes the caller’s variable only through a ref parameter, and the caller writes ref too, so every change is visible at the call site.

fn area(w: float, h: float) -> float:
return w * h
fn greet(name: str):
print(f"hello, {name}")
fn divmod(a: int, b: int) -> (int, int):
return (a / b, a % b)
fn main():
print(area(2.0, 3.0))
print(area(w=2.0, h=3.0))
greet("ann")
let (q, r) = divmod(17, 5)
print(q, r)
print(is_even(10))
fn is_even(n: int) -> bool: # order doesn't matter; recursion is fine
if n == 0:
return true
return not is_even(n - 1)
Output
6.0
6.0
hello, ann
3 2
true
  • Parameter types are required. Local variable types are inferred.
  • A function without -> T returns nothing (its type is ()); return on its own leaves it early.
  • Return several values as a tuple and unpack them at the call.
  • Arguments can be named, in the parameters’ order: area(w=2.0, h=3.0). There are no default values.
  • Functions can be declared in any order, and may call each other recursively. They are declared at the top level of a module (or as methods); there are no nested functions.

A function with a return type must return on every path:

fn sign(x: int) -> int:
if x > 0:
return 1
elif x < 0:
return -1
fn main():
print(sign(5))
error[E0306]: function `sign` may reach the end without returning a value
--> main.br:1:4
|
1 | fn sign(x: int) -> int:
| ^^^^ expected to return `int`
|
= help: add a `return` statement at the end of the function
= run `brine explain E0306` for more about this error
error: could not compile due to 1 error

Functions can be generic (fn first[T](xs: [T]) -> Option[T]); see generics. They can also carry requires and ensures contracts that the compiler proves; see contracts.

A plain parameter is a read-only copy of the argument. Assigning to it is an error that points you at the two alternatives, mut and ref:

fn dec(n: int) -> int:
n -= 1
return n
fn main():
print(dec(3))
error[E0403]: cannot assign to parameter `n`
--> main.br:2:5
|
2 | n -= 1
| ^ parameters are read-only
1 | fn dec(n: int) -> int:
| - declared here
|
= help: to change a local copy, declare it `mut n: ...`; to change the caller's variable, declare it `ref n: ...` and call with `ref`
= run `brine explain E0403` for more about this error
error: could not compile due to 1 error

mut makes the parameter a local copy that the function may change. The caller’s value is untouched.

fn countdown(mut n: int) -> [int]:
var out: [int] = []
while n > 0:
out.push(n)
n -= 1
return out
fn main():
let start = 3
print(countdown(start), start)
Output
[3, 2, 1] 3

This works for arrays too, and it’s cheap: the array is copied only when the function first changes it (copy-on-write).

fn add_all(mut xs: [int], k: int) -> [int]:
for i in 0..len(xs):
xs[i] += k
return xs
fn main():
let base = [1, 2, 3]
let more = add_all(base, 10)
print(base, more)
Output
[1, 2, 3] [11, 12, 13]

A ref parameter is the caller’s variable itself: changes to it change the caller’s variable. The caller writes ref before the argument, so you can see at the call site which variables a call may change.

fn fill(ref xs: [int], v: int):
for i in 0..len(xs):
xs[i] = v
fn swap(ref a: int, ref b: int):
let t = a
a = b
b = t
fn main():
var data = [0] * 4
fill(ref data, 7)
print(data)
var x = 1
var y = 2
swap(ref x, ref y)
print(x, y)
var grid = [[0, 0], [0, 0]]
fill(ref grid[1], 5) # an element or a field works too
print(grid)
Output
[7, 7, 7, 7]
2 1
[[0, 0], [5, 5]]

The argument must be a var (or an element or field of one). Leaving out ref at the call is an error:

fn fill(ref xs: [int], v: int):
for i in 0..len(xs):
xs[i] = v
fn main():
var data = [0] * 3
fill(data, 7)
error[E0402]: parameter `xs` is a `ref`; pass the argument with `ref`
--> main.br:7:10
|
7 | fill(data, 7)
| ^^^^ the callee may modify this
|
= help: passing by reference is always visible at the call site in brine
= run `brine explain E0402` for more about this error
error: could not compile due to 1 error

Methods that change their receiver take ref self; see structs.

let d = ref data makes d a second name for the variable data. Reading or changing d reads or changes data.

fn main():
var data = [1, 2, 3]
let d = ref data # `d` is another name for `data`
d[0] = 100
d.push(4)
print(data)
Output
[100, 2, 3, 4]

The target must be a whole variable (ref data, not ref data[0]).

References in Brine are second-class: you can create them only as parameters and local names, never store them in a struct, an array or a map, and never return them. ref isn’t part of any type, so -> ref int or a field r: ref int doesn’t parse. Storing d from above in an array stores a copy of the value.

Because of that, a reference can’t outlive the variable it names, and the compiler always knows which variable each reference points to. There is no lifetime syntax and no borrow checker to satisfy.

During a call, a ref argument must be the only way to reach that variable. Passing the same variable as ref twice (even two different elements of one array) is an error:

fn swap(ref a: int, ref b: int):
let t = a
a = b
b = t
fn main():
var xs = [1, 2]
swap(ref xs[0], ref xs[1])
error[E0405]: `xs` is passed as `ref` twice
--> main.br:8:25
|
8 | swap(ref xs[0], ref xs[1])
| ^^^^^ second reference here
| ----- passed as `ref` here
|
= note: a `ref` argument must be the only access to that variable during the call
= help: copy the value first: `let xs_copy = ...`
= run `brine explain E0405` for more about this error
error: could not compile due to 1 error

Swap two elements of one array in place instead: let t = xs[0], xs[0] = xs[1], xs[1] = t.

Passing the same variable once by ref and once by value is fine: the by-value argument is the value from before the call.

An expression that changes a variable in place (a ref argument, a ref self method such as xs.pop(), m.remove(k)) can’t also read that variable somewhere else in the same expression, or change it a second time. The result would then depend on the order the parts are evaluated in, so Brine rules it out and the order never matters.

fn main():
var xs = [1, 2, 3]
print(xs.pop(), xs)
error[E0405]: `xs` is changed and also read in the same expression
--> main.br:3:11
|
3 | print(xs.pop(), xs)
| ^^^^^^^^ changed here
| -- read here
|
= help: make the change in its own statement first, then use the result
= run `brine explain E0405` for more about this error
error: could not compile due to 1 error

Make the change in its own statement first. Reading the variable in the changing call’s own receiver and arguments is fine, because they are evaluated before the call. So are the two sides of and/or, which run one after the other.

fn bump(ref x: int) -> int:
x += 1
return x
fn main():
var xs = [1, 2]
xs.push(len(xs)) # fine: the argument is read before the call
let last = xs.pop() # change first...
print(xs, last) # ...then read
var n = 1
let after = bump(ref n)
print(after + n)
Output
[1, 2] 2
4

main takes no parameters (command-line arguments come from args()) and returns one of three things:

  • nothing: the program exits with code 0;
  • an int: the exit code;
  • a Result[(), E]: Ok(()) exits with 0, and an Err(e) prints error: and the error to stderr and exits with 1. This lets main use the ? operator.
config.br
fn parse_port(s: str) -> Result[int, str]:
if not s.is_digit():
return Err(f"not a port number: {s}")
return Ok(int(s))
fn main() -> Result[(), str]:
let port = parse_port("80x")?
print(f"listening on {port}")
return Ok(())
Output (stderr, exit code 1)
error: not a port number: 80x

See Option and Result for ?. A panic exits with code 101, and exit(code) ends the program at once from anywhere.

A function name can only be called. It can’t be stored in a variable, passed as an argument or returned; first-class functions are planned.

fn square(x: int) -> int:
return x * x
fn main():
let f = square
print(f(3))
error[E0310]: `square` is a function; functions can't be used as values yet
--> main.br:5:13
|
5 | let f = square
| ^^^^^^ call it with `(...)`
= run `brine explain E0310` for more about this error
error[E0303]: `f` is a variable of type `?`, not a function
--> main.br:6:11
|
6 | print(f(3))
| ^ not callable
= run `brine explain E0303` for more about this error
error: could not compile due to 2 errors

To choose behavior at run time, use an enum and match on it.

  • Every parameter has a type; the return type follows -> (none means ()).
  • Parameters are read-only. mut p: T is a changeable local copy; ref p: T is the caller’s variable, and the caller writes ref too.
  • let r = ref v makes r another name for the variable v.
  • References can’t be stored or returned, so they never dangle.
  • A variable can’t be passed as ref twice in one call.
  • An expression that changes a variable can’t read it elsewhere or change it again.
  • main returns nothing, an int exit code, or Result[(), E].
  • Functions aren’t values.