Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Ore Programming Language

Ore is a statically-typed, compiled systems language with algebraic effects and automatic memory management via Perceus reference counting.

Design Principles

  • Explicit memory. Three pointer types: [*]T (heap, RC-managed), []T (slice view), *T (raw pointer). No hidden allocations.
  • Two operators, clear roles. := binds and rebinds names. <- mutates memory at a location. Never confused.
  • Everything is name :: thing. Functions, data types, effects, constants — all defined with ::.
  • Public by default. Items are public unless marked pvt.
  • Effects are explicit. Side effects tracked in function signatures via effect rows.
  • No null. ?T for optional values, !T for error unions.
  • One data keyword. data replaces struct and enum — everything is a sum type.
  • UFCS. x.f() is f(x). No methods, no traits — just functions.
  • Operators are intrinsics. +, -, *, etc. are compiler built-ins.
  • Compile-time safety. Argus (escape analysis + borrow checking) prevents dangling references at compile time. No runtime checks, no lifetime annotations.

Syntax Overview

Ore uses indentation with optional curly braces (layout rule):

greet :: fn(name: []const u8)
    print(name)
greet :: fn(name: []const u8) { print(name) }

A Quick Example

Shape :: data
    Circle .{ f64 }
    Rect .{ f64, f64 }

area :: fn(s: Shape) -> f64
    match s
        Shape::Circle.{ r } { 3.14 * r * r }
        Shape::Rect.{ w, h } { w * h }

main :: fn() -> !i32
    0

Binding and Mutation

// :: is compile-time constant (rodata)
PI :: 3.14

// := binds a name to a value (ref cell)
x := 42
x := x + 1         // rebinds — updates the same cell

// <- mutates memory at a location
buf := alloc(u8, 10)
buf[0] <- 99        // writes to heap memory

// : T = is a typed binding
count : i32 = 0

Memory Model

TypeWhat it isManaged by
[*]THeap pointerPerceus (RC)
[]TSlice (view)Argus (scope)
*TRaw pointerArgus (scope)
[N; T]Fixed arrayStack

Perceus handles when to free heap memory. Argus ensures views don’t outlive their source. Both are compile-time — zero runtime overhead.

Multidimensional fixed arrays are written by nesting the element type: [rows; [cols; T]].

matrix : [2; [3; i32]] = .{
    .{ 1, 2, 3 },
    .{ 4, 5, 6 }
}

Effects

Console :: effect
    fn print(msg: []const u8)
    fn read_line() -> []u8

greet :: fn(name: []const u8) -> <Console> ()
    Console.print("Hello, ")
    Console.print(name)

Effects replace exceptions, IO, and mutable state with typed, composable handlers.

Literals & Primitive Types

Integers

Integer literals default to i32. Underscores can be used as visual separators.

42
0
9999999
1_000_000

Alternate bases:

0xff          // hexadecimal (case-insensitive: 0XAB, 0xDeAdBeEf)
0b1010        // binary (0B1100)
0o77          // octal (0O55)

Integer Types

TypeSizeRange
i88-bitsigned
i1616-bitsigned
i3232-bitsigned
i6464-bitsigned
isizepointersigned
u88-bitunsigned
u1616-bitunsigned
u3232-bitunsigned
u6464-bitunsigned
usizepointerunsigned

Floats

Float literals default to f32. Underscores allowed.

3.14159
0.5
100.0
1_000.5

Scientific notation:

1e10          // 10000000000.0
1.5e10        // 15000000000.0
2.5e-3        // 0.0025
3.0e+7        // 30000000.0

Float Types

TypeSize
f3232-bit
f6464-bit

Strings

Double-quoted. The type is []const u8 — an immutable slice of UTF-8 bytes.

"hello"
""                    // empty
"hello world"         // spaces

Escape sequences:

EscapeMeaning
\nnewline
\ttab
\rcarriage return
\"double quote
\\backslash
\0null byte
\'single quote

Triple-quoted strings for raw content:

"""triple quoted"""

Booleans

true
false

The type is bool.

Bytes

Single-quoted character literals produce a u8 value:

'a'           // 97
'z'
'0'
' '           // space

Escape sequences work in byte literals too:

'\n'          // newline byte
'\t'          // tab byte
'\\'          // backslash byte
'\''          // single quote byte
'\0'          // null byte

Variables & Bindings

Let Bindings

Variables are introduced with let. They are immutable by default.

let x = 5
let name = "Alice"

Optional type annotation:

let x: i64 = 5

Bindings can use any expression:

let x = 5 + 3
let y = x * 2
let result = compute(42)
let s = "hello".to_upper()

Var Bindings

Use var for mutable state. A var binding creates a mutable ref cell:

var count = 0
count = count + 1
count += 1

Optional type annotation:

var total: i64 = 0

Destructuring

Positional products:

let .{ a, b } = get_pair()
let .{ a, .{ b, c } } = get_nested()
let .{ _, b } = get_pair()          // wildcard discards first element

Named products and data values:

let Point.{ .x = x, .y = y } = get_point()
let Point.{ .x = px, .y = py } = get_point()   // rename fields
let Config.{ .name = name } = get_config()

Constants

Top-level compile-time values with required type annotations:

const MAX_SIZE: i64 = 1024
const PI: f64 = 3.14159
const GREETING: String = "hello"
const YES: bool = true
const BYTE_A: u8 = 'a'

Constants support all literal forms:

const HEX: i64 = 0xFF
const BIN: i64 = 0b1010
const OCT: i64 = 0o77
const SCI: f64 = 1.5e10
const TRIPLE: String = """triple quoted"""

Visibility and attributes:

pvt const SECRET: i64 = 42

#[deprecated]
const OLD_VALUE: i64 = 0

Functions

Basic Functions

