Language Basics

Part of: Reef Language Reference Last reviewed on version: 0.10


Table of Contents

  1. Introduction
  2. Getting Started
  3. Lexical Structure
  4. Types
  5. Variables
  6. Operators
  7. Control Flow
  8. Functions and Procedures
  9. Structs and Records
  10. Enums and Pattern Matching
  11. Arrays
  12. Sets
  13. 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 unsafe blocks 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 main entry point. Use proc main() for simple programs, or fn main(): int when 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 0
  • fn main(): int returns whatever you specify
  • exit(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:

  1. Postfix: ., [], ()
  2. Unary: -, !, ~, not
  3. Multiplicative: *, /, %
  4. Additive: +, -
  5. Set membership: in
  6. Bitwise shift: <<, >>
  7. Comparison: <, >, <=, >=
  8. Type operators: is, as
  9. Equality: ==, !=
  10. Bitwise AND: &
  11. Bitwise XOR: ^
  12. Bitwise OR: |
  13. Logical AND: &&, and
  14. 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 then keyword, must have else

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 9
  • let 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