User-Defined Types

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


Overview

Reef provides several ways to define custom types:

Type Purpose Example
Struct/Record Group related fields Point { x, y }
Enum Sum types with variants Option { Some, None }
Array Indexed collection [1, 2, 3]
Set Unique value collection {1, 2, 3}
Subrange Constrained integers 0..23
Type Alias Alternative name type Age = int
Passive object Class, single inheritance object Button extends Widget040_OBJECTS.md

Structs and Records

Structs group related data into a single type.

Declaration

type Point = struct
    x: float
    y: float
end Point

// Alternative syntax (Pascal/Oberon style)
type Person = record
    name: string
    age: int
end Person

struct and record are interchangeable - use whichever you prefer.

Creating Instances

With new (heap allocated):

let p = new Point()
p.x = 10.0
p.y = 20.0

With struct literal syntax:

let p = Point { x: 10.0, y: 20.0 }

Field Access

let p = Point { x: 3.0, y: 4.0 }

// Read fields
let x_val = p.x
let y_val = p.y

// Write fields (if mutable)
mut q = Point { x: 0.0, y: 0.0 }
q.x = 5.0
q.y = 12.0

Methods

Structs can have methods defined inside them:

type Point = struct
    x: float
    y: float

    // Method - uses self to access fields
    fn distance(): float
        return (self.x ** 2 + self.y ** 2) ** 0.5
    end distance

    // Another method
    fn add(other: Point): Point
        return Point { x: self.x + other.x, y: self.y + other.y }
    end add

    // Procedure (no return value)
    proc print()
        print("Point(")
        print_float(self.x)
        print(", ")
        print_float(self.y)
        println(")")
    end print
end Point

// Usage
let p = Point { x: 3.0, y: 4.0 }
let dist = p.distance()        // 5.0
p.print()                       // Point(3.0, 4.0)

let q = Point { x: 1.0, y: 1.0 }
let sum = p.add(q)              // Point { x: 4.0, y: 5.0 }

Struct Literal Validation

The compiler validates struct literals:

type Config = struct
    host: string
    port: int
    timeout: int
end Config

// ERROR: Missing field 'timeout'
let c = Config { host: "localhost", port: 8080 }

// ERROR: Unknown field 'debug'
let c = Config { host: "localhost", port: 8080, timeout: 30, debug: true }

// ERROR: Duplicate field 'port'
let c = Config { host: "localhost", port: 8080, port: 9090, timeout: 30 }

// CORRECT: All fields provided once
let c = Config { host: "localhost", port: 8080, timeout: 30 }

Enums (Sum Types)

Enums define types with multiple variants. Each value is exactly one variant.

Simple Enums

type Color = enum
    Red
    Green
    Blue
end Color

type Direction = enum
    North
    South
    East
    West
end Direction

Enums with Data

Variants can carry data:

type Option = enum
    Some(int)
    None
end Option

type Result = enum
    Ok(string)
    Err(string)
end Result

type Shape = enum
    Circle(float)           // radius
    Rectangle(float, float) // width, height
    Point                   // no data
end Shape

Constructors

Constructors are prefixed with the type name:

// Simple enum
let color = Color_Red()
let dir = Direction_North()

// Enum with data
let some_value = Option_Some(42)
let no_value = Option_None()

let ok = Result_Ok("success")
let err = Result_Err("something went wrong")

let circle = Shape_Circle(5.0)
let rect = Shape_Rectangle(10.0, 20.0)
let point = Shape_Point()

