Language Basics

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


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

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, subranges

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, subrange, byte, impl, is, trait, where

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

Sized integers:

let small: int8 = 127
let medium: int16 = 32000
let standard: int32 = 1000000
let large: int64 = 9223372036854775807

let byte: uint8 = 255
let word: uint16 = 65535
let dword: uint32 = 4000000000
let qword: uint64 = 18446744073709551615

Integer literal suffixes:

let a = 127i8        // int8
let b = 255u8        // uint8
let c = 1000i16      // int16
let d = 65535u16     // uint16
let e = 1000000i32   // int32
let f = 1000000u32   // uint32
let g = 123456i64    // int64
let h = 999999u64    // uint64

Floating-Point Types

let pi: float = 3.14159      // 64-bit (default)
let small = 2.5 as float32   // 32-bit (float literals are `float`; cast to narrow)
let precise = 1.0 as float64 // 64-bit (float literals are `float`; cast to widen)

Character Type

let letter: char = 'A'

Note: Characters are Unicode code points (32-bit).

String Type

let message: string = "Hello, Reef!"
let path: string = r"C:\Users\name"  // Raw string (no escapes)

Escape sequences:

  • \n - Newline
  • \t - Tab
  • \r - Carriage return
  • \\ - Backslash
  • \" - Quote

System Types

size_t and pointer exist to describe values that cross the C boundary, and that is where their values come from — an int literal is not assignable to a size_t binding, and 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 size: size_t = strlen("hello")   // C size_t type
    unsafe
        let ptr: pointer = malloc(size)  // C void* equivalent
    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 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

Subrange types - Restrict values to specific ranges:

type Digit = subrange 0 9
type Percentage = subrange 0 100
type Port = subrange 1 65535

Alternative syntax:

type DayOfMonth = 1..31      // Using .. notation

Usage:

type Digit = subrange 0 9
type Percentage = subrange 0 100

let age: Digit = 5
let score: Percentage = 75

Type compatibility: Subranges are compatible with int:

type Digit = subrange 0 9

proc test_subrange()
    let d: Digit = 5           // int literal works
    println("Subrange type works!")
end test_subrange

Output:

Subrange type works!

Implementation: Subranges are stored as int32_t with type compatibility checking.


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
**  // Power/exponentiation

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

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).

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)

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

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. Power: ** (right associative)
  4. Multiplicative: *, /, %
  5. Additive: +, -
  6. Set membership: in
  7. Bitwise shift: <<, >>
  8. Comparison: <, >, <=, >=
  9. Type operators: is, as
  10. Equality: ==, !=
  11. Bitwise AND: &
  12. Bitwise XOR: ^
  13. Bitwise OR: |
  14. Logical AND: &&, and
  15. 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

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 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

Subrange Type Declaration

Keyword syntax:

type Digit = subrange 0 9
type Percentage = subrange 0 100

Range syntax (alternative):

type DayOfMonth = 1..31
type Hour = 0..23

Using Subrange Types

type Digit = subrange 0 9

proc test_digit()
    let d: Digit = 5           // OK - within range
    print_int(d)
    println("")
end test_digit

Type Compatibility

Subranges are compatible with their base integer type:

type Port = subrange 1 65535

proc connect(port: Port)
    println("Connecting...")
end connect

proc main()
    let p: Port = 8080
    connect(p)                // Works - Port is compatible with int
end main

Use Cases

Bounded values:

type Index = subrange 0 9

proc main()
    let arr = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
    let idx: Index = 5

    // A subrange value cannot index an array directly — bind it to an int first.
    let i: int = idx
    print_int(arr[i])
    println("")
end main

A subrange type cannot be used as an array index directly. arr[idx], where idx is subrange-typed, is rejected:

Type Error: Array index must be int, got 'Index'

There is also no cast around it — idx as int is rejected in turn, because casts require a heap-allocated target type. Assign the subrange value to an int binding, as above.

Array and string indices otherwise 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

Domain constraints:

type Age = subrange 0 150
type Score = subrange 0 100
type Month = subrange 1 12

Implementation: Stored as int32_t with type compatibility checking.


This covers the core language: lexical structure, types, variables, operators, control flow, functions, structs, enums, arrays, sets, and subranges. 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