Language Basics

This guide teaches you the core building blocks of Ori through progressive examples. By the end, you’ll understand variables, types, expressions, and control flow well enough to write real programs.

Everything Is an Expression

Before diving into syntax, understand Ori’s fundamental principle: everything is an expression that produces a value.

In many languages, you write “statements” that do things but don’t return values:

// JavaScript - statements don't return values
let x = 5;        // This is a statement
if (x > 0) {      // This is a statement
  console.log("positive");
}

In Ori, everything returns a value:

// Ori - everything is an expression
let x = 5;                                    // Returns () (void)
let sign = if x > 0 then "positive" else "negative";  // Returns "positive"

This means you can use any expression anywhere a value is expected:

let result = {
    let a = 10;
    let b = 20;

    a + b    // No semicolon: this is the block's value (30)
};

let message = `The answer is {if ready then compute() else "pending"}`;

Keep this in mind — it explains why Ori code looks the way it does.

Primitive Types

Ori provides these fundamental types:

Numbers

Integers (int) are signed, with range -2⁶³ to 2⁶³ - 1:

let a = 42;
let b = -17;
let c = 1_000_000;      // Underscores for readability
let d = 0xFF;           // Hexadecimal (255)
let e = 0b1010;         // Binary (10)

Floats (float) follow IEEE 754 double-precision semantics:

let pi = 3.14159;
let tiny = 2.5e-8;
let big = 1.5e10;

Integer division truncates toward zero:

7 / 3;      // 2, not 2.333...
-7 / 3;     // -2, not -3

Integer overflow causes a panic (runtime error):

let max = 9223372036854775807;  // Maximum int
let overflow = max + 1;         // PANIC: integer overflow

For wrapping arithmetic, use std.math:

use std.math { wrapping_add };

let result = wrapping_add(left: max, right: 1);  // Wraps to minimum int

Booleans

let active = true;
let pending = false;

let result = active && pending;   // false (and)
let either = active || pending;   // true (or)
let negated = !active;            // false (not)

Short-circuit evaluation: && and || only evaluate the right side if needed:

let safe = is_valid(x) && expensive_check(x);  // expensive_check only runs if is_valid is true

Strings

Regular strings use double quotes:

let greeting = "Hello, World!";
let multiline = "Line 1\nLine 2\nLine 3";
let escaped = "She said \"Hello\"";
let path = "C:\\Users\\Alice";

Escape sequences:

  • \\ — backslash
  • \" — double quote
  • \n — newline
  • \t — tab
  • \r — carriage return
  • \0 — null character

Template strings use backticks and support interpolation:

let name = "Alice";
let age = 30;

let simple = `Hello, {name}!`;              // "Hello, Alice!"
let computed = `In 10 years: {age + 10}`;   // "In 10 years: 40"
let nested = `{if age >= 18 then "adult" else "minor"}`;

Format specifiers control how values are displayed:

let n = 42;
let pi = 3.14159;

`Hex: {n:x}`;           // "Hex: 2a"
`HEX: {n:X}`;           // "HEX: 2A"
`Binary: {n:b}`;        // "Binary: 101010"
`Padded: {n:05}`;       // "Padded: 00042"
`Float: {pi:.2}`;       // "Float: 3.14"
`Right: {n:>10}`;       // "Right:         42"
`Left: {n:<10}`;        // "Left: 42        "
`Center: {n:^10}`;      // "Center:     42    "

String indexing returns a single character as a string:

