Syntax
This chapter describes the surface syntax exhaustively. Source files use the
.cz extension; one file is one module (see Modules).
Lexical structure
Section titled “Lexical structure”Comments
Section titled “Comments”// a line comment, to the end of the line# a line comment as well/* a block comment /* which may nest */ still a comment */Block comments nest: each /* must be closed by its own */. A newline
inside a block comment does not separate statements; a newline that ends a
line comment does. // and # are interchangeable; because # starts a
line comment, a file may begin with a shebang line
(see Hello, world!):
#!/usr/bin/env brassDoc comments
Section titled “Doc comments”A block comment that opens with /** is a documentation comment. Written
directly above a fun or type declaration, it attaches to that declaration
and is shown by editor tooling: the language server
renders it on hover and in completion, below the signature:
/** The area of a circle with radius `r`. */fun area(r: float64) -> float64 { return 3.14159 * r * r}
/** * A named point on the screen. * Coordinates are pixels. */type Point = { x: int32 y: int32}
println(area(2.0))The text is the comment body with the /** and */ markers and each line’s
leading * decoration removed; it is treated as markdown. Attachment rules:
- The doc comment must sit directly above the declaration: only line breaks and ordinary comments may come between. Several stacked doc comments join into paragraphs.
- Doc comments attach to
fun(including method implementationsfun T.m(...)) andtypedeclarations only. One written above anything else, such as a top-levellet/const, an import, or a member signature inside a type body, is ignored like an ordinary comment. - A plain
/* ... */(single asterisk) is never a doc comment, and the empty/**/is a plain comment.
Doc comments have no runtime presence; they exist for readers and tooling. The standard library documents its public functions and types this way.
Identifiers and keywords
Section titled “Identifiers and keywords”An identifier starts with an ASCII letter or _ and continues with ASCII
letters, digits, or _. Unicode identifiers are not supported. A lone _ is
the wildcard pattern.
The reserved keywords are:
type fun let const if else match for while inreturn break continue null true false self Self importEverything else is an ordinary identifier. In particular ref, mut,
infer, typeof, anonymous are recognized contextually in type position;
print, println, and assert are prelude functions. The compiler-owned
function names len, spawn, with, sync, error, fields, and typeof
cannot be redefined. There is no loop, defer, pub, impl, or trait.
Integer literals
Section titled “Integer literals”Decimal by default, with radix prefixes and _ separators:
let a = 1_000_000let b = 0xFF // hex (0x / 0X)let c = 0b1010 // binary (0b / 0B)let d = 0o755 // octal (0o / 0O)There are no type suffixes: the type of a literal is decided by inference
(see Type system). A literal too large for int64 is a
lexing error. A leading zero alone does not select octal: 0755 is decimal
755. A numeric literal cannot be followed immediately by a letter, digit, or
_ that it cannot consume: 0b12, 0o78, and 0xFFg are errors, not
separate tokens. _ separators remain allowed.
Float literals
Section titled “Float literals”A float literal requires a digit after the decimal point (1.0, not 1.),
and accepts an exponent and _ separators: 6.02e23, 1_0.5. Because a
float needs a digit after ., 42.foo lexes as the integer 42 followed by
a field access. There are no float suffixes, and radix prefixes do not apply
to floats. An exponent marker (e or E), with an optional sign, must be
followed by digits (separators are allowed); 1e and 1e+ are errors.
A literal that overflows float64 is a lexing error. A literal used as
float32 must also remain finite at that width; rounding such as 0.1 and
underflow to zero such as 1e-400 are allowed.
String literals and interpolation
Section titled “String literals and interpolation”Strings are double-quoted UTF-8. The recognized escapes are:
\n \t \r \0 \\ \" \{ \}Any other escape is an error. {expr} inside a string is interpolation:
the expression is evaluated and its text inserted. Literal braces are written
\{ and \} (there is no {{ doubling). The interpolation scanner balances
nested braces and skips nested string literals and comments, so braces inside
//, #, or nested block comments do not close an interpolation:
let items = ["a", "b"]println("first = {items[0]}, count = {items.len()}")There is no character type (a character is a one-character string) and no
raw or multi-line string form (use \n). A raw newline in string text is an
error reported at that string’s opening quote. Newlines are allowed inside
an interpolation’s expression. Strings nested inside interpolations follow
the same rules, including their own escapes and recursive interpolations.
Statements and newlines
Section titled “Statements and newlines”Newlines separate statements; there are no semicolons (; is not a token). Each
statement, top-level declaration, and import must end at a newline, the closing
} of its enclosing block, or end of file. A line continues onto the next in
these cases:
- the line ends with a binary or assignment operator;
- the next line begins with
.(method chain); - an
elsebegins the next line after anifblock; - inside parentheses
(), brackets[], and the braces of record literals, newlines within an expression are insignificant. Statement blocks inside brackets still require statement boundaries.
let total = 100 * 2 + 50
let result = [3, 1, 4] .map((x) -> x * 10) .fold(0, (a, b) -> a + b)Type members, refinement entries, and match arms require commas or newlines
between entries. Trailing commas are accepted in every
comma-separated list (arguments, parameters, array literals, field lists,
imports, patterns, and interface lists). A record pattern may end with ..,.
Operators
Section titled “Operators”From lowest to highest precedence; all binary operators are left-associative:
| Precedence | Operators | Meaning |
|---|---|---|
| 1 (lowest) | || |
logical or |
| 2 | && |
logical and |
| 3 | == != < <= > >= |
comparison |
| 4 | | |
bitwise or |
| 5 | ^ |
bitwise xor |
| 6 | & |
bitwise and |
| 7 | << >> |
shifts |
| 8 | + - |
additive |
| 9 | * / % |
multiplicative |
| 10 | -x !x ~x |
prefix: negation, logical not, bitwise not |
| 11 (highest) | x.f f(args) x[i] x! |
postfix: field, call, index, error propagation |
Notes:
- Postfix
!is error propagation. There is no postfix?operator and no optional chaining (?.);?appears only as the nullable type suffix. - Assignment is a statement, not an expression. The assignment operators are
=+=-=*=/=%=; there are no bitwise or shift compound assignments. The target may be a variable, a field (a.b), or an element (a[i]). - There is no general range operator. The bracket form
[lo..hi]is an expression producing the half-open integer rangelo .. hi-1as an array:[1..5]is[1, 2, 3, 4].
Expressions
Section titled “Expressions”-
Literals: integers, floats, strings,
true,false,null. -
Array/tuple literal
[a, b, c]: how a bracket literal is typed (fixed array, growable array, or tuple) is described in the type system. -
Record construction
TypeName { field: value, ... },Self { ... }inside methods, and variant constructionType.Variant { ... }(a unit variant isType.Variantwith no braces). A barefieldis shorthand forfield: field; see Field-init shorthand. -
Anonymous record literal
{ field: value, ... }: a structural record with no declared type. An opening{otherwise starts a block; the literal form is recognized by the lookahead{ name:,{ name,, or{ name }. A closure’s-> {always opens a block body, so a closure returning a record parenthesizes it:(x) -> ({ x }). -
if/elseis an expression and yields a value;else ifchains. The condition does not take a record literal directly (parenthesize if needed). -
Type test
if subject: Type { ... }orif !(subject: Type) { ... }: tests the subject’s static type at compile time (see Type tests). The subject must be a postfix-level expression (identifier, literal,self, field, call, index, postfix!, or parenthesized expression). Group a binary or prefix subject:if (a == b): bool. Negation requires the complete!(subject: Type)form;if !v: Tis invalid. Tests are only completeif/else ifheads, never values or operands of&&/||;if (v: T)is invalid. In the positive form the type is parsed like a return type, so the following{opens the arm’s block. -
Parenthesized callee
(e)(args)evaluateseas a value and calls it with the written arguments. A closure-valued field works this way. A method member is not a function value;(b.g)(3)is rejected with`b.g` is a method; call it as `b.g(...)`. -
if letif let PATTERN = expr { ... } else { ... }matches one pattern; on a nullable scrutinee, a bare name pattern binds the non-null value. -
matchis an expression:match scrutinee {pattern => expression,pattern => { block },pattern => target = value, // an assignment arm is allowed}Arms are separated by commas or newlines. There are no match guards.
-
Closures
(params) -> expror(params) -> { block }. Parameter annotations are optional; zero parameters are written(). A block body yields a value only throughreturn; an expression body yields the expression. A(opens a closure exactly when its matching)is followed by->. -
Calls, indexing, field access as usual:
f(a, b),xs[i],p.x. Method calls arerecv.method(args); static methods areType.method(args).
Expressions, types, patterns, and prefix chains share one 64-level nesting budget, so mixing those forms does not reset the limit. Exceeding it reports a diagnostic instead of overflowing the parser stack.
Statements
Section titled “Statements”The complete statement list: let/const, assignment, expression statement,
while, for, return, break, continue.
let and const
Section titled “let and const”const pi = 3.14159 // immutable bindinglet count = 0 // mutable bindinglet x: int64 = 10 // with a type annotationlet [a, b] = [10, 20] // destructuring (array/tuple pattern)let p: Point // uninitialized: `let` + annotation onlyconst requires an initializer. An unannotated let requires an
initializer. An annotated let may omit it; the binding must then be
definitely assigned before use (see
definite assignment).
A const binding cannot be mutated at any depth of an evaluated expression:
mutating calls nested in an assignment right-hand side or interpolation
segment are rejected just like a direct mutation.
while cond { ... }for x in xs { ... } // arrays and [lo..hi] rangesbreakcontinueThe for head accepts an irrefutable pattern, including record and tuple
destructuring. break and continue are bare; there are no labels.
return
Section titled “return”return is valid only inside a function or closure body, including nested
control-flow blocks. It is an error in module initialization.
return with no value returns void; return expr returns the value. In a
fallible function a plain value is wrapped in Result.Ok automatically (see
Result).
Patterns
Section titled “Patterns”Patterns appear in match arms, if let, let/const declarations, and for
heads:
| Pattern | Example | Notes |
|---|---|---|
| Wildcard | _ |
matches anything, binds nothing |
| Binding | x |
binds the whole value; also matches a unit variant of that name |
| Literal | 0, -5, 1.5, "s", true, null |
negative numeric literals allowed |
| Variant / record | Circle { radius }, Shape.Circle { radius: r } |
field shorthand binds the field name; field: subpattern destructures further |
| Rest | Full { data, .. } |
.. ignores the remaining fields |
| Array | [a, b] |
fixed length; no .. rest inside arrays |
A variant pattern’s type qualifier is optional: Circle { radius } and
Shape.Circle { radius } both select a variant. When written, the qualifier
must name the matched value’s sum type and that type must declare the variant.
Module paths and renamed imports are allowed (vec.Shape.Circle { radius }).
A qualified bare name such as Shape.Dot is always a unit-variant test, never
a binding; a misspelled type or variant is an error. A unit pattern requires a
variant without fields. For an inferred scrutinee, the qualifier selects its
sum type. A value converted to a declared parent sum is matched with the
parent’s qualifier; a value still typed as the child uses the child’s qualifier.
A module-qualified record pattern such as pt.P { x, y } instead names the
record type P exported by module alias pt. Its nominal type must match the
scrutinee’s; if the scrutinee is still inferred, the pattern selects that record
type. This form also works in let and nested field sub-patterns.
Every name may be bound only once within a pattern, including nested fields and array elements. Separate arms and separate declarations have separate binding scopes. String patterns must be plain literals: interpolation is a compile-time error, even when the interpolated expression is constant.
The pattern in let, const, or for must be irrefutable. Bindings, _,
plain records, and braced variants of a sum with exactly one variant are
allowed, with the same rule applied recursively to sub-patterns. Literals,
unit-variant names (bare or qualified), and variants of multi-variant sums
require if let or match. Tuple and fixed-array destructures must have the
correct arity. A growable array cannot be destructured in these positions
unless the declaration’s initializer is an array literal of the matching
length; the same restriction applies to nested patterns.
There are no or-patterns (|), no @ bindings, and no range patterns.
match over a sum type must be exhaustive (see
exhaustiveness).
Declarations
Section titled “Declarations”Functions
Section titled “Functions”fun name(param, other: Type, third: ref(mut(T))) -> ReturnType { ...}A parameter named _ is anonymous: it binds nothing, cannot be read as a value,
and may appear several times in a parameter list. This applies to functions,
methods, and closures. self is allowed only as the first parameter of a method
implementation or a method signature in a type body.
Both parameter and return annotations are optional. When written, an annotation
must resolve to a valid type; an invalid annotation is an error rather than an
unannotated inference hole. fun T.m(params) declares a method of type T
(see methods); the standard library
also uses the receiver forms fun string.m, fun int32.m (any primitive type
word), fun infer[].m, and fun string[].m to put methods on primitive and
array types (user code cannot).
type Name = { members } // recordtype Name = V1 { members } | V2 // sum typetype Name: IfaceA, IfaceB = { members } // with interface constraintstype Child: Parent = V1 { .. } | V2 // sum with a declared parent (subtyping)type Alias = Base { field: T, ... } // refinement alias (see below)A member is either a field (name or name: Type, the annotation is
optional), a type slot (type name, a type parameter with no storage,
see Type slots; the older
spelling name: type is also accepted), or a method
signature (name(params) -> Ret with no body). A method body inside a type
block is a parse error: implementations go outside the type, as fun T.m(...),
in the same module. Members are separated by commas or newlines. A method
signature or implementation cannot share a name with a record field or a
field of any variant of its sum type.
Field names must be unique in anonymous record literals, anonymous record types, and refinement lists, just as in nominal and variant literals.
Variants of a sum type are Name or Name { members }, separated by |,
which may start a new line. A leading | is optional for a sum with two or
more variants but required for a one-variant sum (type X = | Only { .. }),
so that type Alias = Base { .. } reads as a refinement rather than a
one-variant sum. Variant names must be plain undotted identifiers, excluding
Self and all keywords, in either spelling. A right-hand side that is a plain type expression (a
refinement, or any type) declares an alias: the name resolves to that type
and is not a new nominal.
Imports
Section titled “Imports”import geometry.vec.{ Vec2, dot } // named importsimport geometry.vec.{ dot as vdot } // rename a nameimport geometry.vec.Vec2 // one name, same as .{ Vec2 }import geometry.vec // whole module: vec.dot(..), vec.Vec2import geometry.vec as g // module with custom qualifier: g.dot(..)import path.{ Name, ... } imports the listed names (Name as Local renames);
import path.Name imports the one trailing name; a bare import path imports
the module, whose exports are then used qualified by the path’s last segment
(vec.Vec2, vec.dot(a, b)). import path as name overrides the qualifier.
See
Modules for how the brace-less forms are
distinguished, path resolution, and visibility.
Top level
Section titled “Top level”A module is a sequence of imports, type declarations, fun declarations,
and bare statements, in any order. Functions and types may be referenced
before their textual definition; a top-level binding may not (using a global
before its initializer runs is a compile error). Top-level statements execute
in order when the module loads; main, if defined, runs afterwards.
Top-level statements may use the ! operator: a failed propagation stops
the program with the error instead of returning a Result (see
Result).
Type expressions
Section titled “Type expressions”Base forms:
| Form | Meaning |
|---|---|
int32, Point, … |
named type (primitives are ordinary names) |
infer |
an inferred hole; each occurrence is independent |
[T, U, ...] |
tuple |
(T, U) -> R |
function/closure type |
Self |
the enclosing type, in methods |
mut(T) |
mutable deep-copy parameter only |
ref(T) |
immutable-reference parameter only |
ref(mut(T)) |
mutable-reference parameter only (writes through) |
typeof(expr) |
the static type of expr (see Reflection) |
anonymous { name: T, ... } |
inline structural record type |
type |
a type slot, only as a whole field’s type (a type parameter) |
Self.field |
the type of the enclosing type’s field/slot named field |
Base { field: T, ... } |
a refinement pinning Base’s slots/fields (see Type slots) |
ref(T), ref(mut(T)), and mut(T) are whole parameter annotations only,
including closure/method parameters and parameters of function types.
T must contain no ref/mut. They cannot appear in return types, fields,
variable annotations, type arguments, or inside other type wrappers.
Suffixes on value types, applicable repeatedly and in any order:
| Suffix | Meaning |
|---|---|
T[] |
growable array (slice) |
T[n] |
fixed-length array (n a non-negative integer literal) |
T? |
nullable |
T! |
Result with success payload T |
So int32?[] is an array of nullable int32, int32[]? a nullable array, and
infer?[] and (int32) -> int32? combine as expected. A parameter may use
ref(mut(int32[])); ref(int32)[] is invalid.
The primitive named types are int8, int16, int32, int64, uint8,
uint16, uint32, uint64, float32, float64, bool, string, and
void (the no-value return type).