Functions are declared with fn. The body is indented on the next line:

fn add(x: i64, y: i64) -> i64
  x + y

No return type means the function returns void:

fn greet(name: String)
  print(name)

Short functions can use braces for a single-line body:

fn double(x: i64) -> i64 { x * 2 }
fn apply(x: i64) -> i64 { compute(x) }

Generics

Lowercase type parameters are inferred from usage — no explicit <T> declaration needed on functions:

fn identity(x: t) -> t
  x

Multiple type parameters work the same way:

fn pair(left: t, right: u) -> .{ .left: t, .right: u }
  .{ .left = left, .right = right }

Effects

Functions that perform side effects declare them via effect rows in the return type:

fn read_file(path: String) -> <io> String
  // ...

fn complex(x: i64) -> <io, state> String
  // ...

Polymorphic effects use a tail variable:

fn map(xs: List<a>, f: fn(a) -> e b) -> e List<b>
  // ...

See Effects & Handlers for the full effect system.

Function Type Parameters

Functions can accept other functions as parameters using fn(Args) -> Ret type syntax:

fn apply(f: fn(i64) -> i64, x: i64) -> i64
  f(x)

Visibility and Attributes

Functions are public by default. Use pvt for private:

pvt fn helper() -> i64
  42

Attributes precede the function:

#[inline]
fn fast(x: i64) -> i64 { x * 2 }

Everything Combined

#[some_attr]
pvt fn kitchen_sink(x: t, y: u) -> <io, state> Result<t, u> where t: Display + Clone, u: Default
  // ...

Lambdas

Anonymous functions use pipe syntax:

let f = || x + 1
let add = |x, y| x + y
let constant = || 42

Lambdas are commonly used with higher-order functions:

items.map(|x| x * 2)
items.filter(|x| x > 0)
items.fold(0, |acc, x| acc + x)

Closure Capture

Closures capture variables from their enclosing scope by copy. The closure gets its own copy of the value at the point of creation:

var x = 10
let f = || x + 1    // f captures a copy of x (10)
x = 99              // x is now 99, but f's copy is still 10
f()                  // returns 11, not 100

This means closures are self-contained — they don’t hold references to the original variable. Mutating the variable after creating the closure has no effect on the closure’s captured values, and vice versa.

Handler operations are different. Handler ops capture var bindings by reference, because they need live access to shared mutable state:

var cell = 0
with handler<Counter>
  ctl get()
    resume(cell)     // reads the live value of cell
  ctl set(x)
    cell = x         // mutates the original cell
    resume(())

This is safe because handlers are scoped constructs — they always run to end of scope and cannot escape. The compiler’s scope coloring analysis verifies this.

ConstructCapture modeCan escape?
Closures (|| ...)Copy (value)Yes — safe, owns its data
Handler ops (ctl ...)By referenceNo — scoped to with block

Arity-Based Dispatch

Multiple functions can share a name if they have different parameter counts:

fn add(a: i32, b: i32) -> i32 { a + b }
fn add(a: i32, b: i32, c: i32) -> i32 { a + b + c }

add(1, 2)      // calls add/2
add(1, 2, 3)   // calls add/3

The compiler resolves which variant to call by counting arguments. When passed as a value, the expected type disambiguates. See Arity & Config Structs for the full details.

Calling Functions

Standard call syntax:

add(5, 3)
f(g(x))
f(x + y, y * 2)

Method calls on values:

x.len()
x.concat(y)
x.trim().len()      // chaining

Path-qualified calls:

Foo::new()
std::io::File::open("test.txt")

Arity-Based Dispatch

Ore supports multiple functions with the same name but different parameter counts (arities). The compiler resolves which function to call based on the number of arguments at the call site.

Basic Arity Dispatch

fn add(a: i32, b: i32) -> i32
  a + b

fn add(a: i32, b: i32, c: i32) -> i32
  a + b + c

fn main() -> !i32
  add(1, 2)           // calls add/2 → 3
  add(1, 2, 3)        // calls add/3 → 6
  add(1, 2) + add(1, 2, 3)  // 9

Each arity variant is a completely separate function with its own DefId, type signature, and compiled C name. add/2 compiles to ore_add_2, add/3 compiles to ore_add_3.

Rules

  • Same name + same arity = compile error (duplicate definition)
  • Same name + different arity = allowed (arity dispatch)
  • Non-function definitions (structs, consts, variants) cannot share names

Higher-Order Functions

When passing a function as a value, the compiler infers which arity variant to use from the expected type:

fn apply(f: fn(i32, i32) -> i32, x: i32, y: i32) -> i32
  f(x, y)

fn add(a: i32, b: i32) -> i32 { a + b }
fn add(a: i32, b: i32, c: i32) -> i32 { a + b + c }

fn main() -> !i32
  apply(add, 20, 22)  // apply expects fn(i32, i32) -> i32
                       // only add/2 matches → resolved

If there is only one function with a given name, no disambiguation is needed regardless of context:

fn double(x: i32) -> i32 { x * 2 }

items.map(double)   // only one `double` → no ambiguity

If the compiler cannot determine which arity to use (rare — requires a fully generic parameter with no type constraints), it emits an error:

error: ambiguous function reference: multiple arities available, use in a call to disambiguate

Config Data (Alternative to Optional Parameters)

Ore uses fixed arity. When a function needs several related options, group them into a single data value instead of adding optional parameters.

ServerConfig :: data .{ .port: i32, .host: []const u8, .debug: i32 }

fn start_server(config: ServerConfig) -> i32
  config.port

Aggregate Arguments

For nominal config data, construct the argument explicitly with a headed aggregate:

start_server(ServerConfig.{ .port = 3000, .host = "localhost", .debug = 0 })