Note: Zero-argument constructors (Color_Red, Shape_Point, ...) still need call parentheses — the bare name alone is a Type Error ("'Color_Red' is a function, not a variable"). 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 generated code for that arm still tested the real tag; a match missing a variant elsewhere could be wrongly accepted as exhaustive and crash at runtime if that missing variant was ever matched against (fixed as Always write Color_Red() so the checker sees the real constructor pattern.

Pattern Matching with Enums

The primary way to use enums is with match:

fn describe_color(c: Color): string
    match c
        Color_Red() => return "It's red" end
        Color_Green() => return "It's green" end
        Color_Blue() => return "It's blue" end
    end match
end describe_color

fn get_value(opt: Option): int
    match opt
        Option_Some(x) =>
            return x
        end
        Option_None() =>
            return 0
        end
    end match
    return 0
end get_value

fn area(shape: Shape): float
    match shape
        Shape_Circle(r) =>
            return 3.14159 * r * r
        end
        Shape_Rectangle(w, h) =>
            return w * h
        end
        Shape_Point() =>
            return 0.0
        end
    end match
    return 0.0
end area

Generic Enums

Enums can be parameterized:

type Option[T] = enum
    Some(T)
    None
end Option

type Result[T, E] = enum
    Ok(T)
    Err(E)
end Result

// Usage
proc main()
    let int_opt: Option[int] = Option_Some(42)
    let str_opt: Option[string] = Option_Some("hello")
end main

An enum value that carries a GC-managed payload (a string, array, struct, active object, or nested payload enum) must not be a module-level let/mut/const — the compiler rejects that. Payload-free and all-scalar enums are allowed as module globals.

Heap-embedded payloads (struct / Active Object / object fields, array elements, closure captures) are traced as of 0.9. The remaining restriction is the module-global case only.


Pattern Matching

Pattern matching is Reef's way to destructure and inspect values.

Match Statement

match value
    pattern1 =>
        // code for pattern1
    end
    pattern2 =>
        // code for pattern2
    end
end match

Match Expression

Match can also be an expression that returns a value:

let result = match x
    1 => 100 end
    2 => 200 end
    _ => 0 end
end match

Pattern Types

Wildcard - matches anything:

match value
    _ =>
        println("matches anything")
    end
end match

Variable - binds the value to a name:

match value
    x =>
        println("value is ${x}")
    end
end match

Literal - matches specific values:

match number
    0 => println("zero") end
    1 => println("one") end
    42 => println("the answer") end
    _ => println("something else") end
end match

match text
    "yes" => true end
    "no" => false end
    _ => false end
end match

match flag
    true => println("on") end
    false => println("off") end
end match

Constructor - matches enum variants:

match option
    Option_Some(value) =>
        println("got ${value}")
    end
    Option_None() =>
        println("nothing")
    end
end match

Exhaustiveness Checking

The compiler ensures all cases are covered. This example is missing Closed and is rejected:

type Status = enum
    Active
    Pending
    Closed
end Status

fn describe(s: Status): string
    match s
        Status_Active() => return "active" end
        Status_Pending() => return "pending" end
    end match
end describe

Error message:

Type Error: Non-exhaustive match statement. Missing patterns:
  Status_Closed

Hint: Add missing cases or use '_' wildcard

CORRECT: All cases covered:

type Status = enum
    Active
    Pending
    Closed
end Status

fn describe(s: Status): string
    match s
        Status_Active() => return "active" end
        Status_Pending() => return "pending" end
        Status_Closed() => return "closed" end
    end match
end describe

// ALSO CORRECT: Using wildcard for remaining cases
fn is_active(s: Status): bool
    match s
        Status_Active() => return true end
        _ => return false end
    end match
end is_active

Arrays

Arrays are ordered, indexed collections of the same type.

Array Literals

let numbers = [1, 2, 3, 4, 5]
let names = ["Alice", "Bob", "Carol"]
let flags = [true, false, true]

// Empty array needs type annotation
let empty = new [int](0)   // an empty literal [] cannot infer T

Indexing

Arrays are zero-indexed:

let nums = [10, 20, 30, 40, 50]

let first = nums[0]   // 10
let third = nums[2]   // 30
let last = nums[4]    // 50

// Modify (if array is mutable)
mut arr = [1, 2, 3]
arr[1] = 99           // [1, 99, 3]

Dynamic Arrays

Allocate arrays at runtime with new:

let size = 100
let arr = new [int](size)    // Array of 100 ints

// Initialize
for i in 0 to size
    arr[i] = i * 2
end for

Iteration

let items = ["a", "b", "c"]

for i in 0 to 3
    println(items[i])
end for

Array Type Syntax

[int]           // Array of int
[string]        // Array of string
[Point]         // Array of Point structs
[[int]]         // Array of arrays of int (nested)

Array/String Slicing ✅ NEW

Slicing extracts a portion of an array or string using [start..end] syntax.

Syntax:

arr[start..end]   // Elements from start to end-1
arr[start..]      // Elements from start to the end
arr[..end]        // Elements from the beginning to end-1

String Slicing Examples:

proc main()
    let s = "Hello World"

    let hello = s[0..5]     // "Hello"
    let world = s[6..]      // "World"
    let hel = s[..3]        // "Hel"

    println(hello)   // Hello
    println(world)   // World
    println(hel)     // Hel
end main

Array Slicing Examples:

proc main()
    let nums = [10, 20, 30, 40, 50]

    let first_three = nums[0..3]    // [10, 20, 30]
    let last_two = nums[3..]        // [40, 50]
    let middle = nums[1..4]         // [20, 30, 40]
end main

Notes:

  • Indices are zero-based
  • The end index is exclusive (like Python)
  • Out-of-bounds access returns empty results (no runtime error)
  • Slices create a new copy of the data

Sets

Sets are collections of unique values with efficient membership testing.

Set Type

setof[int]      // Set of integers
setof[char]     // Set of characters
setof[Color]    // Set of enum values

Set Literals

type Color = enum
    Red
    Green
    Blue
end Color

let digits: setof[int] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
let vowels: setof[char] = {'a', 'e', 'i', 'o', 'u'}
let primary: setof[Color] = {Color_Red(), Color_Green(), Color_Blue()}

// Range notation
let teens: setof[int] = {13..19}

// Mixed (individual values and ranges)
let special: setof[int] = {0, 10, 20..25, 100}

Membership Test

let evens: setof[int] = {0, 2, 4, 6, 8}

if 4 in evens
    println("4 is even")
end if

if 5 in evens
    println("never prints")
end if

Set Operations

let a: setof[int] = {1, 2, 3}
let b: setof[int] = {2, 3, 4}

let union = a + b        // {1, 2, 3, 4}
let intersect = a * b    // {2, 3}
let diff = a - b         // {1}

Current Limitations

  • Sets are limited to 32 elements (implemented as bitsets)
  • Elements must be comparable (int, char, enum values)

Subranges

Subranges define constrained integer types.

Declaration

type Digit = subrange 0 9
type Hour = 0..23              // Alternative syntax
type Month = subrange 1 12
type Percent = subrange 0 100

Usage

type Digit = subrange 0 9
type Hour = 0..23
type Month = subrange 1 12

let d: Digit = 5       // OK
let h: Hour = 23       // OK
let m: Month = 6       // OK

// A subrange value is assignable to int
let x: int = d         // OK, d treated as int

Assignment compatibility is not arithmetic compatibility: d + 10 is currently rejected ("Arithmetic operators require numeric operands of matching types"). Widen first (let x: int = d) and do the arithmetic on the int.

With Sets

Subranges are often used with sets:

type Digit = subrange 0 9
let evens: setof[Digit] = {0, 2, 4, 6, 8}

let d: Digit = 4
if d in evens
    println("even digit")
end if

Bounds Checking

Subrange bounds are documentation today, not enforcement: the declared range is recorded in the type (and drives assignment compatibility with int), but neither a literal nor a variable is range-checked, at compile time or at run time.

type Digit = subrange 0 9

let ok: Digit = 5            // in range
let over: Digit = 15         // ALSO accepted today -- not range-checked

let x = 15
let unchecked: Digit = x     // accepted; no runtime check either

Enforcement (compile-time for literals, optional runtime checks for variables) is a future feature.


Type Aliases

Create alternative names for existing types:

type Age = int
type Name = string
type Coordinate = float
type UserID = int
type ErrorMessage = string

Aliases are purely for documentation - the underlying type is unchanged:

type Age = int
type Year = int

let age: Age = 25
let year: Year = 2025

// These are both just int, so this works
let sum = age + year   // 2050

Choosing the Right Type

Need Use
Group related fields struct
Multiple exclusive states enum
State with associated data enum with data
Ordered collection Array [T]
Unique values, membership test Set setof[T]
Constrained integer Subrange
Self-documenting code Type alias

Example: Modeling a Game

// Enum for discrete states
type GameState = enum
    Menu
    Playing
    Paused
    GameOver
end GameState

// Struct for grouped data
type Player = struct
    name: string
    x: int
    y: int
    health: int

    fn is_alive(): bool
        return self.health > 0
    end is_alive
end Player

// Subrange for bounded values
type Health = subrange 0 100

// Set for game options
type Difficulty = enum
    Easy
    Normal
    Hard
end Difficulty

let enabled_modes: setof[Difficulty] = {Difficulty_Easy(), Difficulty_Normal()}

Common Patterns

Option Pattern

Represent optional values with the canonical generic Option[T] from core.option. Use Option[T] when absence is not an error — a lookup that may find nothing:

import core.option

fn find(arr: [int], target: int): option.Option[int]
    for i in 0 to 10  // assuming max 10
        if arr[i] == target
            return Option_Some(i)
        end if
    end for
    return @option.Option[int].None()
end find

option.unwrap(opt) returns the value on Some and panics on None; use option.unwrap_or(opt, default) or a match when None is expected.

A real stdlib example: sys.env.get_env(name): option.Option[string] returns Some(value) if the environment variable is set, None if it is unset.

Result Pattern

Represent success or failure with the canonical generic Result[T, E] from core.result. The stdlib pairs E with the shared structured core.error.Error (a kind + message + code), so use Result[T, core.error.Error] when a failure carries a reason:

import core.result
import core.error

fn divide(a: int, b: int): result.Result[int, error.Error]
    if b == 0
        return @result.Result[int, error.Error].Err(
            error.error(error.ErrorKind_InvalidInput(), "division by zero"))
    end if
    return @result.Result[int, error.Error].Ok(a / b)
end divide

result.unwrap_ok(r) returns the value on Ok and panics on Err (unwrap_err is symmetric); use result.unwrap_or or a match to handle the error path. Construct variants with the @Type[args].Variant(...) form (a module-qualified @alias.Type[args].Variant(...) also works); the bare alias.Variant(...) shorthand (e.g. result.Result_Err(...)) does not reliably infer the full Result[T, E] signature in a return position — it type-checks the success-type parameter as unresolved (Result[?, Error]) — so the explicit @Type[args].Variant(...) form above is the safe idiom.

A real stdlib example: core.convert.toInt(s): result.Result[int, error.Error] returns Ok(n) on a parseable prefix, Err(...) on invalid input (e.g. an empty string).

State Machine

Model state transitions:

type ConnectionState = enum
    Disconnected
    Connecting
    Connected
    Error(string)
end ConnectionState

fn next_state(current: ConnectionState, event: string): ConnectionState
    match current
        ConnectionState_Disconnected() =>
            if event == "connect"
                return ConnectionState_Connecting()
            end if
            return current
        end
        ConnectionState_Connecting() =>
            if event == "success"
                return ConnectionState_Connected()
            elif event == "fail"
                return ConnectionState_Error("Connection failed")
            end if
            return current
        end
        ConnectionState_Connected() =>
            if event == "disconnect"
                return ConnectionState_Disconnected()
            end if
            return current
        end
        ConnectionState_Error(_) =>
            if event == "retry"
                return ConnectionState_Connecting()
            end if
            return current
        end
    end match
    return current
end next_state

Previous: 025_STRING_INTERPOLATION.md Next: 035_ERROR_HANDLING.md