Language
Programs
A program is every source file given to the compiler, taken together. Files contain only declarations: functions, records and unions. Declarations can appear in any order and in any file.
Execution starts at main, which must be declared exactly as fn main() -> int. Its result becomes the exit status of the program, of which the operating system keeps the lowest eight bits.
There are no comments.
Names
A name is made of letters, digits and underscores, and does not start with a digit.
These words are reserved: fn, record, union, let, if, else, match, true, false, and, or, not.
Every top-level name must be unique across the whole program. That covers functions, records, unions, union cases, the built-in functions and the built-in types together. Names made by let, parameters and match patterns are separate from those, and a later one hides an earlier one of the same name.
Types
| Type | Values |
|---|---|
int | Whole numbers of 64 bits, from -9223372036854775808 to 9223372036854775807. |
bool | true and false. |
bytes | A fixed run of bytes. |
never | No values. The type of an expression that does not finish, such as a call to halt. |
| a record | One value for each declared field. |
| a union | One of its declared cases, with that case’s fields. |
A value of type never is accepted wherever any other type is expected. Nothing else is converted automatically.
Literals
An int literal is a run of decimal digits that fits in 64 bits. A negative number is written with the - operator.
A bytes literal is written between double quotes and must end on the line it starts on. Inside it:
| Written | Byte |
|---|---|
\n | Line end, 10. |
\t | Tab, 9. |
\\ | Backslash. |
\" | Double quote. |
\x41 | The byte with that value, as two hexadecimal digits using 0-9 and A-F. |
Functions
fn name(first: type, second: type) -> type { body }Parameters are separated by commas, and each has a type. The result type is required. The body is a block, and its value is the function’s result.
A call passes its arguments by position: name(1, 2).
A function that calls itself as the last thing it does reuses its stack frame, so such a call can repeat without limit. This applies when the call is the value of the function body, of a branch of an if or an arm of a match in that position, or of the last line of a block in that position. Any other call uses stack space until it returns.
Blocks and let
A block is a sequence in braces. Each line except the last ends with a semicolon and is one of:
let name = expression;let name: type = expression;expression;, whose value is dropped.
The last line is an expression without a semicolon, and its value is the value of the block. A block cannot be empty.
A name introduced by let is visible from the next line to the end of its block. It cannot be assigned again.
Operators
From loosest to tightest:
| Operators | Operands | Result | Notes |
|---|---|---|---|
or | bool | bool | The right side is not evaluated when the left is true. |
and | bool | bool | The right side is not evaluated when the left is false. |
not | bool | bool | |
== != | two int or two bool | bool | Not available for bytes, records or unions. |
< <= > >= | int | bool | |
+ - | int | int | |
* / % | int | int | |
- before a value | int | int | |
.field | a record | the field’s type |
The six comparison operators share one level, and comparisons do not chain: an expression holds at most one without parentheses. The arithmetic operators group from left to right. Parentheses group anything.
Arithmetic that goes past the range of int wraps around. Division drops the fraction, rounding toward zero, and % gives the remainder of that division. Dividing by zero stops the program with an error. Dividing the smallest int by -1 wraps around to the smallest int again.
if
if condition { first } else { second }
if condition { first } else if other { second } else { third }The condition is a bool. The else is required, and all branches produce the same type.
Records
record point { x: int, y: int }A record value is made by naming every field in declared order: point(x: 3, y: 4). A field is read with a dot: p.x. A record cannot be changed after it is made and cannot be compared with ==.
Unions
union shape { circle(radius: int), square(side: int), empty() }A union has at least one case. Every case is written with parentheses, with or without fields. A case value is made the way a record is: circle(radius: 3), empty(). A union may refer to itself in its fields. A union value cannot be compared with ==.
match
match value {
circle(radius) => radius * radius * 3,
square(side) => side * side,
empty() => 0
}match takes a union value. It must have exactly one arm for every case of that union, in any order. An arm names the case, gives one new name for each of its fields in declared order, and after => an expression that may use those names. All arms produce the same type.
What is not in the language
Loops, assignment, comments, null, exceptions, generic types, function values, modules and floating-point numbers.