If the parameter itself is structural, the expected type can drive an unheaded aggregate:

fn start_server(config: .{ .port: i32, .debug: i32 }) -> i32
  config.port

start_server(.{ .port = 9000, .debug = 1 })

Name Mangling

All non-extern functions include arity in their C name: ore_{name}_{arity}. This ensures distinct C symbols even for same-name functions:

OreC
fn add(a, b)ore_add_2
fn add(a, b, c)ore_add_3
fn main()ore_main_0
extern fn malloc(n)malloc (unchanged)

Extern functions keep their original C names for FFI compatibility.

Operators

Arithmetic

OperatorMeaningExample
+additionx + y
-subtractionx - y
*multiplicationx * y
/divisionx / y
%modulox % y
**exponentiationx ** y

Comparison

OperatorMeaningExample
==equalx == y
!=not equalx != y
<less thanx < y
<=less than or equalx <= y
>greater thanx > y
>=greater than or equalx >= y

Logical

OperatorMeaningExample
andlogical AND (short-circuit)x and y
orlogical OR (short-circuit)x or y
notlogical NOTnot x

and and or are keywords, not symbols. They short-circuit: the right operand is only evaluated if needed.

Bitwise

OperatorMeaningExample
&bitwise ANDx & y
|bitwise ORx | y
^bitwise XORx ^ y
~bitwise NOT~x
<<left shiftx << y
>>right shiftx >> y

Other

OperatorMeaningExample
<>appendxs <> ys

Unary

OperatorMeaningExample
-negation-x
notlogical NOTnot x
~bitwise NOT~x
&address-of (linear)&x
*dereference (linear)*p

Precedence

From highest to lowest binding power:

PrecedenceOperatorsAssociativity
25. () ?left (postfix)
23- not ~ & * (unary)prefix
21**right
19* / %left
17+ - <>left
15<< >>left
13&left
11^left
9|left
7== != < <= > >=left
5andleft
3orleft
1= += -= *= /= %=right

Parentheses override precedence:

x + y * z       // y * z first
(x + y) * z     // x + y first
-a + b ** 2     // (-a) + (b ** 2)

Compound Assignment

Available to var only

OperatorDesugars to
x += yx = x + y
x -= yx = x - y
x *= yx = x * y
x /= yx = x / y
x %= yx = x % y

Try Operator

The postfix ? operator propagates errors from error union values:

fn read_config(path: String) -> !Config
  let contents = read_file(path)?     // returns error early if read fails
  let config = parse(contents)?
  config

Types

Data Types

All user-defined types use the data keyword. data is a sum type — a single-variant data acts like a struct, a multi-variant data acts like an enum. The compiler optimizes single-variant types (no tag needed).

Single-Variant (Struct)

Single-variant data is constructed with a headed aggregate literal:

Point :: data .{ .x: i32, .y: i32 }

p := Point.{ .x = 1, .y = 2 }
p.x   // 1
p.y   // 2

Multi-Variant (Enum)

Indented variants:

Shape :: data
  Square .{ f32 }
  Circle .{ f32 }

area :: fn(s: Shape) -> f32
  match s
    Shape::Square.{ side } => side * side
    Shape::Circle.{ r } => 3.14 * r * r

Simple Enums (No Fields)

Pipe syntax for variants without fields:

Color :: data
  Red
  Green
  Blue

Or indented:

Color :: data
  Red
  Green
  Blue

Variant Field Shapes

Each variant independently chooses its field shape:

// No fields
Unit :: data

// Positional product fields
Pair :: data .{ i32, i32 }

// Named fields
Person :: data .{ .name: []const u8, .age: i32 }

// Mixed per variant
Expr :: data
  Lit .{ i32 }
  Add .{ .lhs: i32, .rhs: i32 }
  Nil

Explicit Variant Name

For single-variant types, you can write the variant name explicitly. If the variant name matches the type name, it’s treated as a single-variant type:

Person :: data
  Person .{ .name: []const u8, .age: i32 }

// Equivalent to:
Person :: data .{ .name: []const u8, .age: i32 }

UFCS (Uniform Function Call Syntax)

Any free function whose first parameter matches a type can be called with dot syntax:

Person :: data .{ .name: i32, .age: i32 }

greet :: fn(p: Person) -> i32
  p.age + 1

p := Person.{ .name = 10, .age = 30 }
p.greet()    // calls greet(p)

This works on any type, including primitives:

double :: fn(x: i32) -> i32
  x * 2

5.double()   // 10

Type Aliases

type Id = i32
type Callback = fn(i32) -> i32

Aliases are transparent — the compiler treats them as the underlying type.

Error Sets

data FileError = error
  FileNotFound
  ReadFailed

Error sets define named error codes used with error unions (!T).

Error Unions

A function that can fail returns !T (error union):

fn read_file(path: []const u8) -> !i32
  let fd = open(path.ptr, 0)
  return error.FileNotFound if fd < 0
  // ...
  size

The caller uses try to propagate errors:

let size = try read_file("test.txt")

Or catch to handle them:

let size = read_file("test.txt") catch e
  0  // default on error

Optionals

?T is an optional type — either a value of type T or nil:

fn find(items: []i32, target: i32) -> ?i32
  // ...
  return nil  // not found

Use orelse to provide a default:

let x = find(items, 42) orelse -1

Primitive Types

See Literals & Primitive Types for the full list of built-in types: i8, i16, i32, i64, u8, u16, u32, u64, f32, f64, bool, isize, usize.

Pointer Types

See Pointers for *T, *const T, [*]T, and [*]const T.

Slice Types

See Slices & Strings for []T and []const T.

Products

Ore has one structural product surface with two shapes: positional products and named products. They are distinct types and do not unify with each other.

Positional Products

Positional products use .{ ... } with numeric field access.

