Language Basics
Part of: Reef Language Reference Last reviewed on version: 0.10
Table of Contents
- Introduction
- Getting Started
- Lexical Structure
- Types
- Variables
- Operators
- Control Flow
- Functions and Procedures
- Structs and Records
- Enums and Pattern Matching
- Arrays
- Sets
- Subranges (removed in 0.10)
Active Objects, Passive Objects, Generics, Modules, and FFI each have their own dedicated chapter — see 045_ACTIVE_OBJECTS.md, 040_OBJECTS.md, 055_GENERICS.md, 075_MODULES.md, and 105_FFI.md.
1. Introduction
What is Reef?
Reef is a modern, statically-typed systems programming language with language-level concurrency through Active Objects. Reef combines:
- Type safety from ML and Rust
- Clean syntax from Ruby and Crystal
- Concurrency from Active Oberon
- Systems performance from C
Key Features
- Active Objects: Language-level safe concurrency with automatic synchronization
- Passive objects: Classes with single inheritance and virtual methods (040_OBJECTS.md)
- Pattern Matching: First-class pattern matching with exhaustiveness checking
- Generics: Compile-time specialization (monomorphization)
- Set Types: Efficient bitsets from Pascal/Oberon tradition
- Memory Safety: Automatic garbage collection for heap-allocated values.
Raw pointers obtained inside
unsafeblocks are outside that guarantee. - Zero-Cost Abstractions: Compiles to efficient C code
When to Use Reef
Reef excels at:
- Concurrent systems programming
- Safe multi-threaded applications
- Systems requiring both safety and performance
- Projects benefiting from Active Object patterns
Reef is inspired by:
- Active Oberon (A2) - Active Objects, concurrency
- Modula-3 - Module system, safety
- Rust - Type safety, modern syntax
- Ruby/Crystal - Clean, readable syntax
- Pascal - Set types
2. Getting Started
Installation
See INSTALLATION.md in the main repository.
Requirements:
- reef-runtime (C library)
- OCaml and Dune (to build compiler)
- GCC or Clang (C11 support)
Your First Reef Program
proc main()
println("Hello, Reef!")
end main
Note: Every Reef program needs a
mainentry point. Useproc main()for simple programs, orfn main(): intwhen you need to return an exit code.
Compile and run:
reefc hello.reef
./hello
Output:
Hello, Reef!
Program Exit Codes
Programs can report success or failure to the operating system via exit codes.
Option 1: fn main(): int - Return an exit code from main:
fn main(): int
if something_failed
return 1 // Error
end if
return 0 // Success
end main
Option 2: exit(code) - Exit immediately from anywhere:
proc validate()
if invalid_input
println("Error: invalid input")
exit(1) // Terminate program immediately
end if
end validate
proc main()
validate() // May exit here
println("Valid!") // Only runs if validate() didn't exit
end main
Key points:
proc main()always exits with code 0fn main(): intreturns whatever you specifyexit(code)can be called from any function/procedure- Exit code 0 = success, non-zero = error
Check exit code in shell:
./myprogram && echo "Success" || echo "Failed"
echo $? # Shows the exit code
See Error Handling - Exit Codes for exit code conventions and more examples.
A Simple Function
fn add(a: int, b: int): int
return a + b
end add
proc main()
let result = add(5, 3)
print("5 + 3 = ")
print_int(result)
println("")
end main
Output:
5 + 3 = 8
3. Lexical Structure
Comments
// Single-line comment
/*
Multi-line comment
/* Nesting is supported */
*/
Identifiers
- Start with letter or underscore:
a-z A-Z _ - Continue with letters, digits, underscores:
a-z A-Z 0-9 _ - Case-sensitive
- Examples:
counter,_temp,MyType,value_2
Keywords
Reserved words (cannot be used as identifiers):
Control flow: if, else, elif, then, unless, when, while, for, in, to, step, loop, do, until, break, continue, match, defer, end
Functions: fn, Fn, proc, return, spawn
Variables: let, mut, const
Types: type, struct, record, enum, setof, byte, impl, is, trait, where
Reserved (not a type in 0.10): subrange — still reserved so it cannot be an identifier; subrange types were removed (N10).
Active Objects: active, object, exclusive, shared, await, init, finalize, run, self
Passive objects (reserved since 0.9 Phase 6f — see the migration note in the CHANGELOG): typecase, inherited
Modules: import, export, module, as, ifarch
Primitive types: bool, char, int, string, float, int8, int16, int32, int64, uint8, uint16, uint32, uint64, float32, float64, size_t, pointer, Any
FFI: extern
Literals: true, false, new, nil
Special: unsafe, asm, to_map, type_id, type_name
Logical word forms: and, or, not (aliases for &&, ||, !)
This list (85 entries) is generated from the compiler's keyword table; using any of these as an identifier is a syntax error (the compiler names the word — see BUG-153).
Contextual keywords
Two words of the 0.9 passive-object surface are contextual, not
reserved: extends and override. Each has special meaning only in
its object-declaration position (object Button extends Widget,
override shared proc paint()) and remains an ordinary identifier
everywhere else — existing code using these names keeps compiling:
fn extends(override: int): int
return override + 1
end extends
proc main()
let answer = extends(2)
println("${answer}")
end main
typecase and inherited are different: they are reserved words
(0.9 Phase 6f, owner decision 2026-08-12 — a deliberate pre-1.0
breaking change). The contextual spellings were tried first and
measured: contextual typecase is structurally unparseable at
statement head, and contextual inherited could not represent
value-position base calls (let r = inherited area()). Reserving the
two words makes both forms ordinary grammar, at the standard cost —
using either word as an identifier is now a syntax error:
example.reef: Syntax error at line 2, column 17
note: 'inherited' is a reserved word and cannot be used as an identifier
(As with every reserved word, the reported location points at the token AFTER the identifier. See the CHANGELOG's 0.9 migration note; the 85-entry list above includes both words.)
What these four words do today
All four compile. Inheritance (extends / override / inherited),
typecase, finalize, and traits on objects are in 0.9 — see
040_OBJECTS.md. A stray override or inherited on a
class with no base is still a type error about your program:
| word | meaning |
|---|---|
extends |
one base class; methods are virtual |
override |
mandatory on a redefinition; rejected when nothing matches |
inherited |
static call to the defining class's base implementation |
typecase |
first-match runtime type dispatch; binder is the narrowed view |
extends and override remain ordinary identifiers outside those
positions (the fn extends(override: int) block above). typecase and
inherited cannot be identifiers.
The surface that is not in 0.9 is listed at the end of
040_OBJECTS.md (dyn Trait, generic objects,
object-typed spawn arguments, multiple inheritance).
Each word names one construct. A program that misuses one is diagnosed at that line rather than as a generic "objects" failure.
Block Terminators
All blocks use end with optional labels:
fn calculate(x: int): int
if x > 0
return x
end if
return 0
end calculate
Labeled ends (recommended):
fn calculate(x: int): int
if x > 0
return x
end if
return 0
end calculate
Rule: Named constructs (functions, types, objects) prefer end Name, control structures use end keyword.
4. Types
Primitive Types
Boolean
let flag: bool = true
let active: bool = false
Values: true, false
Integer Types
Default integer:
let count: int = 42 // 32-bit signed; `int32` is the same type
Sized integers:
let small: int8 = 127
let medium: int16 = 32000
let standard: int32 = 1000000
let large: int64 = 9223372036854775807
let u: uint8 = 255
let word: uint16 = 65535
let dword: uint32 = 4000000000
let qword: uint64 = 18446744073709551615
int and int32 are one type (two spellings), like byte and uint8
(byte is a type name, not a legal variable). Unsuffixed integer literals
adapt to the annotation if the value fits, so let u: uint8 = 255
is legal; let too_big: uint8 = 256 is a compile error.
Integer literal suffixes:
let a = 127i8 // int8
let b = 255u8 // uint8
let c = 1000i16 // int16
let d = 65535u16 // uint16
let e = 1000000i32 // int32 (same type as `int`)
let f = 1000000u32 // uint32
let g = 123456i64 // int64
let h = 999999u64 // uint64
Digit separators:
let million = 1_000_000 // decimal, grouped by thousands
let mask = 0xFFFF_FFFFu32 // hex, grouped at byte boundaries
let bits = 0b1010_1010 // binary, grouped at nibbles
let perms = 0o7_5_5 // octal
let ratio = 0.1_234 // floats, fraction and exponent alike
let big = 1.5e1_0
Unsuffixed integer literals take their type from context when the value
fits (binding annotation, argument slot, the other operand of a binary
op, an as target, a return type). With no context, the type is
int if the value fits in signed 32-bit, otherwise int64 if it fits
in signed 64-bit, otherwise a compile error: let x = 3000000000 is
int64, and let a: int = 3000000000 is rejected. Suffixes override
and are still range-checked (300u8 is an error, not a wrap). Constant
integer expressions fold, then the result is range-checked, so
0 - 2147483648 as int is INT_MIN and legal.
An underscore in a numeric literal is grouping for the reader and nothing
more. The lexer strips it, so it never reaches the type system or the
generated code — two files differing only in separator placement compile to
identical output. It is unrelated to the decimal point: 1_5 is the integer
15, and . is still the only way to write a float.
A separator must sit between two digits. _1 is an identifier rather than a
literal, and 1_, 1__0, 0x_1 and 0xFF_ are all rejected — the first two
with a "Misplaced digit separator" error naming the radix. This matches the
rule encoding.toml already applies to TOML input.
Because grouping carries no meaning, an inconsistent one can mislead where
plain digits cannot: 1_00_000 reads as a million and is one hundred
thousand. Group at the boundaries the radix suggests.
Floating-Point Types
let pi: float = 3.14159 // 64-bit (default); `float64` is the same type
let small = 2.5 as float32 // 32-bit (float literals are `float`; cast to narrow)
let precise: float64 = 1.0 // same type as `float`, C `double`
float and float64 are one type (two spellings), both C double.
float32 is distinct. float32 + float promotes to float. Float
literals (1.0, 1e3) are float. Narrowing to float32 is
explicit: write 1.5 as float32. An annotation does not adapt —
let x: float32 = 1.5 is a type error.
Float arithmetic is IEEE-754: overflow is Inf / -Inf, invalid
results (such as 0.0 / 0.0) are NaN, and neither panics. Converting
a non-finite or out-of-range float to an integer with as panics at
run time; a constant that does not fit (1.0e20 as int) is a compile
error.
Exponent notation:
let avogadro = 6.022e23 // e or E, then the power of ten
let tiny = 1.5e-8 // negative exponent
let loud = 2.5E+10 // explicit + is allowed
let thousand = 1e3 // no fractional part needed with an exponent
A literal with an exponent is always a float, even when it has no decimal
point and names a whole number: let n: int = 1e3 is a type error, not the
integer 1000.
The exponent needs at least one digit. 1.0e is not a malformed literal but
the float 1.0 followed by an identifier e, so it fails with
Undefined variable 'e'. A float without an exponent still needs a digit after
the point — 1. is a syntax error — which is what keeps 1..10 a range.
Character Type
let letter: char = 'A'
let newline: char = '\n' // also \t, \r, \0, \\, \', and \xNN
A char is one unsigned 8-bit UTF-8 code unit on every target
(C unsigned char). 'é' is a lexical error (two UTF-8 bytes).
'\xFF' is byte 255. '\u{E9}' is not a char (that spelling is
Latin-1, not UTF-8 é). Indexing a string yields bytes: in "héllo",
index 1 is the first byte of é. Code points use int, as
input.keys.KeyCode.Char does. char and uint8 are distinct types
with implicit conversion both ways; ${c} prints a character and
${c as uint8} prints a number.
String Type
let message: string = "Hello, Reef!"
let path: string = r"C:\Users\name" // Raw string (no escapes)
Escape sequences:
\n\t\r\\\"\xNN— one raw byte (may be invalid UTF-8)\u{N}— UTF-8 encoding of code point N (surrogates and > 0x10FFFF are errors)
System Types
size_t and pointer exist to describe values that cross the C boundary.
size_t is the same type as uint64. Unsuffixed integer literals adapt
into size_t if they fit (let n: size_t = 64); a typed int needs
n as size_t. pointer values need an unsafe context:
extern "C" fn strlen(s: string): size_t
extern "C" fn malloc(n: size_t): pointer
proc example()
let n: size_t = 64 // unsuffixed literal adapts
let size: size_t = strlen("hello") // C size_t type
unsafe
let ptr: pointer = malloc(size) // C void* equivalent
let p2: pointer = malloc(n)
end unsafe
end example
Composite Types
Arrays
Array types:
type IntArray = [int]
type Matrix = [[float64]] // 2D array
Array literals:
let numbers = [1, 2, 3, 4, 5]
let bools = [true, false, true]
let strings = ["hello", "world"]
Array operations:
let first = numbers[0] // Read
numbers[2] = 100 // Write
Example:
proc test_arrays()
let mut arr = [10, 20, 30]
arr[1] = 25
print("arr[1] = ")
print_int(arr[1])
println("")
end test_arrays
Output:
arr[1] = 25
Sets
Set types - Unordered collections of unique elements:
type Flags = setof[int]
type FileMode = enum
Read
Write
Execute
end FileMode
type Permissions = setof[FileMode]
Set literals:
let s1: setof[int] = {1, 2, 3}
let s2: setof[int] = {0..9} // Range notation
let s3: setof[int] = {0, 2, 5..10} // Mixed
Set operations:
let union = s1 + s2 // Union
let intersection = s1 * s2 // Intersection
let difference = s1 - s2 // Difference
Membership test:
if 2 in s1
println("2 is in the set")
end if
Complete example:
proc test_sets()
let s1: setof[int] = {1, 2, 3}
let s2: setof[int] = {2, 3, 4}
if 2 in s1
println("2 is in s1")
end if
if !(5 in s1)
println("5 is not in s1")
end if
end test_sets
Output:
2 is in s1
5 is not in s1
Implementation: Sets are implemented as 32-bit bitsets (uint32_t), supporting elements 0-31.
Subranges — removed in 0.10
Subrange types (subrange 0 9 and 0..9 as a type) were removed in 0.10
(N10). Use a fixed-width integer (int, uint8, …) or a type alias to one.
.. remains for set ranges ({0..9}) and slices (a[1..2]).
5. Variables
Immutable Variables (let)
let name: type = value
let name = value // Type inferred
Example:
let x: int = 42
let message = "Hello" // Type inferred as string
let pi = 3.14159 // Type inferred as float
Mutable Variables (mut)
mut counter: int = 0
mut flag = true // Type inferred
Mutation:
counter = counter + 1
flag = false
Type Inference
Type annotations are optional when type can be inferred:
let x = 42 // Inferred as int
let s = "hello" // Inferred as string
let arr = [1, 2, 3] // Inferred as [int]
let obj = new Counter() // Inferred from constructor
When type annotation is required:
- Ambiguous expressions
- Empty collections
- Function parameters (always required)
- Explicit type desired for clarity
6. Operators
Arithmetic Operators
+ // Addition
- // Subtraction
* // Multiplication
/ // Division
% // Modulo
There is no ** exponentiation operator (removed in 0.10). Integer
powers of two use 1 << n. Other integer powers use a loop or
math.pow_int (returns Option[int]; overflow is None). Float
powers use math.pow_f.
Integer arithmetic:
let sum = 5 + 3 // 8
let diff = 10 - 4 // 6
let product = 6 * 7 // 42
let quotient = 15 / 3 // 5
let remainder = 17 % 5 // 2
Integer + - * and unary - wrap at the result type's width:
127i8 + 1i8 is -128 both stored and inline, and -INT_MIN is
INT_MIN. Integer / and % truncate toward zero (matching pair)
and panic on / 0, % 0, and signed INT_MIN / -1 (and
INT_MIN % -1) — they do not wrap.
Float + - * / is IEEE-754: 1.0e308 * 1.0e308 is Inf, with no panic.
Mixing Integer Types
A conversion is implicit exactly when it cannot change the mathematical value.
| Left | Right | Result |
|---|---|---|
int8 |
int |
int (wider signed) |
uint8 |
int |
int (unsigned → strictly wider signed) |
uint8 |
uint32 |
uint32 (wider unsigned) |
int |
uint32 |
error (same width, different signedness) |
int64 |
uint64 |
error |
size_t |
int |
error (size_t ≡ uint64) |
int |
float |
float (every int is exact in IEEE-64) |
int |
float32 |
error (not every int is exact in IEEE-32) |
int64 |
float |
error (not every int64 is exact in IEEE-64) |
Same-width signed/unsigned has no value-preserving answer, so it is a
type error in arithmetic and in comparisons. Write the as that says
which answer you want:
proc main()
let x: int = -1
let y: uint32 = 0
// let bad = x + y // type error: same-width signed/unsigned
// if x < y ... end if // type error, for the same reason
let signed_sum: int = x + (y as int) // -1
let unsigned_sum: uint32 = (x as uint32) + y // 4294967295
println("${signed_sum} ${unsigned_sum}")
end main
proc main()
let u: uint8 = 5
let i: int = 7
let s = u + i // int, 12 — unsigned byte inhabits int
println("${s}")
end main
size_t is the same type as uint64 (two spellings). size_t + 1
adapts the literal; size_t + int is the same-width error above.
Unsuffixed integer literals are exempt, because a literal has no signedness
of its own. count + 1 works whether count is signed or unsigned, and the
result has count's type in either order: 1 + count is a uint32 when
count is. A negative literal is not exempt: -1 is unary minus applied to
a literal, so count + -1 on an unsigned count is rejected, and so is
count < -1.
char is unsigned 8-bit in this lattice and distinct from uint8:
char + char is char (wraps), char + int is int, and
char + uint8 is a type error. See Character Type.
Numeric conversions (as)
e as T for a numeric T wraps at T's width at run time. An
out-of-range constant is a compile error (300 as uint8 and
1.0e20 as int do not compile). Float → integer truncates toward zero
and panics on NaN, Inf, or a value outside T when the source is not
constant. Identity as is a no-op. unsafe does not narrow:
let b: uint8 = n or let c: char = n with n: int is a type error
in every context — write n as uint8 / n as char.
Comparison Operators
== // Equal
!= // Not equal
< // Less than
> // Greater than
<= // Less or equal
>= // Greater or equal
Returns: bool
== / != apply to any two matching types (value equality for
numbers, strings, structs, enums; reference identity for objects and
Active Objects). Ordering (<, >, <=, >=) is defined only
for numeric types (integers including char and size_t, and floats).
string < string is not lexicographic — it is a type error (BUG-219).
Comparing a signed integer with an unsigned one follows the same mixing
table as arithmetic; see Mixing Integer Types.
Example:
if x == 5
println("x is 5")
end if
if score > 90
println("Excellent!")
end if
Logical Operators
Symbolic form:
&& // Logical AND
|| // Logical OR
! // Logical NOT
Word form (equivalent):
and // Logical AND
or // Logical OR
not // Logical NOT
Example:
if (x > 0) && (x < 100)
println("x is in range")
end if
if (x > 0) and (x < 100) // Same as above
println("x is in range")
end if
if !(flag) // Negation
println("flag is false")
end if
Set Operators
When operands are sets:
+ // Union
* // Intersection
- // Difference
in // Membership test
Example:
let s1: setof[int] = {1, 2, 3}
let s2: setof[int] = {2, 3, 4}
let union = s1 + s2 // {1, 2, 3, 4}
let common = s1 * s2 // {2, 3}
let unique = s1 - s2 // {1}
if 2 in s1 // true
println("Found!")
end if
Unary Operators
- // Numeric negation
! // Logical NOT
~ // Bitwise NOT (complement; wraps at the operand's width)
Example:
let x = 5
let neg = -x // -5
let flag = true
let inverted = !flag // false
Bitwise Operators
& // Bitwise AND
| // Bitwise OR
^ // Bitwise XOR
~ // Bitwise NOT
<< // Left shift
>> // Right shift
Example:
let a = 0b1100 // 12
let b = 0b1010 // 10
let and_result = a & b // 0b1000 = 8
let or_result = a | b // 0b1110 = 14
let xor_result = a ^ b // 0b0110 = 6
let shifted = a << 2 // 0b110000 = 48
The shift result wraps at the left operand's type, so
(255u8 << 1) >> 1 is 127. The count must be in 0 .. W-1
(W = width of the left type): a constant out of range is a compile
error (1u8 << 9), a run-time count (including a negative) panics.
Counts are not masked to W bits. Signed >> is arithmetic. The left
operand is the result type — 1 << 40 is still int, not int64;
write 1i64 << 40.
Compound Assignment Operators
+= // Add and assign
-= // Subtract and assign
*= // Multiply and assign
/= // Divide and assign
%= // Modulo and assign
Example:
mut x = 10
x += 5 // x = 15
x -= 3 // x = 12
x *= 2 // x = 24
Operator Precedence
From highest to lowest:
- Postfix:
.,[],() - Unary:
-,!,~,not - Multiplicative:
*,/,% - Additive:
+,- - Set membership:
in - Bitwise shift:
<<,>> - Comparison:
<,>,<=,>= - Type operators:
is,as - Equality:
==,!= - Bitwise AND:
& - Bitwise XOR:
^ - Bitwise OR:
| - Logical AND:
&&,and - Logical OR:
||,or
Use parentheses for clarity when precedence is unclear.
7. Control Flow
If Statement
if condition
statements
elif other_condition
statements
else
statements
end if
Example:
fn classify(n: int): string
if n < 0
return "negative"
elif n == 0
return "zero"
else
return "positive"
end if
end classify
If Expression (with then)
When you need a conditional that returns a value (like a ternary operator), use then:
let result = if condition then value1 else value2 end if
Key difference:
- If statement: Control flow, executes statements, no value returned
- If expression: Returns a value, requires
thenkeyword, must haveelse
Examples:
// Assign based on condition
let max = if a > b then a else b end if
// Use inline in expressions
println(if ready then "Yes" else "No" end if)
// Select between two values
let sign = if x < 0 then -1 else 1 end if
// With function calls
let name = if str.is_empty(input) then "default" else input end if
Note: If expressions must always have an else branch since they must return a value in all cases.
Unless (Inverted If)
unless condition
statements
end unless
Equivalent to: if !(condition)
Example:
unless ready
println("Not ready yet")
end unless
// Equivalent to:
if !ready
println("Not ready yet")
end if
While Loops
while condition
statements
end while
Example:
mut i = 0
while i < 10
print_int(i)
println("")
i = i + 1
end while
For Loops
for variable in start to stop
statements
end for
Example:
for i in 0 to 9
print_int(i)
println("")
end for
Note: Uses to keyword. The end is exclusive (0 to 9 loops 0-8).
With step:
for i in 0 to 100 step 10
print_int(i) // 0, 10, 20, ..., 90
end for
Note: The start/stop/step bounds must be simple values (a literal or a variable) — a call expression (e.g. for i in 0 to get_len()) is a syntax error; bind the result to a local first (let len = get_len()) and use that.
For-Each Loops
// Iterate over array elements
for item in collection
print_int(item)
end for
// With index variable
for i, item in collection
print_int(i) // 0, 1, 2, ...
print_int(item)
end for
Infinite Loops
loop
statements
if exit_condition
break
end if
end loop
Example:
mut count = 0
loop
count = count + 1
print_int(count)
println("")
if count >= 5
break
end if
end loop
Do-While and Do-Until
do while condition
statements
end do
do until condition
statements
end do
Both forms guarantee the body executes at least once. do while repeats while the condition is true; do until repeats until the condition becomes true.
Example:
mut count = 0
do while count < 5
count = count + 1
end do
// count is now 5
mut n = 0
do until n == 3
n = n + 1
end do
// n is now 3
Break and Continue
break // Exit innermost loop
continue // Skip to next iteration
Example:
for i in 0 to 9
if i == 5
continue // Skip 5
end if
if i == 8
break // Stop at 8
end if
print_int(i)
println("")
end for
8. Functions and Procedures
Functions (with return value)
fn name(param: type): return_type
statements
return value
end name
Example:
fn square(x: int): int
return x * x
end square
proc main()
let result = square(5)
print_int(result) // 25
println("")
end main
Procedures (no return value)
proc name(param: type)
statements
end name
Example:
proc greet(name: string)
print("Hello, ")
print(name)
println("!")
end greet
proc main()
greet("Alice")
end main
Output:
Hello, Alice!
Parameters
Type annotations required for all parameters:
fn add(a: int, b: int): int
return a + b
end add
Multiple parameters:
fn calculate(x: int, y: int, z: int): int
return x + y * z
end calculate
Return Statements
Functions must return a value:
fn get_value(): int
return 42
end get_value
Procedures can use return for early exit:
proc process(value: int)
if value < 0
return // Early exit
end if
println("Processing...")
end process
Labeled Ends
Functions and procedures support labeled ends:
fn calculate(x: int): int
return x * 2
end calculate
Both end calculate and end fn are valid.
9. Structs and Records
Declaring Structs
type Point = struct
x: int
y: int
end Point
Alternative (Pascal/Oberon style):
type Point = record
x: int
y: int
end Point
Note: struct and record are synonyms.
Creating Struct Instances
let p = new Point()
Accessing Fields
let p = new Point()
p.x = 10
p.y = 20
print_int(p.x) // 10
Structs with Methods
type Rectangle = struct
width: int
height: int
fn area(): int
return self.width * self.height
end area
end Rectangle
proc main()
let rect = new Rectangle()
rect.width = 5
rect.height = 10
let a = rect.area()
print_int(a)
println("")
end main
Output:
50
10. Enums and Pattern Matching
Declaring Enums
Simple enums:
type Color = enum
Red
Green
Blue
end Color
Enums with data (Sum types):
type Option = enum
Some(int)
None
end Option
type Result = enum
Ok(string)
Err(string)
end Result
Using Enums
Constructors are generated automatically:
let color = Color_Red()
let value = Option_Some(42)
let none = Option_None()
Note: Zero-argument constructors still need call parentheses — Color_Red
without () is a Type Error ("'Color_Red' is a function, not a variable"),
not a value. This applies in match patterns too: a bare Color_Red
pattern is REJECTED with a dedicated type error ("Bare enum constructor
pattern 'Color_Red' is not allowed — write 'Color_Red()' to match the
variant, or rename the binding") — because the same bare form used to
parse as a variable-binding pattern (a catch-all), which the exhaustiveness
checker trusted even though the code generated for that arm still tested
the real tag, so a match that's missing a variant elsewhere could be
wrongly accepted as exhaustive (no compile error) and crash at runtime if
that missing variant was ever matched against. Always
write Color_Red() — as an expression, a pattern, or a set-literal
element — so the checker sees the real constructor pattern.
Pattern Matching
match expression
pattern =>
statements
end
end match
match is for enums and other values. Runtime dispatch on a class
hierarchy is typecase — see
is / as / typecase.
Patterns supported:
- Wildcard:
_ - Variable:
name - Integer literal:
42 - Boolean literal:
true,false - String literal:
"hello" - Constructor:
Option_Some(x) - Constructor without binding:
Option_None()
Example with exhaustiveness:
type Option = enum
Some(int)
None
end Option
fn unwrap_or(opt: Option, default: int): int
match opt
Option_Some(_) =>
println("Has value")
return 42 // Simplified
end
Option_None() =>
println("No value")
return default
end
end match
end unwrap_or
Exhaustiveness Checking
The compiler enforces exhaustiveness - all cases must be covered:
Compile error (missing case):
fn bad_match(b: bool): int
match b
true => return 1 end
// ERROR: Missing case for 'false'
end match
end bad_match
Error message:
Type Error: Non-exhaustive match statement. Missing patterns:
false
Hint: Add missing cases or use '_' wildcard
Fixed with wildcard:
fn safe_match(n: int): string
match n
0 => return "zero" end
1 => return "one" end
_ => return "other" end // Catch-all
end match
end safe_match
Nested Patterns
A constructor pattern can hold another constructor pattern, a literal, a
binder or _, to any depth. Arms are tried top to bottom, through the
nesting:
type Shape = enum
Circle(int)
Square(int)
end Shape
type Slot = enum
Full(Shape)
Empty
end Slot
fn describe(s: Slot): string
match s
Slot_Full(Shape_Circle(0)) => return "dot" end
Slot_Full(Shape_Circle(r)) => return "circle ${r}" end
Slot_Full(other) => return "some other shape" end
Slot_Empty() => return "empty" end
end match
end describe
proc main()
println(describe(Slot_Full(Shape_Circle(0))))
println(describe(Slot_Full(Shape_Circle(3))))
println(describe(Slot_Full(Shape_Square(2))))
end main
Exhaustiveness is checked through the nesting too. Nested patterns work in
match statements; a match expression (let x = match ...) or a match
that also uses a when guard is rejected with a message saying so.
Match Guards (when clause) ✅ NEW
Match guards add conditional logic to patterns using the when keyword. The guard is evaluated after the pattern matches, and the arm only executes if the guard is true.
Syntax:
match expression
pattern when condition =>
statements
end
end match
Example - Range checking:
fn categorize(n: int): string
match n
_ when n < 0 =>
return "negative"
end
_ when n == 0 =>
return "zero"
end
_ when n > 100 =>
return "large"
end
_ =>
return "small positive"
end
end match
end categorize
Multiple conditions in guard:
fn in_range(n: int): bool
match n
_ when n >= 1 and n <= 10 =>
return true
end
_ =>
return false
end
end match
end in_range
Guards with external variables:
fn check_threshold(value: int, threshold: int): string
match value
_ when value > threshold =>
return "above"
end
_ when value == threshold =>
return "equal"
end
_ =>
return "below"
end
end match
end check_threshold
Note: Guards can reference both external variables (declared outside the match) and the current arm's pattern-bound variable, e.g. x when x != "" => .... The pattern binding is assigned before the guard runs, so it is safe to read there and in the arm body.
11. Arrays
Array Types
type IntArray = [int]
type StringArray = [string]
type BoolArray = [bool]
Array Literals
let numbers = [1, 2, 3, 4, 5]
let names = ["Alice", "Bob", "Charlie"]
let flags = [true, false, true]
Type inference: Element type inferred from first element.
Array Indexing
Reading:
let first = numbers[0]
let third = numbers[2]
Writing:
let arr = [1, 2, 3]
arr[0] = 100
arr[2] = 300
Complete Array Example
proc array_demo()
let arr = [1, 2, 3, 4, 5]
println("Original array:")
print_int(arr[0])
println("")
arr[0] = 100
println("After modification:")
print_int(arr[0])
println("")
end array_demo
Output:
Original array:
1
After modification:
100
Array Methods
Arrays have built-in methods that return new arrays (immutable style). The original array is not modified; reassign to update:
mut arr = [10, 20, 30]
// append(item) - returns new array with item added at end
arr = arr.append(40) // [10, 20, 30, 40]
// pop() - returns new array without last element
arr = arr.pop() // [10, 20, 30]
// remove(index) - returns new array without element at given index
arr = arr.remove(0) // [20, 30]
| Method | Description | Returns |
|---|---|---|
arr.append(item) |
Add item to end | New array with item appended |
arr.pop() |
Remove last element | New array without last element |
arr.remove(index) |
Remove element at index | New array without that element |
Note: these operations return a new array rather than mutating in place.
After reassignment the old array becomes unreachable and is reclaimed by the
collector eventually — but not at the point of reassignment. Reef sweeps lazily:
a collection marks live objects, and memory is reclaimed incrementally
afterwards, inside the allocator, when a later allocation needs a block. Building
an array by repeated push in a hot loop therefore produces garbage that is
reclaimed on someone else's schedule, not yours.
Multi-dimensional Arrays
type Matrix = [[int]]
let matrix = [[1, 2], [3, 4], [5, 6]]
let elem = matrix[0][1] // 2
12. Sets
Set Type Declaration
type IntSet = setof[int]
type CharSet = setof[char]
type Color = enum
Red
Green
Blue
end Color
type ColorSet = setof[Color] // Set of enum values
Set Literals
Element enumeration:
let s: setof[int] = {1, 2, 3, 4}
Range notation:
let digits: setof[int] = {0..9}
Mixed:
let mixed: setof[int] = {0, 2, 5..10, 15}
Note: Empty set literals {} are not allowed (cannot infer type). Provide at least one element.
Set Operations
let s1: setof[int] = {1, 2, 3}
let s2: setof[int] = {2, 3, 4}
// Union - all elements in either set
let union = s1 + s2 // {1, 2, 3, 4}
// Intersection - elements in both sets
let common = s1 * s2 // {2, 3}
// Difference - elements in s1 but not s2
let unique = s1 - s2 // {1}
Membership Test
if element in set_value
println("Element is in set")
end if
Example:
let primes: setof[int] = {2, 3, 5, 7, 11}
if 7 in primes
println("7 is prime")
end if
if !(4 in primes)
println("4 is not prime")
end if
Output:
7 is prime
4 is not prime
Practical Set Example
type Permission = enum
Read
Write
Execute
end Permission
proc check_permissions()
let admin_perms: setof[Permission] = {Read, Write, Execute}
let user_perms: setof[Permission] = {Read, Write}
if Execute in admin_perms
println("Admin can execute")
end if
if !(Execute in user_perms)
println("User cannot execute")
end if
end check_permissions
Implementation Notes:
- Sets are value types (not heap-allocated)
- Implemented as bitsets (uint32_t)
- Efficient for small sets (0-31 elements)
- Suitable for flags, permissions, options
13. Subranges (removed in 0.10)
Subrange types are gone in 0.10 (numeric-core N10). These spellings are now parse errors:
type Digit = subrange 0 9let d: 0..9 = 3
Use a fixed-width integer, or a type alias to one:
type Digit = int
type Port = int
proc main()
let d: Digit = 5
let p: Port = 8080
println("ok")
end main
subrange remains a reserved word (it cannot be an identifier). The
.. token remains for set-range literals ({0..9}), slices (a[1..2]),
and for-loop ranges — only the type forms were removed.
Ada-style constrained integers may return later as a separate feature; they are not a 0.10 goal.
Array and string indices accept the whole integer family, so an int64
index needs no conversion:
proc main()
let arr = [10, 20, 30]
let i: int64 = 1i64
print_int(arr[i])
println("")
end main
This covers the core language: lexical structure, types, variables, operators, control flow, functions, structs, enums, arrays, and sets. For Active Objects, generics, modules, and FFI, see the chapters linked in the table of contents above.
Previous: 010_PROJECT_STRUCTURE.md Next: 020_FUNCTIONS.md Index: 000_INDEX.md