let s = "Hello";
s[0];     // "H"
s[4];     // "o"
s[# - 1]; // "o" (# is the length inside brackets)

Characters

Single characters use single quotes:

let letter = 'A';
let emoji = '🎉';
let newline = '\n';

Characters are Unicode code points, not bytes.

Void and Unit

void represents “no meaningful value”:

@print_greeting (name: str) -> void = print(msg: `Hello, {name}!`);

The unit value () is the single value of type void:

let nothing = ();
let also_nothing: void = ();

Never

The Never type represents computations that never complete normally:

@fail (msg: str) -> Never = panic(msg: msg);

Functions returning Never either panic or loop forever.

Special Literals

Duration for time values:

let timeout = 30s;       // 30 seconds
let interval = 100ms;    // 100 milliseconds
let delay = 5m;          // 5 minutes
let long = 2h;           // 2 hours

Size for byte quantities:

let buffer = 4kb;        // 4 kilobytes
let limit = 10mb;        // 10 megabytes
let quota = 2gb;         // 2 gigabytes

Variables and Bindings

Creating Variables

Use let to bind a name to a value:

let name = "Alice";
let age = 30;
let pi = 3.14159;
let active = true;

The variable exists from its declaration to the end of its scope:

@main () -> void = {
    let x = 10;          // x exists from here...
    let y = x + 5;       // ...can use it here...
    print(msg: `{y}`);   // ...and here
}                        // x and y go out of scope

Type Inference

Ori infers types from values:

let count = 42;          // Ori infers: int
let price = 19.99;       // Ori infers: float
let name = "Alice";      // Ori infers: str
let active = true;       // Ori infers: bool

You can add type annotations for clarity or when inference needs help:

let count: int = 42;
let price: float = 19.99;
let items: [int] = [];        // Empty list needs annotation
let lookup: {str: int} = {};  // Empty map needs annotation

Mutable vs Immutable

By default, bindings can be reassigned:

let counter = 0;
counter = 1;      // OK - reassignment
counter = 2;      // OK - reassignment again

Add $ to make a binding immutable:

let $max_size = 100;
max_size = 200;   // ERROR: cannot reassign immutable binding

Guidance: Use $ by default. Only remove it when you actually need to reassign:

// Good: clearly communicates intent
let $config = load_config();      // Won't change after loading
let $user_id = get_user_id();     // Identity, shouldn't change

// Only mutable when needed
let total = 0;
for item in items do
    total = total + item.price;   // Accumulating, needs reassignment

Shadowing

You can declare a new variable with the same name, which shadows the previous one:

let x = 10;
let x = x + 5;        // New binding, shadows previous x
let x = `value: {x}`; // New binding, different type is OK

This is different from reassignment — you’re creating a new binding:

let $x = 10;      // Immutable
let $x = x + 5;   // OK: new immutable binding, shadows previous

Shadowing is useful for transforming values through a pipeline:

let data = fetch_raw();
let data = parse(raw: data);
let data = validate(parsed: data);
let data = transform(validated: data);

Scope

Variables are scoped to their containing block:

@example () -> int = {
    let outer = 10;

    let inner_result = {
        let inner = 20;        // Only visible in this block

        outer + inner          // outer is still visible
    };

    // inner is not visible here
    outer + inner_result
}

Operators

Arithmetic

OperatorDescriptionExample
+Add5 + 3 -> 8
-Subtract5 - 3 -> 2
*Multiply5 * 3 -> 15
/Divide5 / 3 -> 1
%Remainder5 % 3 -> 2
divFloor divide5 div 3 -> 1

For floats, / does true division: 5.0 / 3.0 -> 1.666...

Comparison

OperatorDescriptionExample
==Equal5 == 5 -> true
!=Not equal5 != 3 -> true
<Less than3 < 5 -> true
>Greater than5 > 3 -> true
<=Less or equal5 <= 5 -> true
>=Greater or equal5 >= 3 -> true

Logical

OperatorDescriptionExample
&&And (short-circuit)true && false -> false
||Or (short-circuit)true || false -> true
!Not!true -> false

Bitwise

OperatorDescriptionExample
&Bitwise and0b1100 & 0b1010 -> 0b1000
|Bitwise or0b1100 | 0b1010 -> 0b1110
^Bitwise xor0b1100 ^ 0b1010 -> 0b0110
~Bitwise not~0b1100 -> ...0011
<<Left shift1 << 4 -> 16
>>Right shift16 >> 2 -> 4

String Concatenation

Use + to concatenate strings:

let full = "Hello" + ", " + "World!";  // "Hello, World!"

Or use template strings:

let first = "Hello";
let second = "World";
let full = `{first}, {second}!`;       // "Hello, World!"

Operator Precedence

From highest to lowest:

  1. . [] () ? as as? — access, index, call, propagate, conversion
  2. ** — power
  3. ! - ~ — unary operators
  4. * / % div — multiplicative
  5. + - — additive
  6. << >> — shift
  7. .. ..= by — range
  8. < > <= >= — comparison
  9. == != — equality
  10. & — bitwise and
  11. ^ — bitwise xor
  12. | — bitwise or
  13. && — logical and
  14. || — logical or
  15. ?? — coalesce
  16. |> — pipe

When in doubt, use parentheses:

let result = (a + b) * c;
let flag = (x > 0) && (y < 10);

The Coalesce Operator ??

The ?? operator provides a default value for None or Err:

let name = maybe_name ?? "Anonymous";
let count = parse_int(s: input) ?? 0;
let config = load_config() ?? default_config;

Control Flow

Conditionals

The if expression evaluates a condition and returns one of two values:

let status = if age >= 18 then "adult" else "minor";

Both branches must have the same type:

// ERROR: branches have different types
let result = if condition then 42 else "hello";

Chain conditions with else if:

let grade = if score >= 90 then "A"
    else if score >= 80 then "B"
    else if score >= 70 then "C"
    else if score >= 60 then "D"
    else "F";

Without else: When you don’t need a value, omit else:

if should_log then print(msg: "Logging...");
// Returns () if condition is false

Loops with for

Basic iteration:

for item in items do
    print(msg: item);

Multiple statements in a block:

for item in items do {
    let processed = transform(input: item);
    print(msg: processed);
};

Collecting with yield:

let doubled = for x in numbers yield x * 2;
// [1, 2, 3] becomes [2, 4, 6]

Filtering with if:

let positive = for x in numbers if x > 0 yield x;
// [-1, 2, -3, 4] becomes [2, 4]

Combining filter and transform:

let result = for x in numbers if x > 0 yield x * 2;
// [-1, 2, -3, 4] becomes [4, 8]

Ranges

Create sequences of numbers:

0..5;        // 0, 1, 2, 3, 4 (exclusive end)
0..=5;       // 0, 1, 2, 3, 4, 5 (inclusive end)
0..10 by 2;  // 0, 2, 4, 6, 8 (with step)
10..0 by -1; // 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 (descending)

Use in loops:

for i in 0..10 do
    print(msg: `Index: {i}`);

for i in 0..100 by 10 do
    print(msg: `Tens: {i}`);

Loop Control

break exits a loop early:

let found = loop {
    let item = next_item();
    if item == target then break item;
    if is_empty(collection: remaining) then break None;
};

continue skips to the next iteration:

for item in items do {
    if should_skip(item: item) then continue;
    process(item: item);
};

Labeled loops for nested control:

for:outer i in 0..10 do
    for j in 0..10 do {
        if condition(x: i, y: j) then break:outer;
        process(x: i, y: j);
    };

The loop Pattern

For loops that don’t iterate over a collection:

let result = loop {
    let input = read_line();
    if input == "quit" then break "goodbye";
    print(msg: `You said: {input}`);
};

loop runs forever until break is called. The value passed to break becomes the loop’s result.

Type Conversions

Ori doesn’t do implicit conversions. Use explicit syntax:

Infallible Conversions with as

When conversion always succeeds:

let x = 42;
let y = x as float;      // 42.0

let n = 65;
let c = n as char;       // 'A'

Fallible Conversions with as?

When conversion might fail:

let s = "42";
let n = s as? int;       // Some(42)

let bad = "hello";
let m = bad as? int;     // None

Common Conversions

// Numbers
42 as float;             // 42.0
3.7 as int;              // 3 (truncates)
65 as char;              // 'A'
'A' as int;              // 65

// Strings
42 as str;               // "42"
3.14 as str;             // "3.14"
true as str;             // "true"

// Parsing (fallible)
"42" as? int;            // Some(42)
"3.14" as? float;        // Some(3.14)
"true" as? bool;         // Some(true)
"nope" as? int;          // None

Comments and Documentation

Line Comments

Comments must be on their own line:

// This is valid
let x = 42;

let y = 42;  // This is a syntax error - no inline comments

Doc Comments

Use special markers for documentation:

// Calculates the area of a circle
//
// * radius: The radius of the circle (must be positive)
// ! Panics if radius is negative
// > area(radius: 5.0) -> 78.54

@area (radius: float) -> float
    pre(radius >= 0.0 | "radius must be non-negative")
= 3.14159 * radius * radius;

Putting It Together

Let’s write a small program using everything we’ve learned:

// Configuration
let $TAX_RATE = 0.08;
let $DISCOUNT_THRESHOLD = 100.0;

// Types
type Item = { name: str, price: float, quantity: int }

// Calculate item total
@item_total (item: Item) -> float =
    float(item.quantity) * item.price;

@test_item_total tests @item_total () -> void = {
    let item = Item { name: "Widget", price: 10.0, quantity: 3 };
    assert_eq(actual: item_total(item: item), expected: 30.0);
}

// Calculate subtotal
@subtotal (items: [Item]) -> float =
    items.map(item -> item_total(item: item))
        .fold(initial: 0.0, op: (acc, x) -> acc + x);

@test_subtotal tests @subtotal () -> void = {
    let items = [
        Item { name: "A", price: 10.0, quantity: 2 },
        Item { name: "B", price: 5.0, quantity: 3 },
    ];
    assert_eq(actual: subtotal(items: items), expected: 35.0);
}

// Calculate discount
@discount (amount: float) -> float =
    if amount >= $DISCOUNT_THRESHOLD then amount * 0.10 else 0.0;

@test_discount tests @discount () -> void = {
    assert_eq(actual: discount(amount: 150.0), expected: 15.0);
    assert_eq(actual: discount(amount: 50.0), expected: 0.0);
}

// Calculate final total
@calculate_total (items: [Item]) -> float = {
    let sub = subtotal(items: items);
    let disc = discount(amount: sub);
    let tax = (sub - disc) * $TAX_RATE;

    sub - disc + tax
}

@test_calculate_total tests @calculate_total () -> void = {
    let items = [
        Item { name: "Widget", price: 50.0, quantity: 3 },
    ];
    // subtotal: 150, discount: 15, taxable: 135, tax: 10.8
    assert_eq(actual: calculate_total(items: items), expected: 145.8);
}

// Main program
@main () -> void = {
    let $cart = [
        Item { name: "Keyboard", price: 75.0, quantity: 1 },
        Item { name: "Mouse", price: 25.0, quantity: 2 },
        Item { name: "Cable", price: 10.0, quantity: 3 },
    ];
    print(msg: `Subtotal: {subtotal(items: cart):.2}`);
    print(msg: `Total:    {calculate_total(items: cart):.2}`);
}

Quick Reference

Variables

let x = 42;              // Mutable
let $x = 42;             // Immutable
let x: int = 42;         // With type

Types

int, float, bool, str, char, byte, void, Never
Duration, Size
[T], {K: V}, Set<T>     // Collections
(T, U), (T, U, V)       // Tuples
Option<T>, Result<T, E> // Sum types

Operators

+ - * / % div           // Arithmetic
== != < > <= >=         // Comparison
&& || !                 // Logical
& | ^ ~ << >>           // Bitwise
.. ..= by               // Range
??                      // Coalesce
as as?                  // Conversion

Control Flow

if cond then a else b
for x in items do expr
for x in items yield expr
for x in items if cond yield expr
loop {expr}
break value
continue

What’s Next

Now that you understand the language basics:

  • Functions — Deep dive into function definitions, generics, and lambdas
  • Collections — Lists, maps, sets, and tuples