t := .{ 10, 20, true }
t.0
t.1
t.2

Type annotation: .{ i32, i32, bool }.

Parentheses are only for grouping. Unit remains ().

(42)        // grouping
.{ 42 }     // 1-element positional product
()          // unit

Named Structural Products

Named products are structural values with labeled fields.

point := .{ .x = 10, .y = 20 }
point.x
point.y

Their type syntax uses the same field set with : instead of =:

.{ .x: i32, .y: i32 }

Type Annotation

alloc :: fn(n: i32) -> .{ .ptr: [*]u8, .len: usize }
  .{ .ptr = ore_alloc(n), .len = n as usize }

Field Access

Named products are accessed only by field name.

buf := alloc(100)
buf.ptr
buf.len

As Function Parameters

area :: fn(rect: .{ .w: i32, .h: i32 }) -> i32
  rect.w * rect.h

area(.{ .w = 10, .h = 5 })

Key Rules

Field names are part of identity

.{ .x: i32, .y: i32 } and .{ .a: i32, .b: i32 } are different types.

Named and positional products are distinct

.{ .x: i32, .y: i32 } does not unify with .{ i32, i32 }.

Field access matches the product family

Positional products use .0, .1, and so on. Named products use .field.

.{ ... } is structural by default

Without an explicit head, .{ ... } builds a structural product. Use a head when you want a nominal data value or an array.

Point.{ .x = 10, .y = 20 }
[3; i32].{ 1, 2, 3 }

Comparison with data

FeaturePositional ProductNamed Productdata
IdentityStructural by positionStructural by field namesNominal
Access.0, .1.name.name
Type syntax.{ A, B }.{ .x: A, .y: B }Name
Literal syntax.{ 1, 2 }.{ .x = 1, .y = 2 }Point.{ .x = 1, .y = 2 }

Use named structural products for lightweight labeled data. Use data when the type’s name should matter across the program.

Library Convention

Functions that allocate often return a named product containing the owner and its size:

read_file :: fn(path: []const u8) -> .{ .data: [*]u8, .size: i32 }
file := read_file("input.txt")
view := file.data[0..file.size as usize]

Control Flow

If

Inline Conditional

if x > 0 then "positive" else "non-positive"

if condition then expr else expr — both branches required for inline form.

Multi-Arm Conditional

if followed by a block introduces guard-style arms with =>:

fn classify(x: i64) -> String
  if
    x > 0  => "positive"
    x < 0  => "negative"
    else   => "zero"

Each arm is condition => body. Use else => for the default case.

Arms can have multi-statement bodies:

fn process(x: i64) -> i64
  if
    x > 0 =>
      let doubled = x * 2
      doubled + 1
    else => 0

Postfix Conditional

expr if condition — executes expr only if condition is true:

return error.FileNotFound if fd < 0
break if i >= 10

This is the primary guard/early-exit pattern.

Match

Pattern matching with => arms:

fn describe(s: Shape) -> i32
  match s
    Shape::Square.{ side } => side * side
    Shape::Circle.{ r }    => r * r

Literal Patterns

match x
  0 => "zero"
  1 => "one"
  _ => "other"

Data Type Patterns

match result
  Shape::Square.{ s } => s
  Shape::Circle.{ r } => r

Or Patterns

Multiple patterns separated by |:

match x
  1 | 2 | 3 => "small"
  4 | 5 | 6 => "medium"
  _         => "large"

Guards

Add conditions with if after the pattern:

match opt
  Some.{ x } if x > 0 => "positive"
  Some.{ x } if x < 0 => "negative"
  Some.{ _ }          => "zero"
  _                   => "none"

Multi-Statement Arms

match x
  0 =>
    let result = compute()
    result + 1
  _ => x

For

for is the only loop construct. All forms support break to exit and return to exit the enclosing function.

Infinite Loop

Bare for loops forever. Use break or return to exit.

for
  event := poll()
  break if event == quit
  handle(event)

Condition (While-Style)

for cond loops while the condition is true.

mut i := 10
for i > 0
  i <- i - 1
// i is 0 here
mut sum := 0
mut i := 0
for i < 10
  sum <- sum + i
  i <- i + 1
// sum is 45

Numeric Range

for i in start..end iterates from start up to (but not including) end. The loop variable i is bound to the current value.

mut sum := 0
for i in 0..10
  sum <- sum + i
// sum is 45

Collection Iteration

for x in coll iterates over slices, buffers, and fixed arrays. The loop variable x is bound to each element.

for c in text
  process(c)

Collection with Index

for x, i in coll — element first, index second. x is the element type, i is usize.

for c, i in text
  if is_lower(c) then
    buf[i] <- rotate(c, shift)
  else
    buf[i] <- c

Nested fixed arrays compose naturally with loops and indexing.

matrix : [2; [3; i32]] = .{
  .{ 1, 2, 3 },
  .{ 4, 5, 6 }
}

sum : i32 = 0
for row in matrix
  for cell in row
    sum <- sum + cell

matrix[1][2] <- sum

Collection with Filter

for x in coll where cond — only visits elements where the condition is true. The condition can reference the loop variable.

for c in text where is_lower(c)
  idx := (c as i32) - 97
  counts[idx] <- counts[idx] + 1

Early Return from Loop

return exits the enclosing function, not just the loop:

find :: fn(buf: []u8, target: u8) -> i32
  for x, i in buf
    return i as i32 if x == target
  -1

Return

return exits the function early with a value:

fn clamp(x: i32, lo: i32, hi: i32) -> i32
  return lo if x < lo
  return hi if x > hi
  x

In error union functions, return error.X wraps the error:

fn read_file(path: []const u8) -> !i32
  let fd = open(path.ptr, 0)
  return error.FileNotFound if fd < 0
  // ...

