Every diagnostic Brine prints has a stable code, a primary source span, the reason,
and how to fix it. This page lists every code with the same explanation that
brine explain <code> prints. Search for a code with Ctrl K, or
jump to it with #e0601-style anchors.
brine explain E0601 # the explanation in your terminalbrine explain --json E0601 # {"code": ..., "title": ..., "explanation": ...}brine explain # list every codeCodes are grouped by the part of the compiler that reports them. 76 codes, from brine 0.4.0.
Syntax (E01xx)
Section titled “Syntax (E01xx)”Literals, indentation, and operators or punctuation from other languages.
| Code | Meaning |
|---|---|
| E0101 | invalid literal |
| E0102 | unterminated string |
| E0103 | bad indentation |
| E0104 | tab in indentation |
| E0105 | operator from another language |
| E0106 | braces or semicolons |
| E0110 | syntax error |
| E0111 | expected an expression |
| E0112 | contract in the wrong place |
| E0113 | misused par |
| E0114 | else if |
Invalid literal
A number or string literal couldn’t be read: the integer is too large to
represent, or a string contains an unknown escape such as \q. The largest
integer literal is 340282366920938463463374607431768211455, the maximum of
u128 (literals above the i128 range need a u128 type).
Supported escapes: \n \t \r \0 \\ \” \’ \{ \} and \u{XXXX}.
Integers may use _ separators and the 0x, 0o and 0b prefixes: 1_000_000, 0xff.
Unterminated string
A string or f-string literal isn’t closed on the same line, or an f-string has an unbalanced brace.
In f-strings, {expr} interpolates and {{ / }} write literal braces:
print(f"{name} has {{braces}}")Bad indentation
brine uses indentation (like Python) to group statements. A block must be
indented after a line ending in :, and nothing else may be indented.
fn main(): print("indented by 4 spaces")Tab in indentation
Indentation must use spaces. Tabs look different in every editor, so they
are not allowed. brine fmt uses 4 spaces per level.
Operator from another language
brine spells logical operators as words and has no ++/--:
a and b (not a && b)a or b (not a || b)not a (not !a)x += 1 (not x++)Braces or semicolons
Blocks are written with : and indentation, and statements end at the end
of the line. Remove { } and ;.
Syntax error
The parser expected something else here. The message says what it expected.
Common causes: a missing : at the end of if/for/fn lines, a missing
type on a function parameter (fn f(x: int)), or a construct that brine
doesn’t support yet (slicing, nested comprehensions).
Run brine guide for the full syntax.
Expected an expression
A value was expected here (a number, a variable, a call, …), but the parser found something else, such as a keyword or a closing bracket.
Contract in the wrong place
requires and ensures must be the first lines of a function body, and
invariant must be the first lines of a while or for body:
fn f(x: int) -> int: requires x > 0 ensures result > x return x + 1Misused par
par comes in exactly four forms:
par for i in 0..n: # parallel looppar [f(x) for x in xs] # parallel mappar sum(x * x for x in xs) # parallel reduction (sum/product/min/max/any/all)par(f(a), g(b)) # run two or more expressions in parallelelse if
brine writes elif (like Python):
if x < 0: ...elif x == 0: ...else: ...Names (E02xx)
Section titled “Names (E02xx)”Names that are not found, defined twice, or shadowed; unknown types, fields and methods.
| Code | Meaning |
|---|---|
| E0201 | name not found |
| E0202 | defined twice |
| E0203 | unknown type |
| E0205 | no such field |
| E0206 | no such method |
| E0207 | shadowing |
Name not found
A variable, function, type, or module name isn’t defined where it’s used.
The message often suggests the closest existing name. Variables are only
visible after their let/var line and inside the block that declares them.
Defined twice
Two items, fields, methods, or parameters share a name in the same scope. Rename one of them.
Unknown type
The type name isn’t a built-in type, struct or enum in scope.
Built-in types: int (i64), i8, i16, i32, u8, u16, u32, u64, float (f64), f32,
bool, str, arrays [T], tuples (A, B).
Types from another module are written module.Name.
No such field
The struct, tuple or enum variant doesn’t have this field. Tuple fields are
numbered: pair.0, pair.1. Enum values have no fields of their own; read
the data of a variant with match:
match shape: case Shape.Circle(r): print(r) case _: passNo such method
The type doesn’t have this method. Arrays have push(x) and pop();
other operations are functions, e.g. len(xs). Methods are defined inside a
struct body; methods with self are called on a value (p.norm()), methods
without self are called on the type (Point.origin()).
Shadowing
brine doesn’t allow a second variable with the same name in one function,
even in an inner block. This avoids a classic source of bugs. Pick a new name,
or use var and assign to the existing variable.
Types (E03xx)
Section titled “Types (E03xx)”Type mismatches, missing or extra arguments, match coverage, generics and conversions.
| Code | Meaning |
|---|---|
| E0301 | type mismatch |
| E0302 | wrong arguments |
| E0303 | not callable |
| E0304 | bad indexing |
| E0305 | bad operands |
| E0306 | missing return |
| E0307 | condition isn’t bool |
| E0308 | can’t infer type |
| E0309 | recursive struct |
| E0310 | not a value |
| E0311 | unsupported extern signature |
| E0312 | range outside a loop |
| E0313 | break/continue outside a loop |
| E0314 | not iterable |
| E0315 | literal out of range |
| E0316 | generator outside a reduction |
| E0317 | constant not computable |
| E0318 | bad format spec |
| E0319 | unsupported type for this operation |
| E0320 | match doesn’t handle every value |
| E0321 | names bound inside ` |
| E0322 | constant used as a pattern |
| E0331 | a method’s type parameter repeats its type’s |
| E0332 | can’t infer a type argument |
| E0333 | too many generic instances |
| E0334 | wrong type arguments |
| E0335 | invalid map key type |
| E0336 | duplicate key in a map literal |
| E0337 | a variable without a written type |
| E0340 | ? on a value that isn’t an Option or Result |
| E0341 | ? in a function that can’t return its failure |
| E0342 | ? inside a comprehension or par body |
Type mismatch
A value has a different type than the one required here. brine never converts implicitly; convert explicitly instead:
float(i) int(x) str(n) i32(n) int("42")Numeric literals adapt to context, so let x: f32 = 1 and let y: u8 = 200
work without conversions.
Wrong arguments
A call has the wrong number of arguments or bad keyword arguments. Struct constructors take every field, positionally or by name:
Point(1, 2)Point(x=1, y=2)Not callable
Only functions, methods, struct names, and built-ins can be called.
Bad indexing
Only arrays can be indexed, and only with an integer: xs[i].
Bad operands
The operator doesn’t work on these types. Arithmetic needs two numbers of the
same type; and/or/not need bool; + also joins two strings. Arrays,
tuples and structs support == and !=; strings and numbers can also be
ordered with <.
Missing return
Some path through a function with a return type reaches the end without a
return. Add a return at the end (or make every branch of a final
if/elif/else return).
Condition isn’t bool
if, elif and while need a bool. brine has no “truthiness”: write
if len(xs) > 0: instead of if xs:.
Can’t infer type
The compiler can’t tell the type from the expression alone. Add an annotation:
var xs: [int] = []let n: int = world.recv(0)Recursive struct
A struct can’t contain itself directly (it would be infinitely large). Use an enum for recursive data: enum values are stored behind a pointer, so a variant may contain the enum itself.
enum Tree: Leaf(value: int) Node(left: Tree, right: Tree)An array of the struct also works: children: [Node].
Not a value
Types, modules and functions can’t be stored in variables or passed around (first-class functions are planned).
Unsupported extern signature
extern fn declares a C function. For now extern functions may only take and
return integers, floats and bools.
Range outside a loop
Ranges (a..b, a..=b, range(...)) are only allowed as the iterable of a
for loop or comprehension. To make an array of a range use
[i for i in 0..n].
Break/continue outside a loop
break and continue only work inside for and while loops.
Not iterable
for can iterate over ranges and arrays.
Literal out of range
The literal doesn’t fit the type it’s used as, e.g. let x: u8 = 300.
Generator outside a reduction
x for x in xs (without brackets) is a generator. It can only be passed to
sum, product, min, max, any or all. Use [x for x in xs] to build an array.
Constant not computable
const values must be computable at compile time from literals and other
constants, using arithmetic, comparisons and logic.
Bad format spec
Format specs in f-strings follow {value:[<|>][0][width][.precision][kind]}:
f"{x:.3f}" 3 decimals f"{n:>8}" right-align in 8 columnsf"{n:05}" zero-pad to 5 f"{n:x}" hexadecimal (also `b` for binary)f"{x:e}" scientificUnsupported type for this operation
This operation only works on certain types. world messages can be int,
float, bool, str, [int] or [float]; world reductions work on int and float.
match doesn’t handle every value
Every match must handle every possible value, so a new enum variant can
never slip through unnoticed. The error shows an example value that no case
handles. Add a case for it, or end with a catch-all:
match shape: case Shape.Circle(r): return 3.14 * r * r case _: return 0.0Cases with an if guard don’t count toward this, because the guard may be
false. Integers and strings have too many values to list, so matching on them
always needs case _: (or a plain name, which also matches everything).
Names bound inside |
In case A | B: either alternative may match, so a name bound in only one
of them might not have a value. Use _ inside | patterns, or write a
separate case for each alternative.
Constant used as a pattern
A plain name in a pattern always captures the matched value under that name; it doesn’t compare against a constant of the same name. Compare explicitly:
match n: case x if x == LIMIT: ...A method’s type parameter repeats its type’s
A method can have type parameters of its own, and a method of a generic
type can also use the type’s parameters. The two can’t share a name, since
T would then mean two things inside the method:
struct Box[T]: v: T
fn pair[T](self, x: T) -> (T, T): # error: `T` is already Box's ...
fn pair[U](self, x: U) -> (T, U): # ok return (self.v, x)Type arguments of a method are inferred from its arguments, or written after
the method name: b.pair[str]("x").
Can’t infer a type argument
The compiler works out type arguments from the arguments and from the type the result must have. When neither says what a type parameter is, write it:
let xs = make[int]() # explicit type argumentlet o: Option[str] = None # or annotate the variablelet p = Pair[int, str](a=1, b="x")Too many generic instances
Each combination of type arguments makes a separate copy of a generic function
or type. A function that calls itself with an ever larger type, like f[[T]]
inside f[T], would need infinitely many. Restructure the recursion so the
type arguments stay the same.
Wrong type arguments
Generic types need one type argument per parameter: Option[int],
Result[int, str], Pair[A, B]. Types that aren’t generic take none.
Invalid map key type
Map keys must be compared with == reliably, so they can be integers,
bool, str, enums, or arrays, tuples and structs made of those.
Floats are not allowed because nan != nan; maps can’t be keys either.
var by_id: {int: str} = {} # okvar cells: {(int, int): bool} = {} # okvar bad: {float: str} = {} # errorDuplicate key in a map literal
The same constant key appears twice in one {...} literal, so the
first value would be silently replaced. Remove one of them.
A variable without a written type
This project asks for a written type on every variable: its brine.toml has
[check]typed = true(or the command was given --typed). Types are written after the name:
let count: int = 0var names: [str] = []let (q, r): (int, int) = (17 / 5, 17 % 5)for i: int in 0..n:for name: str, age: int in ages.items():Comprehension variables ([x * x for x in xs]), match bindings and _
don’t need one: their types come from the code right next to them. Without
typed = true, writing types is optional everywhere.
? on a value that isn’t an Option or Result
x? unwraps an Option or a Result: it gives the value inside Some/Ok,
or returns None/Err from the current function right away.
let n = parse(s)? # parse returns Result[int, str]Other values have nothing to unwrap, so ? doesn’t apply to them.
? in a function that can’t return its failure
? returns early with the None or Err it found, so the function has to be
able to return that:
opt?needs the function to returnOption[...];res?needs it to returnResult[..., E]with the same error typeE.
fn total(parts: [str]) -> Result[int, str]: var sum = 0 for p in parts: sum += parse_int(p)? # an Err(str) is returned as is return Ok(sum)There are no automatic conversions between error types; use match to turn
one error into another. In main, return Result[(), str] to use ?.
? inside a comprehension or par body
A comprehension or par body isn’t a function you can return from early, so
? can’t be used inside it. Write a for loop instead:
var out: [int] = []for s in parts: out.push(parse_int(s)?)Mutability and references (E04xx)
Section titled “Mutability and references (E04xx)”let and var, ref parameters and arguments, and aliasing.
| Code | Meaning |
|---|---|
| E0401 | assigning to an immutable binding |
| E0402 | ref mismatch at a call |
| E0403 | modifying a parameter |
| E0404 | not assignable |
| E0405 | a variable is changed and used again in one expression |
Assigning to an immutable binding
Variables declared with let (and loop variables) can’t change. Declare the
variable with var if it needs to change:
var total = 0total += 1ref mismatch at a call
Arguments are passed by value. When a function takes ref x: T, it can
change the caller’s variable, and the caller must say so explicitly:
f(ref xs). Without ref in the parameter list, the argument may not be
marked ref. This makes every change to a caller’s variable visible at the
call site. Methods that change their receiver are declared with ref self
and called normally: p.move_by(1.0).
Modifying a parameter
Parameters are read-only. To change a local copy, declare the parameter
mut; to change the caller’s variable, declare it ref:
fn countdown(mut n: int): # local copy while n > 0: n -= 1
fn fill(ref xs: [int], v: int): # caller's variable: call as fill(ref data, 0) for i in 0..len(xs): xs[i] = vNot assignable
Only variables, their fields and their elements can be assigned:
x = 1, p.x = 1, xs[i] = 1.
References are made with let r = ref x, where x is a variable. r is
then another name for x for the rest of the block. A reference is always
declared with let (it can’t be re-pointed) and names a whole variable.
A variable is changed and used again in one expression
An expression that changes a variable in place (xs.pop(), m.remove(k),
a ref self method, a ref argument) can’t also read that variable somewhere
else, or change it a second time, because the result would depend on the
order of evaluation:
print(xs, xs.pop()) # error: read and changedprint(xs.pop(), xs.pop()) # error: changed twicexs.push(xs.pop()) # error: changed twicelet last = xs.pop() # ok: change first,print(xs, last) # then readReading the variable in the changing call’s own receiver and arguments is fine: they are evaluated before the call (as in Rust and Swift), and a by-value argument keeps that value even while the call changes the variable:
append_twice(ref v, v) # ok: the second argument is v before the callxs.push(len(xs)) # okThe two sides of and/or, and the parts of a if cond else b, run one
after the other, so they may change and read the same variable:
if not self.eat(Tok.Comma) or self.at(Tok.RBracket): # okThe same variable can’t be passed as ref twice in one call (two names for
one variable inside the function). Copy it first: let snapshot = xs.
Parallel safety and communication (E05xx)
Section titled “Parallel safety and communication (E05xx)”par rules, and the world checks that rule out deadlocks.
| Code | Meaning |
|---|---|
| E0501 | writing shared state in par |
| E0502 | par write array accessed elsewhere |
| E0503 | side effect not allowed in par |
| E0505 | control flow out of par |
| E0507 | unsupported invariant |
| E0508 | filter in par [...] |
| E0510 | communication not reached by every rank |
| E0511 | value must be the same on every rank |
| E0512 | no send/recv |
Writing shared state in par
Iterations of par code run at the same time. To rule out data races, a
par body may not assign variables from outside it, call functions that
mutate them, or push/pop them. The one exception: in par for i in ...,
arrays from outside may be written at index i:
var out = [0.0] * npar for i in 0..n: out[i] = f(i) # OK: each iteration writes its own slotTo combine results, use a reduction instead: par sum(f(i) for i in 0..n).
Par write array accessed elsewhere
An array written as out[i] inside a par for may only be accessed as
out[i] in that loop (with exactly the loop variable as the index), since
any other index might belong to an iteration running at the same time.
Side effect not allowed in par
exit and world communication would be non-deterministic inside parallel
code, and so would write_file, run and the os functions that change
things outside the program (os.spawn, os.wait, os.poll, os.kill,
os.make_dir, os.remove_file, os.rename, os.sleep, os.enable_ansi).
Do them before or after the par construct.
Control flow out of par
return and break can’t leave a par body: other iterations are running
at the same time. continue is fine (it ends the current iteration).
Unsupported invariant
Invariants describe the state between iterations, which doesn’t exist for a
par for. Stepped ranges don’t support invariants yet; rewrite the loop as a
while loop with an invariant.
Filter in par [...]
Parallel comprehensions need a known output size. Filter afterwards, or use
a sequential comprehension: [x for x in xs if x > 0].
Communication not reached by every rank
world communication is collective: every rank takes part in every call,
and all ranks must make the same calls in the same order. If some ranks skip
a call that others make, the others would wait forever. brine rules this out
at compile time.
A call is rejected when the condition that leads to it may differ between
ranks: anything computed from world.rank(), clock(), values received with
world.shift/world.exchange, files, processes, os functions (except
os.name() and os.arch()), or C functions. Early return, break and
continue under such a condition count too, as do and/or.
if world.rank() == 0: world.barrier() # error: only rank 0 gets hereCompute the rank-specific part first, then communicate on every rank:
let mine = 1 if world.rank() == 0 else 0let total = world.sum(mine) # every rank calls thisResults of world.sum, min, max, broadcast and gather_all are the
same on every rank, so they can be used in conditions freely:
while world.max(err) > 1e-6: ...Value must be the same on every rank
Some arguments must have the same value on every rank: the root of
world.broadcast, the distance of world.shift, and parameters that decide
whether a function communicates (for example a loop count around a
world.sum). Compute them from values that are the same everywhere:
constants, world.size(), args(), or results of world.broadcast/sum.
No send/recv
brine has no unmatched send/recv, because a receive without a matching
send hangs forever. Every communication is collective instead:
world.shift(v, 1) # send v to rank + 1, receive from rank - 1 (wraps around)world.exchange(xs) # send xs[j] to rank j; returns what each rank sent youworld.broadcast(root, v) # everyone gets root's valueworld.gather_all(v) # everyone gets [v from rank 0, v from rank 1, ...]For example, “rank 0 sends a message to rank 1”:
var outbox = [""] * world.size()if world.rank() == 0 and world.size() > 1: outbox[1] = "hello"let inbox = world.exchange(outbox)# on rank 1, inbox[0] == "hello"Proofs (E06xx)
Section titled “Proofs (E06xx)”Contracts (requires, ensures, invariant, prove) the compiler could not prove.
| Code | Meaning |
|---|---|
| E0601 | precondition not proven |
| E0602 | postcondition not proven |
| E0603 | invariant false on entry |
| E0604 | invariant not preserved |
| E0605 | claim not proven |
| E0606 | not allowed in a specification |
Precondition not proven
The function’s requires clause must hold at every call, and the compiler
must be able to prove it from what it knows at the call site. Rules can’t be
broken, so an unproven claim is an error, not a runtime check.
Fix it by giving the prover the facts it needs, for example:
if 0 <= i and i < len(xs): # the call is only made when it's safe print(get(xs, i))
assert 0 <= i < len(xs) # checked at runtime, then known to the proverprint(get(xs, i))or by adding a requires to the calling function, which moves the obligation
to its callers. The error lists the facts the prover knew and the claim it
needed.
Postcondition not proven
An ensures clause must hold on every return. The prover reasons about
each path through the function. If it can’t show the claim on some path,
add a requires the claim depends on, an invariant to loops before the
return, or an assert (runtime check) before the return.
Invariant false on entry
A loop invariant must hold before the first iteration. For for i in a..b
loops, invariants may mention i; on entry they’re checked with i = a.
Initialize variables so the invariant holds, or weaken the invariant.
Invariant not preserved
Assuming the invariant (and the loop condition) at the start of an
iteration, the prover couldn’t show it holds again at the end. For
for i in a..b, the invariant is checked at the end of the iteration with
i + 1.
The prover only knows what the invariant says about variables the loop
changes, so invariants often need to carry bounds such as 0 <= i <= n.
Claim not proven
prove e asks the compiler to show e is always true at that point. Unlike
assert, it generates no runtime code, so it must be provable. Use assert
if a runtime check is acceptable.
Not allowed in a specification
Contracts (requires, ensures, invariant, prove) are pure logical
statements. They may use arithmetic, comparisons, and/or/not, len,
abs, min, max, fields, indexing, constants, and result (in ensures).
Function calls and other side effects aren’t allowed.
Driver (E07xx)
Section titled “Driver (E07xx)”Missing modules, no main, and C compiler or linker problems.
| Code | Meaning |
|---|---|
| E0701 | module not found |
| E0702 | no C compiler |
| E0703 | no main function |
| E0704 | cannot write build files |
| E0705 | building the generated code failed |
| E0706 | invalid main signature |
| E0707 | cannot embed a file |
Module not found
import a.b loads a/b.br relative to the directory of the main file.
Check the file name and location.
No C compiler
brine uses the system C compiler (clang or gcc) to build its runtime and
link executables, and for optimized builds it compiles the program through
C. Install one, or point BRINE_CC at it. brine doctor checks your setup.
WebAssembly (--target wasm or web) takes the clang and C library of
wasi-sdk instead: download it from
https://github.com/WebAssembly/wasi-sdk/releases and set WASI_SDK_PATH to
its folder (or unpack it to /opt/wasi-sdk).
No main function
A program starts at fn main(): in its main file. main may also return an
int, which becomes the process exit code. (Libraries and test-only files
can be checked with brine check and tested with brine test.)
Cannot write build files
The compiler couldn’t create its output or cache files. Check permissions
and free disk space for the target/ directory.
Building the generated code failed
The generated code didn’t compile or link. That’s a bug in brine, not in
your program. Please report it with the source file and the output of
brine doctor. Building with BRINE_BACKEND=c (or =native) tries the
other backend.
Invalid main signature
main takes no parameters (command-line arguments come from args()) and
returns one of:
fn main(): # exit code 0fn main() -> int: # the exit codefn main() -> Result[(), str]: # Err(e) prints "error: e" and exits with 1The Result form lets main use ?:
fn main() -> Result[(), str]: let text = read_file("config.txt")? print(len(text)) return Ok(())Cannot embed a file
embed(path) puts a file’s text into the program when it is compiled, so
the file must exist then, be UTF-8 text, and its path must be a string literal
(not a variable or an f-string). The path is relative to the directory of the
.br file that contains the call; “\r\n” line endings become “\n”.
const HELP = embed("help.txt") # help.txt next to this filefn main(): print(HELP)To read a file while the program runs instead, use read_file(path).