Effects & Handlers

Ore uses algebraic effects for side effects, resource management, and control flow. The system is modeled after Koka and has three core constructs:

  • effect — declares an effect and its operations
  • handler — defines an implementation of an effect
  • with — installs a handler for a scope

Declaring Effects

An effect declares a set of operations that code can perform:

effect Console
  fn print(msg: String)
  fn read_line() -> String

Operation Kinds

There are three kinds of effect operations:

KindMeaningContinuation
fnTail-resumptive: always resumes exactly onceNo continuation captured (cheap)
ctlControl: may resume zero or many timesCaptures continuation
finalFinal: never resumesNo continuation (like exceptions)
effect Async<a>
  fn spawn(task: fn() -> a) -> a     // always resumes
  ctl yield() -> a                    // may suspend/resume
  ctl abort(reason: String) -> Never  // may not resume

effect Error
  final throw(msg: String) -> Never   // never resumes

fn is the cheapest — use it when the handler always returns a value and continues. ctl captures a continuation, which the handler can call to resume. final is an optimization hint: the compiler knows no continuation is needed.

Scoped Effects

A scoped effect creates a fresh scope token each time it is handled, preventing handlers from being used outside their scope:

scoped effect heap<s::S>
  ctl new_ref(init: a) -> ref<s, a>

The s::S annotation means s is a scope kind parameter — it’s not a regular type, but a token that ties references to their heap.

Named Effects

A named effect is an effect instance that can be passed as a first-class value. Named effects declare their containing scoped effect with in:

named effect ref<s::S, a::V> in heap<s>
  fn get() -> a
  fn set(value: a)

This says ref is an instance of an effect that lives inside a heap<s> scope. You can have multiple ref instances within the same heap.

Kind Annotations

Type parameters can have kind annotations using :::

KindMeaning
::VValue type (default)
::SScope token
::EEffect row
::HHeap/handler
scoped effect heap<s::S>          // s is a scope token
named effect ref<s::S, a::V>     // s is scope, a is a value type

Marker Effects

Effects with no operations:

effect Pure

Generics, Bounds, and Where Clauses

effect Transform<a: Clone>
  fn transform(value: a) -> a

effect Convert<a, b> where a: Clone, b: From<a>
  fn convert(value: a) -> b

Visibility and Attributes

pvt effect InternalEffect
  fn internal_op() -> i64

#[some_attr]
effect Documented
  fn describe() -> String

Everything Combined

#[some_attr]
pvt effect FullEffect<a, b: Default> where a: Clone
  fn required(value: a) -> b
  ctl fail(msg: String) -> Never
  final panic(msg: String) -> Never

Effect Rows

Effects appear in function signatures as effect rows — a list of effects the function may perform. Effect rows appear before the return type:

fn read_file(path: String) -> <io> String
  // performs the io effect

fn complex(x: i64) -> <io, state> String
  // performs io and state effects

Open Rows (Polymorphic Effects)

A row variable e makes effects polymorphic — the function can perform whatever additional effects the caller has:

fn map(xs: List<a>, f: fn(a) -> e b) -> e List<b>
  // ...

Here e is an effect variable — map performs whatever effects f performs. A bare e before the return type is shorthand for an open effect row.

Explicit row syntax with a tail:

fn heap(action: forall<s> fn() -> <heap<s>, div|e> a) -> <div|e> a
  // action performs heap<s> + div + whatever e is

The |e separates named effects from the row tail variable.

Forall

The forall quantifier introduces scoped type/effect variables, commonly used with scoped effects:

fn heap(action: forall<s> fn() -> <heap<s>, div|e> a) -> <div|e> a

The forall<s> ensures s is fresh for each call — references created inside can’t escape.

Handlers

A handler expression defines how to handle an effect’s operations.

Basic Handler

fn state_handler(init: i64)
  var state = init
  handler State
    fn get() { state }
    fn set(s) { state = s }

Operation parameters are just names — types come from the effect definition.

Named Handler

For named effects, use named handler:

fn with_ref(init: a, action: fn(ref<s, a>) -> e b) -> e b
  var state = init
  with r <- named handler
    fn get() { state }
    fn set(x) { state = x }
  action(r)

The r binding captures the handler instance as a first-class value.

Return Clause

Process the final value of the handled computation:

handler State
  fn get() { state }
  fn set(s) { state = s }
  return(val) { val }

Initially and Finally

Resource management without special destructor hooks:

fn file_handler(path: String)
  let fd = Os::open(path)
  handler File
    initially { print("opening") }
    finally { Os::close(fd) }
    fn read(buf, len) { Os::read(fd, buf, len) }
    fn write(data) { Os::write(fd, data) }

initially runs when the handler is installed. finally runs when the handled scope exits (whether normally or via a final operation).

Resume

In ctl handlers, resume(value) continues the captured continuation with a value:

fn example() -> i64
  with handler Ask
    ctl ask() { resume(42) }
  Ask::ask() + 1    // ask() resumes with 42, so this returns 43

Not calling resume short-circuits — the handler’s value becomes the result of the entire with block:

fn safe_div(x: i64, y: i64) -> i64
  with handler Fail
    ctl fail(msg) { -1 }        // no resume → with block returns -1
  if y == 0 { Fail::fail("division by zero") }
  x / y

resume can be called multiple times for effects like non-determinism:

fn example() -> List<i64>
  with handler Choose
    ctl choose()
      let a = resume(true)
      let b = resume(false)
      a ++ b
  let x = if Choose::choose() { 1 } else { 2 }
  [x]

fn operations auto-resume (no explicit resume needed). final operations never resume.

With Expressions

with installs a handler for a block of code.

Rest of Scope

with installs a handler that covers the rest of the enclosing scope. It does not return a value — it establishes a scope.

fn example() -> i64
  with state(0)
  State::set(10)
  State::get()

Handler Binding

Use <- to bind a named handler instance:

fn main()
  with heap
  let r1 = new_ref(41)
  let r2 = dynamic_ref(true)
  let v2 = if r2.get() { 1 } else { 0 }
  println(r1.get() + v2)

Named handlers are bound with:

with r <- named handler
  fn get() { state }
  fn set(x) { state = x }

Stacking Handlers

Multiple with statements stack flat — each covers the rest of the scope:

fn example() -> i64
  with diagnostic_stderr()
  with state_handler(0)
  State::set(42)
  State::get()

Single-Operation Shorthand

For handlers with a single operation, skip the handler keyword — specify the operation kind and name directly:

fn example() -> i64
  with ctl fail(msg)
    -1
  safe_div(10, 0)

fn example2() -> i64
  with ctl ask()
    resume(42)
  Ask::ask() + 1

fn example3() -> i64
  with fn get()
    42
  State::get()

This is equivalent to with handler Effect: op(params) { body } but more concise.

Override

override with re-handles an effect that is already in scope, replacing its handler.

The key difference from regular with: a regular handler is not in scope for its own body. If you call the same effect operation inside the handler, it escapes to an outer handler. override with makes the handler visible to itself, enabling recursion.

Delegating to the Outer Handler

fn wrap_ref(action: fn() -> e a) -> e a
  override with fn get() -> a
    get()     // calls the SAME handler (recursive)
  action()

Recursive Handlers

This is the primary use case. If your handler’s logic needs to call its own operation (e.g., open resolving a symlink by calling open again), override is required:

// Regular 'with': the inner open() escapes to a DIFFERENT handler above
with fn open(path)
  if is_symlink(path) { open(target) }  // looks for outer handler!

// 'override with': the inner open() calls back into THIS handler
override with fn open(path)
  if is_symlink(path) { open(target) }  // recursive — same handler

Without override, the default behavior prevents accidental infinite loops and allows layering (e.g., a file-logger that calls println, handled by a different console-logger above).

Mask

mask<effect> { body } temporarily hides an effect from the inner scope, so operations in the body skip the nearest handler and go to an outer one:

fn skip_inner(action: fn() -> e a) -> e a
  let x = mask<ref<s, a>> { get() }   // get() skips the inner ref handler
  action()

This is the inverse of override. Where override makes a handler visible to itself, mask makes a handler invisible to the inner scope. This is useful when you need to reach past a local handler to access an outer one:

fn example()
  with handler Logger
    fn log(msg) { print("[outer] " ++ msg) }

  with handler Logger
    fn log(msg) { print("[inner] " ++ msg) }

  log("hello")                         // goes to inner handler
  mask<Logger> { log("hello") }        // skips inner, goes to outer handler

Full Example: Heap

This example shows scoped effects, named effects, handlers, override, and mask working together:

scoped effect heap<s::S>
  ctl new_ref(init: a) -> ref<s, a>

named effect ref<s::S, a::V> in heap<s>
  fn get() -> a
  fn set(value: a)

fn with_ref(init: a, action: fn(ref<s, a>) -> e b) -> e b
  var state = init
  with r <- named handler
    fn get() { state }
    fn set(x) { state = x }
  action(r)

fn heap(action: forall<s> fn() -> <heap<s>, div|e> a) -> <div|e> a
  with ctl new_ref(init: a)
    with_ref(init, |v| resume(v))
  action()

fn main()
  with heap
  let r1 = new_ref(41)
  let r2 = dynamic_ref(true)
  let v2 = if r2.get() { 1 } else { 0 }
  println(r1.get() + v2)

Slices

A slice is a view into a contiguous sequence of elements: a pointer plus a length. Slices don’t own memory — they’re lightweight references to data that lives elsewhere (a string literal, an array, a heap allocation).

Slice Types

[]u8          // mutable slice of bytes
[]const u8    // immutable slice of bytes (read-only)
[]i32         // mutable slice of 32-bit integers
[]const i32   // immutable slice of integers

[]T is a mutable slice — you can read and write elements. []const T is an immutable slice — you can only read. A []T coerces to []const T (giving up write permission), but not the reverse.

String Literals

String literals are []const u8 — an immutable slice of UTF-8 bytes:

let greeting = "hello"     // type: []const u8
greeting.len               // 5
greeting[0]                // 104 (ASCII 'h')

There’s no special String type for literals. A string literal is just bytes with a known length.

Built-in Fields

Slices have two built-in fields:

let s = "hello world"

s.len    // usize — number of elements
s.ptr    // *const u8 — raw pointer to the first element

.len returns the number of elements as usize. .ptr returns a raw pointer to the underlying data — use this for FFI interop.

Indexing

Use [i] to access individual elements:

let s = "hello"
s[0]                  // 104 ('h')
s[4]                  // 111 ('o')
s[s.len - 1]          // 111 ('o') — last element

Indexing on []const T returns the element value. You cannot assign through a const slice:

let s = "hello"
s[0] = 65             // ERROR: cannot write through const pointer/slice

Subslicing (Range Syntax)

Use [start..end] to create a subslice:

let s = "hello world"
let hello = s[0..5]        // []const u8, "hello"
let world = s[6..11]       // []const u8, "world"

hello.len                  // 5
world[0]                   // 119 ('w')

The subslice shares the underlying data — no copy is made. The new slice is { ptr + start, end - start }.

Passing Slices to Functions

Slices are value types (pointer + length, two machine words). Pass them by value:

fn byte_count(s: []const u8) -> i32
  s.len as i32

byte_count("hello")       // 5
byte_count("hello world") // 11

Use []const T for read-only parameters. Use []T when the function needs to modify elements.

FFI Interop

For C functions that take a pointer and length separately, extract .ptr and .len:

// write(2) expects a raw pointer and byte count
write(1, s.ptr, s.len)

The .ptr field on []const T returns *const T. On []T it returns *T.

Creating Slices from Pointers

When working with C APIs that return raw pointers, use range syntax to create a slice:

extern fn strlen(s: *const u8) -> usize

// argv[0] is a *u8 (C string, null-terminated)
let name_ptr = argv[0]
let name = name_ptr[0..strlen(name_ptr)]  // []const u8

This is the explicit boundary between “raw pointer world” (no length, no safety) and “slice world” (known length, safe indexing).

Coercion Rules

  • []T coerces to []const T (safe: giving up write permission)
  • []const T does NOT coerce to []T (would allow writing read-only data)
  • *T does NOT implicitly become []T (must use range syntax with explicit length)

Many-Item Pointers ([*]T)

[*]T is a many-item pointer — a Perceus-managed (reference counted) heap allocation. It’s the ONE managed pointer type. Created by ore_alloc:

fn alloc(count: i32) -> [*]a
  ore_alloc(count * @sizeof(a)) as [*]a

Creating Slices from [*]T

Use range syntax to create a slice view of managed memory:

let buf: [*]u8 = alloc(100)
let view: []u8 = buf[0..50]     // slice view into buf

The slice borrows from the [*]T — Argus (the escape checker) ensures the slice doesn’t outlive the buffer.

Ownership Model

[*]T  → owns memory (Perceus RC, heap allocated)
[]T   → views memory (ptr + len, no ownership, 2 words)
*T    → raw pointer (no ownership, no length, 1 word)

[*]T is the owner. []T is a view. You can create views from owners, but views cannot escape the owner’s scope.

Library Convention

Functions that allocate often return a named product with the owner and size:

read_file :: fn(path: []const u8) -> !.{ .buf: [*]u8, .size: i32 }
  // ...
  .{ .buf = buf, .size = size }

file := try read_file("input.txt")
view := file.buf[0..file.size as usize]

The caller creates views as needed. The [*]T is dropped when the last reference dies.

Arrays vs Slices

Array [N]TSlice []T
SizeFixed, compile-timeRuntime length
MemoryInline (stack)Fat pointer (ptr + len)
OwnershipOwns dataBorrows data
Syntax[i32; 5][]i32

Arrays have a fixed size known at compile time. Slices have a runtime-known length and don’t own the underlying memory.

Summary

OperationExampleResult
Lengths.lenusize
Raw pointers.ptr*T or *const T
Indexs[i]Element T
Subslices[a..b][]T or []const T
String literal"hello"[]const u8

Ore Ownership & Memory Model

Ore’s memory and ownership system is simple, predictable, and orthogonal. Memory and operations are defined across four independent layers. Together, they dictate how names, memory, and mutation behave.

The Four Layers of Ore Memory:

  1. Binding — How a name is tied to a value (:: vs :=).
  2. Constness — Whether data can be changed (const).
  3. Representation — Where it lives (Stack, Heap, Rodata).
  4. Addressability — How you access it (Direct, Slice, Pointer).

1. Bindings: :: vs :=

:: — compile-time constant: Creates a name bound at compile time. Usually stored in .rodata or .data. Immutable and shared across calls.

:= — runtime binding: Binds a name to a value at runtime. The value may reside on stack or heap depending on type. Always rebindable; := does not mutate existing memory.

digits :: [10; u8].{0,1,2,3,4,5,6,7,8,9}  // compile-time, shared
x := [3; u8].{10,20,30}                    // stack-allocated, local
matrix := [2; [3; u8]].{
	.{1,2,3},
	.{4,5,6}
}                                          // nested fixed array, still stack-allocated

Nested fixed arrays stay in the fixed-array family. [2; [3; T]] is still a stack or rodata value, depending on whether it is introduced with := or ::.

Note: In Ore, a variable name is a fixed “slot.” Rebinding with := changes what’s in the slot; it does not create a new slot with the same name (no shadowing).

2. Memory Representation

SyntaxSegmentOwnerLifetimeNotes
:: [N; T].rodataBinaryGlobalImmutable, baked into binary
:= [N; T]StackScopeLocalCleared on function return
:= alloc(T, n)HeapPerceusManagedAutomatic RC; freed when ref drops
[]TSlice/ViewArgusScope-boundPoints into an allocation; no ownership
*TEscapee/RawArgusScope-confinedUntracked, cannot escape scope
  • Perceus: Automatic Memory Manager / Reference Counting.
  • Argus: Compile-time Safety / Escape Checker.

3. Ownership & Lifetime

3.1 Allocation-level ownership

  • Stack [N; T] — scope-owned; freed automatically.
  • Heap [*]T / Vec[T] — Perceus-managed; reference-counted.
  • Rodata :: [N; T] — immutable; lifetime tied to the binary.

3.2 Views & slices

  • []T — mutable view; cannot escape unless backing storage is Rodata or heap.
  • []const T — immutable view.

Slices do not increment RC. Mutating through a slice affects all aliases of the underlying allocation. A sub-slice (s2 := s1[1..3]) inherits the scope of the original allocation, not the intermediate slice — this prevents “scope laundering” through chains of sub-slices.

3.3 Who can leave the function?

  • Owners ([*]T, Vec[T]) — can escape any scope; returning one moves ownership to the caller.
  • Views ([]T, *T) — confined to the scope of backing storage. Argus enforces this at compile time.

4. Operations: := vs <-

4.1 Binding and Rebinding (:=)

:= creates or updates a ref cell. Rebinding an existing name updates the same cell (Koka-style x.set(value)).

When rebinding a heap allocation ([*]T):

  • FBIP (Functional But In Place): Reuses memory if RC=1.
  • COW (Copy on Write): If RC>1, a copy is made to preserve value semantics.

4.2 Memory mutation (<-)

Mutates addressable locations (elements, fields, dereferenced pointers). Operates directly on memory; never changes ownership or RC.

Mutation is not allowed on:

  • Bare bindings: x <- 20 — compile error. Use := to rebind.
  • []const T slices: view[0] <- 42 — compile error.
  • :: [N; T] values: Rodata is immutable — compile error.

5. Cheat Sheet

SyntaxTypeMemoryOwner<-:=Escapes?
x :: [N; T]ConstantRodataBinaryNoNoYes
x := [N; T]Local bufferStackScopeYesYesNo
x := [*]TTracked ptrHeapPerceusYesYes (FBIP)Yes
x := Vec[T]GrowableHeapPerceusYesYesYes
x : []TMutable viewViewArgusYesNoIf Rodata/Heap
x : *TEscapeeRawArgusYesNoNo

6. Summary

  1. :: is compile-time; := is runtime binding.
  2. := updates an existing cell (no shadowing).
  3. FBIP/COW ensures value semantics for heap rebindings.
  4. <- mutates memory at a location; RC is irrelevant here.
  5. Argus enforces safety at compile-time; no runtime checks or lifetime annotations required.

Pointers & Linear Types

Modules & Imports

Modules

File-Based Modules

Declare a module that maps to a file:

mod utils

This looks for utils.ore in the same directory.

Inline Modules

Define a module’s contents inline:

mod math
  fn add(x: i64, y: i64) -> i64
    x + y

Inline modules can contain any items (structs, functions, nested modules, etc.):

pvt mod helpers
  struct Helper
  fn assist() -> i64 { 42 }

Visibility and Attributes

pvt mod internal

#[cfg(test)]
mod tests
  fn test_add() -> bool { true }

Imports

Simple Path

Import a module:

use std::io

Specific Item

use std::io::File

Glob Import

Import everything from a module:

use std::io::*

Aliases

Rename on import:

use std::io as sio
use std::io::File as F

Tree Imports

Import multiple items from the same path:

use std::io::{File, BufReader}

Nested trees:

use std::{io::{File, BufReader}, collections::HashMap}

Aliases inside trees:

use std::io::{File, Write as W}

Special Path Segments

SegmentMeaning
superParent module
ignotPackage/crate root
use super::something
use super::super::deep
use ignot::parser::ast::Item

Private Imports

pvt use std::io::File

Intrinsics

Intrinsics are compiler-builtin operations prefixed with @. They bypass normal name resolution and map directly to low-level operations.

Syntax

@name(args)

Examples:

@add(x, y)
@nop()
@fma(a, b, c)

Purpose

Intrinsics exist for operations that cannot be expressed in Ore itself:

  • Arithmetic primitives@add<T>, @sub<T>, @mul<T>, etc. (used to implement operator traits)
  • Memory operations@alloc, @free, @load<T>, @store<T>, @copy
  • System calls@write, @read, @open, @close, @exit
  • Type operations@sizeof<T>, @as<To> (casting)
  • Process control@panic, @exit, @arg_count, @arg

Regular Ore code rarely uses intrinsics directly. They are primarily used in the standard library to implement traits and effects.

Keyword Reference

KeywordUsage
andLogical AND (short-circuit): x and y
asType cast: x as f64
breakExit enclosing for loop: break if i >= 10
catchInline error handling: expr catch err { fallback }
constCompile-time constant: const N: i64 = 42
ctlControl effect operation (captures continuation)
dataDeclare a data type (sum type): Point :: data .{ .x: i32, .y: i32 }
effectDeclare an algebraic effect
elseDefault arm: else => ..., or inline: if x then a else b
errorError set modifier: data E = error ..., or error value: error.NotFound
falseBoolean literal
finalFinal effect operation (never resumes)
fnDeclare a function, or a tail-resumptive effect operation
forallUniversal quantifier for scoped type variables: forall<s> fn() -> ...
handlerDefine an effect handler
ifConditional: if x > 0 then ..., guard-style: if { cond => ... }, postfix: break if x
ignotPackage/crate root in paths
inNamed effect containment: named effect ref<s, a> in heap<s>
letBind an immutable value to a name
forLoop: for, for cond, for i in 0..n, for x in coll, for x, i in coll, for x in coll where f(x)
maskHide an effect from the current scope
matchPattern matching: match x { pat => body }
modDeclare a module
namedNamed effect modifier: named effect ref<s, a>
nilAbsence of value (for ?T optional types)
notLogical NOT: not x
orLogical OR (short-circuit): x or y
orelseOptional default: expr orelse fallback
overrideRe-handle an effect in scope
pvtMark an item as private (public by default)
resumeResume continuation in ctl handler: resume(value)
returnReturn a value from a function early: return error.X if cond
scopedScoped effect modifier: scoped effect heap<s::S>
superParent module in paths
thenInline if branch: if cond then expr else expr
trueBoolean literal
tryError propagation: try expr
typeDeclare a type alias: type Id = i32
unsafeUnsafe block
useImport items from a module
varMutable binding: var x = 5
withInstall an effect handler

Reserved Symbols

SymbolUsage
@Intrinsic prefix: @add(x, y), @sizeof(T)
!Error union type prefix: !T
?Optional type prefix: ?T
=>Arm separator in match/if: pattern => body
#[...]Attribute annotation
|...|Lambda parameters: |x| x + 1
|Pipe separator in data variants: data Color = Red | Green
::Path separator or kind annotation: Foo::bar, s::S
..Spread in literals, range in patterns
->Return type separator
<-Handler binding: with r <- named handler
_Wildcard pattern