Getting Started

Oxynium is a statically-typed, compiled language. It compiles directly to x86-64 NASM assembly, which is then assembled and linked by GCC to produce a native binary. It targets Linux x86-64 and macOS x86-64.

Installation

You can install Oxynium on any Linux/macOS system by running the following command in your terminal:

curl -sSL https://oxynium.org/scripts/install | bash

Unstable version

You can also install the latest version by passing the latest argument to the installation script.

curl -sSL https://oxynium.org/scripts/install | bash -s -- latest

Requirements

Oxynium requires cargo, rust, nasm, and gcc to be installed on your system.

Upgrade

To upgrade to the latest version, run the installation script again:

curl -sSL https://oxynium.org/scripts/install | bash

Uninstallation

curl -sSL https://oxynium.org/scripts/uninstall | bash

Compiling and running

The compiler binary is oxy. Pass a source file to compile it:

oxy hello.oxy

This produces an executable in the current directory. Run it directly:

./hello

Hello, World

The simplest possible Oxynium program is a single top-level statement:

print("Hello, world!")

Output:

Hello, world!

For larger programs, define a main function:

def main() {
    print("Hello, world!")
}

When main is defined, all executable code must live inside it — top-level statements outside main are a compile error. See Program Structure for details.

Accepting command-line arguments

def main(args: List<Utf8Str>) {
    print(args.len().Str())
    for arg in args {
        print(arg.Str())
    }
}

args includes the binary name as the first element, so a program run with no user arguments has args.len() equal to 1. Utf8Str is the raw OS string type; call .Str() to convert it to the standard Str type before using string operations.

Compilation pipeline

source.oxy
  → Lexer (tokens)
  → Parser (AST)
  → Type Checker
  → Code Generator (NASM assembly)
  → Post-processor
  → NASM assembler
  → GCC linker
  → native binary

Error types

The compiler emits one of the following error kinds:

Error kindWhen it occurs
SyntaxErrorThe source text cannot be parsed
TypeErrorA type rule is violated
UnknownSymbolA name is used that has not been declared
IoErrorAn external function is declared but not linked
NumericOverflowAn integer literal exceeds the 64-bit signed range

Program Structure

Two modes of execution

Script mode — no main function. Top-level statements execute in the order they appear:

const MSG = "Hello"
print(MSG)          // runs immediately
print("World")      // runs immediately

Main mode — a main function is defined. Only main, const, class, def, and extern def may appear at the top level. Executable statements outside main are a SyntaxError:

def f() {}
def main() {
    print("hi")
}
f()           // SyntaxError: executable statement after main definition

What can appear at the top level

In script mode: const, class, def, extern def, and any executable statement. In main mode: const, class, def, extern def only.

Semicolons

Semicolons are optional. The compiler performs automatic end-of-statement insertion. Both styles compile identically:

print("a"); print("b")
print("a")
print("b")

Declaration order

The compiler performs a setup pass before type checking, so functions and classes can be used before their definition in the source file:

def f(a: A) A { return a }
class A;    // A is used above its definition — valid

Local variables, however, cannot be used before their let declaration in the same function body — this is a TypeError.

Locally-defined classes

Classes can be defined inside function bodies and are scoped to that function:

def main() {
    class C
    let x = new C    // C is usable here
}
def f(a: C)    // UnknownSymbol: C is not in scope here

Comments

Single-line comments start with // and run to the end of the line:

// This entire line is a comment.
let x = 1  // This is an inline comment.

There is no block comment syntax. Use multiple // lines.

/// is used in the standard library for documentation annotations. In user code, /// behaves identically to //.

Variables and Constants

Global constants (const)

Declared at the top level of a file (outside any function). Always require an initial value. Type is inferred from the value. Cannot be mutated.

const MAX = 100
const GREETING = "Hello"
const PI_APPROX = 314

Constants may reference expressions involving other constants:

const BASE = 10
const LIMIT = BASE * 5   // LIMIT = 50

Attempting to redeclare a constant with the same name is a TypeError. Declaring a const inside a function is a SyntaxError.

Local variables (let)

Declared inside a function body. Immutable by default — the variable cannot be reassigned after its initial binding.

def main() {
    let x = 42
    let name = "Alice"
    let greeting = "Hello, " + name
}

An optional type annotation must match the inferred type:

let x: Int = 42      // valid
let x: Int = ""      // TypeError: mismatched types

Variables cannot be declared without an initial value (use let mut for that):

def f() {
    let a;        // SyntaxError: missing value
}

Redeclaring the same name in the same function is a TypeError. There is no shadowing.

Declaring a let at the top level is a SyntaxError.

Mutable local variables (let mut)

Allows reassignment after the initial binding. The type is fixed at declaration.

def main() {
    let mut count = 0
    count = count + 1
    count = 99
}

Assigning a different type is a TypeError:

def main() {
    let mut a = 1
    a = ""    // TypeError: cannot assign Str to Int
}

Empty let mut declarations

let mut may be declared with a type annotation but no initial value. The variable must be assigned before it is read:

def f(condition: Bool) Int {
    let mut result: Int
    if condition {
        result = 1
    } else {
        result = 2
    }
    return result    // valid: both branches assign
}

Reading an uninitialised variable is a TypeError. An immutable let cannot use the empty declaration form.

Compound assignment operators

Shorthand for variable = variable OP value. The variable must be mut.

OperatorExampleEquivalent
+=a += 3a = a + 3
-=a -= 3a = a - 3
*=a *= 3a = a * 3
/=a /= 3a = a / 3
%=a %= 3a = a % 3
def main() {
    let mut a = 1
    a += 1    // 2
    a -= 1    // 1
    a *= 4    // 4
    a /= 2    // 2
    print(a.Str())
}

+= also works on Str variables (string concatenation):

def main() {
    let mut s = "hello"
    s += " world"
    print(s)    // hello world
}

Scoping rules

Oxynium does not have block-level scoping. A variable declared inside an if, while, or for block is accessible in the enclosing function after that block. Redeclaring a name from an outer scope inside an inner block is a TypeError:

def main() {
    let a = 1
    if true {
        let a = 2    // TypeError: duplicate declaration
    }
}

For-loop variables persist after the loop ends, holding the last values seen:

def main() {
    for c, i in "abc" {}
    // c = 'c', i = 3 — last values from the loop
    print(i.Str() + c.Str())    // 3c
}

Field mutation through immutable variables

The let / let mut distinction controls whether the variable binding can be reassigned. Fields on a class instance can still be mutated through a non-mut variable:

class Point { x: Int, y: Int }

def main() {
    let p = new Point { x: 0, y: 0 }
    p.x = 5    // valid: mutating a field, not reassigning p
}

Types

Built-in primitive types

TypeDescriptionDefault (new T)Size
Int64-bit signed integer08 bytes
BoolBooleanfalse8 bytes
CharSingle Unicode characternull char (0)8 bytes
StrImmutable character string"" (empty)pointer
VoidUnit / no value

Void is the return type of functions that produce no value. new Void is a valid expression used in low-level code but has no useful value.

Literals

42          // Int
-7          // unary minus applied to Int 7
true        // Bool
false       // Bool
'a'         // Char
'\n'        // Char — newline
"hello"     // Str
""          // Str — empty string

Type annotations

Annotations appear after the identifier, separated by ::

let x: Int = 5
let s: Str = "hello"
def f(a: Int, b: Bool) Str { ... }

Type inference

The compiler infers types when they can be determined from context. Parameter types are never inferred — they must always be explicit. Return types can be inferred for single-expression (->) functions.

let x = 42               // inferred: Int
let s = "hi"             // inferred: Str
def square(n: Int) -> n * n   // return type inferred: Int

Using a variable before it is declared is a TypeError:

def f() Int {
    let b = a    // TypeError: a not yet declared
    let a = 5
    return b
}

Generic types

Parameterised with angle brackets. The standard library provides List<T>, Option<T>, Result<T, E>, Ptr<T>, and Range.

List<Int>
Option<Str>
Result<Int, Str>
Ptr<Bool>

Nesting is allowed:

List<Option<Int>>
Option<List<Str>>
Result<Option<Int>, Str>

Optional shorthand

T? is syntactic sugar for Option<T>. Multiple ? can be chained:

let a: Int? = Option.none!<Int>()
let b: Int?? = Option.none!<Int?>()
let c: Str? = Option.some!<Str>("hi")

Function types

Written as Fn(Param1, Param2, ...) ReturnType. Used as parameter or variable types.

def apply(f: Fn(Int) Int) Int { return f(5) }
def run(f: Fn() Void) { f() }
def combine(f: Fn(Int, Str) Bool) { ... }

Generic function types (Fn<T>(T) T) are not permitted — this is a SyntaxError.

The typeof operator

typeof expr evaluates to a Str naming the compile-time type of the expression. See typeof.

Operators

Arithmetic operators

All arithmetic operators work on Int operands and return Int. Mixing types is a TypeError.

OperatorDescriptionExampleResult
+Addition3 + 47
-Subtraction10 - 37
*Multiplication3 * 412
/Integer division7 / 23
%Modulo7 % 21

Integer division truncates toward zero. Division or modulo by the literal 0 is a compile-time TypeError. Division by a runtime zero value causes undefined behaviour.

Operator precedence follows standard mathematical convention:

  • *, /, % bind tighter than +, -
  • Left-to-right associativity within the same precedence level
print((1 + 2 * 3 + 4 * 5).Str())      // 27  (= 1 + 6 + 20)
print((1 + 2 * 3 - 4 * 5 / 2).Str())  // -3

Unary operators

OperatorDescriptionExampleResult
-Arithmetic negation-5-5
!Logical NOT!truefalse

Unary - applies only to Int. ! applies only to Bool. Applying ! to an Int is a TypeError.

Comparison operators

Return Bool. For Int, all six are built in. Str supports == and != only.

OperatorDescription
==Equal
!=Not equal
<Less than
>Greater than
<=Less than or equal
>=Greater than or equal

Comparison operators are not chainable1 < 2 < 3 is a TypeError because the result of 1 < 2 is Bool, and Bool < Int is not defined.

Logical operators

OperatorDescriptionOperand typesReturn type
&&Logical ANDBool, BoolBool
\|\|Logical ORBool, BoolBool
!Logical NOTBoolBool

&& and || do not short-circuit — both sides are always evaluated.

true && false    // false
true || false    // true
!true            // false
0 && 1           // TypeError: && requires Bool operands

String concatenation

+ on two Str values returns a new Str:

"Hello, " + "world!"    // "Hello, world!"
"" + "abc"              // "abc"

None-coalescing operator (??)

Unwraps an Option<T>. Returns the contained value if some, or the right-hand default if none:

def main() {
    let x: Int? = Option.none!<Int>()
    let v = x ?? 42     // v = 42

    let y: Int? = Option.some!<Int>(7)
    let w = y ?? 42     // w = 7
}

Compound assignment

+=, -=, *=, /=, %= — see Variables. These are not overloadable.

Full precedence table (high to low)

LevelOperatorsAssociativity
1unary -, !, typeofright
2*, /, %left
3+, -left
4<, >, <=, >=left
5==, !=left
6&&left
7\|\|left
8??left
9=, +=, -=, *=, /=, %=right

Operator overloading

Classes may define methods named after binary operator symbols. Only binary operators may be overloaded; unary operators (!) may not.

Valid overloadable operators: +, -, *, /, %, ==, !=, <, >, <=, >=, &&, ||, ??.

Rules:

  • The method must take exactly one parameter in addition to self.
  • That parameter must not have a default value.
  • Operator methods can only be defined inside a class, not at the top level.
  • Compound assignment operators (+= etc.) cannot be overloaded.
class Vec2 {
    x: Int,
    y: Int,
    def + (self, other: Vec2) Vec2 ->
        new Vec2 { x: self.x + other.x, y: self.y + other.y },
    def == (self, other: Vec2) Bool ->
        self.x == other.x && self.y == other.y
}

def main() {
    let a = new Vec2 { x: 1, y: 2 }
    let b = new Vec2 { x: 3, y: 4 }
    let c = a + b
    print(c.x.Str())          // 4
    print(c.y.Str())          // 6
    print((a == b).Str())     // false
}

Invalid overload forms:

class C {
    def ! (self) C { ... }              // SyntaxError: ! is not overloadable
    def += (self, other: C) C { ... }   // SyntaxError: compound assignment not overloadable
    def + (self) C { ... }              // TypeError: must take exactly one non-self parameter
}
def + (a: C, b: C) C { ... }           // SyntaxError: top-level operator definition

Control Flow

if statements

if condition {
    // body
}

if condition {
    // then
} else {
    // else
}

if condition {
    // branch 1
} else if other_condition {
    // branch 2
} else {
    // branch 3
}

The condition must have type Bool. Using an Int, Str, or class instance as a condition is a TypeError:

if 0 { }      // TypeError: condition must be Bool
if "" { }     // TypeError

Arrow syntax for single statements

-> replaces { } when the body is a single statement:

const x = 1
if x > 0 -> print("positive")
if x > 0 -> print("positive") else print("not positive")

while loops

Conditional while — runs while the condition is Bool true:

def main() {
    let mut i = 0
    while i < 5 {
        print(i.Str())
        i += 1
    }
}
// prints 01234

Infinite loop — no condition; must use break to exit:

while {
    print("once")
    break
}

Arrow form — single-statement body:

def main() {
    let mut i = 0
    while i == 0 -> i = 1
}

The condition (when present) must be Bool. while 1 {} is a TypeError.

break, continue, and return

break exits the innermost loop. continue skips to the next iteration. Both are valid inside while and for loops.

def main() {
    let mut i = 0
    while {
        i += 1
        if i < 5 -> continue
        print(i.Str())    // prints 5
        break
    }
}

return inside a loop exits the enclosing function:

def f() {
    let mut i = 0
    while {
        i += 1
        if i > 2 -> return
        print(i.Str())
    }
}
f()    // prints 12

for loops

Iterates over any value that has len() and at_raw() methods. The built-in iterable types are List<T>, Str, and Range.

for item in collection {
    // body
}

// With index (0-based):
for item, index in collection {
    // body
}

// Arrow form:
for item in collection -> print(item.Str())

Iterating a List<T> yields values of type T:

def main() {
    let arr = List.empty!<Int>()
    arr.push(1)
    arr.push(2)
    arr.push(3)
    for n in arr {
        print(n.Str(), ",")
    }
    // prints 1,2,3,
}

Iterating a Str yields Char values:

def main() {
    for c, i in "abc" {
        print(i.Str() + c.Str(), " ")
    }
    // prints 0a 1b 2c
}

Iterating a Range yields Int values:

for i in range(5) {
    print(i.Str())    // 01234
}

Iterating over a non-iterable type (e.g. Int, Bool) is a TypeError.

Loop variable persistence

Loop variables persist after the loop ends, holding the last values assigned:

def main() {
    for c, i in "abc" {}
    print(i.Str() + c.Str())    // 3c
}

break and continue in for loops

def main() {
    for i in range(5) {
        if i >= 3 -> break
        print(i.Str(), ",")
    }
    // prints 0,1,2,
}
def main() {
    for i in range(5) {
        print(i.Str())
        if i >= 3 -> continue
        print(",")
    }
    // prints 0,1,2,34
}

Nested loops

def main() {
    let arr = List.empty!<Int>()
    arr.push(1)
    arr.push(2)
    for n in arr {
        for m in "ab" {
            print(m.Str() + n.Str())
        }
    }
    // prints a1b1a2b2
}

Functions

Named functions (def)

def name(param1: Type1, param2: Type2) ReturnType {
    // body
}

Parameter types are always required. Omitting a type annotation is a TypeError. The return type can be omitted when it is Void or when using -> syntax (inferred from the expression).

A function with a non-Void return type must have a return on every execution path. A missing return on any path is a TypeError:

def f() Int {
    if true {
        return 1
    } else {
        return 2
    }
    // valid: both paths return
}

An if without an else does not count as covering all paths, even when the condition is true — the compiler performs a structural check, not value analysis.

A while { } with no condition is treated as always returning (an infinite loop). A conditional while condition { } does not cover the case where the loop never runs:

def f(a: Bool) Int {
    while a {
        return 1
    }
    return 2    // required
}

Arrow syntax

Single-expression body with implicit return:

def add(a: Int, b: Int) Int -> a + b
def square(n: Int) -> n * n          // return type inferred as Int
def greet(name: Str) -> print("Hello, " + name)   // inferred as Void

Default parameters

Parameters may have default values. Parameters with defaults must come after all parameters without defaults:

def f(a: Int, b: Int = 2, c: Int = 3) {
    print(a.Str() + b.Str() + c.Str())
}
f(1)       // prints 123
f(1, 9)    // prints 193

def f(a: Int = 1, b: Int) {}   // TypeError: non-default after default

The default value's type must match the parameter's type:

def f(a: Int = "") {}    // TypeError: Str default for Int parameter

Trailing commas in parameter lists are allowed.

Recursive functions

def fib(n: Int) Int {
    if n <= 1 -> return n
    return fib(n - 1) + fib(n - 2)
}
print(fib(10).Str())    // 55

The main entry point

main is an optional entry point. It must return Void.

def main() {
    print("running")
}

main accepts an optional List<Utf8Str> argument for command-line arguments. Any other parameter type is a TypeError. When main is defined, no executable statements may appear at the top level.

External functions (extern def)

Declare a function implemented in another compilation unit (C interop). The compiler emits a call; the linker must provide the implementation.

extern def my_c_function(x: Int) Str;

Must end with ; — bodies are forbidden. Cannot be named main. Calling an unlinked external function produces an IoError.

Anonymous Functions

Anonymous (lambda) functions are first-class values. They can be stored in variables, passed as arguments, and returned from functions.

Syntax

def main() {
    let double = fn (x: Int) Int { return x * 2 }
    let triple = fn (x: Int) Int -> x * 3
    let add    = fn (a: Int, b: Int) -> a + b
    let greet  = fn () -> print("hi")
}

The return type can be inferred with ->. An explicit return type is written before ->:

def main() {
    let f = fn () Int -> 1     // explicit return type
    let g = fn () -> 1         // inferred return type
}

Anonymous functions cannot be immediately invoked:

(fn () { print("hi") })()    // SyntaxError

Closure restriction

Anonymous functions can only access global constants and top-level definitions. They cannot close over local variables from the enclosing function:

def main() {
    let x = 5
    let f = fn () Int { return x }    // UnknownSymbol: x is not accessible
}

Global constants are accessible:

const MESSAGE = "hi"
def main() {
    let a = fn () -> print(MESSAGE)
    a()    // prints hi
}

Anonymous functions also cannot reference sibling local fn values (one local fn cannot call another declared in the same function).

Higher-order functions

Passing anonymous functions to functions that accept function parameters:

def apply(f: Fn(Int) Int, x: Int) Int -> f(x)

def main() {
    print(apply(fn (n: Int) Int -> n * n, 5).Str())    // 25
}
def do_something(f: Fn(Int) Int) Int {
    return f(42)
}

def main() {
    let plus_one = fn (x: Int) Int { return x + 1 }
    print(do_something(plus_one).Str())    // 43
}

An anonymous function passed with a mismatched signature is a TypeError:

def apply(f: Fn(Int) Int) { ... }
apply(fn (x: Int) -> "")    // TypeError: Str returned, Int expected

Named functions as first-class values

Named functions can also be stored in variables and reassigned:

def inc(x: Int) Int -> x + 1
def add_two(x: Int) Int -> x + 2

def main() {
    let mut f = inc
    print(f(1).Str())    // 2
    f = add_two
    print(f(1).Str())    // 3
}

Classes

Defining a class

class Point {
    x: Int,
    y: Int
}

Fields are separated by commas. A trailing comma is allowed. Fields and method definitions can be interleaved in any order.

Empty class forms:

class Token
class Empty;
class Empty {}

Classes cannot be defined inside other classes (SyntaxError). Redefining an existing class name is a TypeError.

Instantiation (new)

def main() {
    let p = new Point { x: 3, y: 4 }
}

All fields must be provided. Providing the wrong fields, wrong count, or wrong types is a TypeError. For classes with no fields, new C, new C{}, and new C { } are all valid.

Field shorthand: if a local variable has the same name as a field, the name alone can be used:

class C { a: Int }

def main() {
    let a = 1
    print(new C { a }.a.Str())    // prints 1
}

Field access and assignment

Fields can be read and assigned through any variable — field mutation is independent of whether the variable itself is mut:

class A { a: Int }

def main() {
    let a = new A { a: 1 }
    a.a = 2
    print(a.a.Str())    // 2
}

Instance methods

Defined with self as the first parameter. self must not have a type annotation:

class Point {
    x: Int,
    y: Int,
    def magnitude_sq(self) Int -> self.x * self.x + self.y * self.y
}

def main() {
    let p = new Point { x: 3, y: 4 }
    print(p.magnitude_sq().Str())    // 25
}

Static methods

Methods without a self parameter. Called on the class name, not on an instance:

class Counter {
    count: Int,
    def zero() Counter -> new Counter { count: 0 }
}

def main() {
    let c = Counter.zero()
}

Calling instance methods statically by passing the instance explicitly is also valid:

class S {
    def f(self, msg: Str) Str { return msg }
}
def main() {
    print(S.f(new S, "abc"))    // abc
}

Default parameters on methods

Same rules as for standalone functions:

class Greeter {
    def say(self, msg: Str = "Hello") {
        print(msg)
    }
}

def main() {
    new Greeter{}.say()        // Hello
    new Greeter{}.say("Hi")   // Hi
}

Static methods as first-class values

Static methods can be stored in variables and used as first-class functions. Instance methods cannot:

class S {
    def g() -> 1
}
def main() {
    let g = S.g
    print(g().Str())    // 1
}

Operator overloading

See Operators for full details:

class Foo {
    x: Int,
    def + (self, other: Foo) Foo {
        return new Foo { x: self.x + other.x }
    }
}

Class composition

class Inner { value: Int }
class Outer { inner: Inner, tag: Str }

def main() {
    let o = new Outer {
        inner: new Inner { value: 42 },
        tag: "hello"
    }
    print(o.inner.value.Str())    // 42
    print(o.tag)                  // hello
}

Generics

Generic classes

Declared with one or more type parameters in angle brackets:

class Box<T> {
    value: T
}

def main() {
    let b = new Box<Int> { value: 42 }
    print(b.value.Str())    // 42
}

Multiple type parameters:

class Pair<A, B> {
    first: A,
    second: B
}

def main() {
    let p = new Pair<Int, Str> { first: 1, second: "one" }
    print(p.first.Str())    // 1
    print(p.second)         // one
}

Type parameters are in scope for all field declarations and method bodies within the class. Providing the wrong concrete type for a generic field is a TypeError.

Generic methods on generic classes

Instance methods on a generic class can introduce their own additional type parameters:

class Wrapper<T> {
    data: T,
    def map<U>(self, f: Fn(T) U) Wrapper<U> ->
        new Wrapper<U> { data: f(self.data) }
}

def main() {
    let w = new Wrapper<Int> { data: 5 }
    let s = w.map!<Str>(fn (n: Int) Str -> n.Str())
    print(s.data)    // 5
}

A method-level type parameter must not duplicate the class-level parameter name — this is a TypeError.

Generic functions

def identity<T>(x: T) T -> x

def main() {
    print(identity!<Int>(42).Str())    // 42
    print(identity!<Str>("hello"))     // hello
}

Multiple type parameters:

def apply<T, A>(t: T, f: Fn(T) A) A -> f(t)

def main() {
    const x = 2
    print(apply!<Int, Int>(x, fn (n: Int) -> n + 1).Str())    // 3
}

Instantiation syntax

Generic functions and methods are called with !<Type> between the name and the argument list:

def main() {
    identity!<Int>(42)
    List.empty!<Int>()
    // list.map!<Str>(fn (n: Int, i: Int) -> n.Str())
}

Omitting the !<Type> on a generic function is a TypeError. Providing the wrong number of type arguments is also a TypeError.

First-class generic functions

Generic functions can be stored in variables and instantiated through the variable:

def f<T>(a: T) T -> a

def main() {
    let f_ = f
    print(f_!<Str>("abc"))    // abc
}

Nested generics and chaining

class Box<T> { value: T }

def main() {
    let nested = new Box<Box<Int>> {
        value: new Box<Int> { value: 1 }
    }
    print(nested.value.value.Str())    // 1
}

Chained generic method calls:

def main() {
    let result = List.empty!<Int>()
        .map!<Str>(fn (n: Int, i: Int) -> n.Str())
        .map!<Int>(fn (s: Str, i: Int) -> s.len())
}

Type parameters are not first-class

Passing an unknown type T to a function that does not declare it is an UnknownSymbol:

def a<T>(x: T) T { return x }
def main() {
    // a!<T>("")    // UnknownSymbol: T is not defined at the call site
}

Primitives

Primitives are types backed by a single 64-bit value rather than a heap-allocated struct. They are not stored as a reference.

The built-in primitives are Int, Bool, Char, and Ptr<T>. These are defined using the primitive keyword in the standard library. User code cannot define new primitive types — primitive MyType is a TypeError.

new Int produces 0. new Bool produces false. new Char produces the null character.

Int

64-bit signed integer. Range: −2⁶³ to 2⁶³−1.

Integer literals exceeding 2⁶³−1 produce a NumericOverflow error at compile time. Runtime overflow wraps around silently (two's complement):

print((9223372036854775807 + 1).Str())    // -9223372036854775808
MethodDescription
StrConvert to decimal string
Bool0false, any other value → true
max(other=MAX_INT)Return the larger of the two values
min(other=MIN_INT)Return the smaller of the two values
absAbsolute value
compare(other)Returns -1, 0, or 1
print((-5).abs().Str())      // 5
print(3.max(7).Str())        // 7
print(10.min(4).Str())       // 4
print(0.Bool().Str())        // false
print((-1).Bool().Str())     // true
print(Int.compare(1, 2).Str()) // -1

Bool

Boolean type. Literals are true and false.

Operators: && (AND), || (OR), ! (NOT). Neither && nor || short-circuits.

MethodDescription
Str"true" or "false"

Char

Single Unicode character. Single-quoted literals: 'a', 'Z', '💖'.

Escape sequences: '\n' (newline), '\t' (tab), '\r' (CR), '\\' (backslash), '\'' (single quote), '\"' (double quote).

MethodDescription
StrConvert to a one-character string
IntCode point as integer
from_int(i)Create Char from a code point (static)
is_digitTrue for '0''9'
is_alphabeticTrue for 'A''Z' or 'a''z'
is_uppercaseTrue for 'A''Z'
is_lowercaseTrue for 'a''z'
is_alphanumericTrue for alphabetic or digit
is_asciiTrue if code point < 128
is_whitespaceTrue for ' ', '\n', '\t', '\r'
print('A'.is_uppercase().Str())    // true
print('3'.is_digit().Str())        // true
print('a'.Int().Str())             // 97
print(Char.from_int(65).Str())     // A

Ptr<T>

An unmanaged pointer to a heap-allocated value. No garbage collection or reference counting. Use Ptr<T> only for low-level code — prefer Option<T> or Result<T, E> for safe values.

def main() {
    let p = Ptr.make!<Int>(42)
    print(p.unwrap().Str())     // 42
    print(p.is_null().Str())    // false
}

See Memory Management for full details.

typeof

typeof expr evaluates at compile time to a Str naming the type of the expression.

Syntax

typeof expr
typeof(expr)    // parentheses are optional

typeof always returns a Str. Since its result is a Str, typeof typeof x is valid and returns "Str".

Examples

print(typeof "abc")         // Str
print(typeof 2)             // Int
print(typeof true)          // Bool
print(typeof new Void)      // Void
print(typeof typeof Bool)   // Str

On classes and instances:

class C
print(typeof C)       // C
print(typeof new C)   // C

On functions and calls:

def a() {}
print(typeof a)      // Fn a() Void
print(typeof a())    // Void

Inside generic functions

typeof reflects the concrete type at the call site. Within the generic function body, typeof T prints the type parameter name as a string, not the concrete type:

def show<T>(a: T) T {
    print(typeof a, ",")     // prints "T"
    print(typeof T, ",")     // prints "T"
    return a
}
def main() {
    let int = show!<Int>(1)
    print(typeof int, ",")           // Int
    print(typeof show!<Str>("Hi"))   // Str
}
// prints T,T,Int,Str

Restrictions

The following are SyntaxError:

  • typeof alone with nothing after it
  • typeof 1 2 (two expressions)
  • typeof () (empty parentheses)
  • typeof while {} or typeof if true {} (control-flow as argument)

Using an undeclared name — typeof Foo where Foo is not defined — is an UnknownSymbol.

Standard Library

All standard library types are automatically available — no import needed.

Type / FunctionDescription
Int64-bit signed integer
BoolBoolean value
CharSingle Unicode character
StrImmutable string with rich methods
Utf8StrRaw UTF-8 string for OS interop
List<T>Dynamic resizable array
Option<T>Optional / nullable value
Result<T, E>Success-or-error value
RangeLazy integer sequence
Ptr<T>Unsafe heap pointer
range()Constructor for Range values
print()Print to stdout
input()Read a line from stdin
panic()Abort with an error message
exit()Terminate with an exit code

Str

Immutable string. Uses an internal UTF-64 encoding — each Unicode character occupies exactly 8 bytes, giving O(1) indexing by character position.

"hello"     // Str literal
""          // empty string
"line1\
line2"      // continuation: "line1line2"

Escape sequences: "\n", "\t", "\r", "\\", "\"".

MethodSignatureDescription
len(self) IntNumber of characters (not bytes)
at(self, i: Int) Option<Char>Character at index; supports negative indices
at_raw(self, i: Int) CharCharacter at index; unchecked
+(self, other: Str) StrConcatenate
contains(self, other: Str) BoolTrue if other appears as a substring
find(self, other: Str) IntIndex of first occurrence, or -1
starts_with(self, other: Str) BoolTrue if string begins with other
ends_with(self, other: Str) BoolTrue if string ends with other
substr(self, start=0, end=MAX_INT) StrSubstring (supports negative indices)
repeat(self, n: Int) StrRepeat n times
reversed(self) StrReversed copy
replace(self, search: Str, replace_with="", max=-1) StrReplace occurrences
insert(self, index: Int, other: Str) StrInsert at position
remove(self, index: Int, count=1) StrRemove characters
split(self, separator: Str) List<Str>Split into substrings
join(self, l: List<Str>) StrJoin list with self as separator
List(self) List<Char>Convert to list of characters
List_strings(self) List<Str>Convert to list of one-character strings
Int(self) Result<Int, Str>Parse as decimal integer
Utf8Str(self) Utf8StrConvert to raw UTF-8 string
utf8_size(self) IntSize in UTF-8 bytes
print("hello".len().Str())           // 5
print("hello".reversed())            // olleh
print("abc".at(-1).unwrap().Str())   // c
print("a,b,c".split(",").len().Str()) // 3
print(",".join(["a", "b", "c"]))     // a,b,c

Str.find returns -1 when not found. Searching for an empty string always returns 0.

Str.Int() returns Result<Int, Str>:

let r = "42".Int()
print(r.unwrap().Str())    // 42

let bad = "abc".Int()
print(bad.ok.Str())        // false

List<T>

Heap-allocated, resizable array.

let l = List.empty!<Int>()
let l2 = List.with_capacity!<Int>(10)
MethodDescription
lenNumber of elements
push(value)Append an element
popRemove and return the last element (Option<T>)
at(i)Element at index (safe; supports negative indices)
at_raw(i)Element at index (unchecked)
set_at(i, val)Set element at index (safe; returns Result)
remove_at(i)Remove and return element at index (Option<T>)
map<To>(f)Transform every element; callback receives (element, index)
filter(f)Retain elements matching predicate
sort(f)Sort with a comparator; returns negative/0/positive
cloneShallow copy
concat(other)New list with all elements of both
index_of(item, cmp?)First index where item matches
contains(item, cmp?)True if any element matches
def main() {
    let nums = List.empty!<Int>()
    nums.push(10)
    nums.push(20)
    nums.push(30)

    print(nums.len().Str())               // 3
    print(nums.at(0).unwrap().Str())      // 10
    print(nums.at(-1).unwrap().Str())     // 30

    let doubled = nums.map!<Int>(fn (n: Int, i: Int) -> n * 2)
    let big = nums.filter(fn (n: Int) -> n > 15)
    let sorted = nums.sort(fn (a: Int, b: Int) -> a.compare(b))
}

For Str or class instances, provide a comparator to index_of and contains:

let idx = strs.index_of("hello", fn (a: Str, b: Str) Bool -> a == b)

Option<T>

Wraps either a value (some) or nothing (none). The T? shorthand is exactly equivalent to Option<T>.

let present = Option.some!<Int>(42)
let absent  = Option.none!<Int>()

let a: Int? = Option.some!<Int>(5)
MethodDescription
is_someBool field — true if this holds a value
unwrapGet the value, or panic
or(default)Get the value, or return default
map<U>(f)Transform the value if present; propagates none
is_some_and(f)true if present and predicate holds
??None-coalescing: same as or
let x: Int? = Option.some!<Int>(5)
print(x.unwrap().Str())        // 5
print(x.or(0).Str())           // 5
print((x ?? 99).Str())         // 5

let y: Int? = Option.none!<Int>()
print(y.or(0).Str())           // 0
print((y ?? 99).Str())         // 99

Result<T, E>

Holds either a success value of type T or an error value of type E. The ok: Bool field distinguishes the two cases.

def main() {
    let success = Result.ok!<Int, Str>(42)
    let failure = Result.err!<Int, Str>("something went wrong")
}
MethodDescription
okBool field — true if success
unwrapReturn the success value, or panic
OptionConvert to Option<T> (discards the error)
errorReturn the error as Option<E>, or none if success
let r = Result.ok!<Int, Str>(42)
print(r.ok.Str())         // true
print(r.unwrap().Str())   // 42

let bad = Result.err!<Int, Str>("oops")
print(bad.ok.Str())       // false
print(bad.error().unwrap()) // oops

Range and range()

range(n)             // 0 to n (exclusive), step 1
range(start, end)    // start to end (exclusive), step 1
range(start, end, step)
for i in range(5) { print(i.Str()) }     // 01234
for i in range(2, 8, 2) { print(i.Str()) }  // 246
MethodDescription
lenNumber of elements in the range
at_rawValue at position i (0-based)
ListMaterialise as a List<Int>

Utf8Str

Raw null-terminated UTF-8 string. Used at OS boundaries (command-line arguments, file I/O). Not interchangeable with Str.

def main(args: List<Utf8Str>) {
    for arg in args {
        print(arg.Str())    // convert to Str before operations
    }
}

Convert: utf8_value.Str()Str; str_value.Utf8Str()Utf8Str.


I/O functions

print

def print(msg="", line_end="\n")

Writes msg to stdout followed by line_end. Default line_end is "\n".

print("Hello")          // Hello\n
print("Hello", "")      // Hello  (no newline)
print("item", ", ")     // item, 

input

def input(prompt="", buffer_size=1000) Str

Optionally prints prompt, then reads a line from stdin. Strips the trailing newline.

let name = input("Enter your name: ")
print("Hello, " + name + "!")

panic

def panic(msg="explicit panic")

Prints "PANIC: " + msg to stdout and exits with code 1.

exit

def exit(code=0)

Terminates the program with the given exit code. de. de.

Memory Management

Oxynium does not have a garbage collector or reference counting. Memory management is manual for heap-allocated values, but most programs never need to think about this — Str, List<T>, and other standard types manage their own memory internally.

The only explicit memory primitive is Ptr<T>.

Ptr<T>

primitive Ptr<T> — an unmanaged pointer to a heap-allocated value of type T. The programmer is fully responsible for memory.

Creating a pointer

let p = Ptr.make!<Int>(42)

This allocates 8 bytes on the heap, stores 42, and returns a pointer to that memory.

Methods

MethodSignatureDescription
makestatic <From>(val: From) Ptr<From>Allocate and store a value
unwrap(self) TDereference the pointer; no null check
is_null(self) BoolTrue if the address is 0 (null pointer)
Str(self) Str"Ptr(<address>)"
let p = Ptr.make!<Int>(42)
print(p.is_null().Str())    // false
print(p.unwrap().Str())     // 42

Null pointers

def main() {
    let null_ptr = #unchecked_cast(Ptr<Int>, 0)
    print(null_ptr.is_null().Str())    // true
    // calling null_ptr.unwrap() causes undefined behaviour
}

Safety

Ptr<T> bypasses the type system entirely. Dereferencing a null or invalid pointer is undefined behaviour. Prefer Option<T> or Result<T, E> for safe optional/fallible values. Use Ptr<T> only when interfacing with memory, system calls, or external C functions.

Unsafe type casting (#unchecked_cast)

#unchecked_cast(TargetType, expression)

The bits of expression are reinterpreted directly as TargetType. No conversion occurs. Equivalent to a C-style transmute.

Common uses:

#unchecked_cast(Ptr<Int>, 0)    // create a null pointer
#unchecked_cast(Int, my_ptr)    // pointer address as integer
#unchecked_cast(Int, 'A')       // 65

Completely unsafe. Misuse causes undefined behaviour. Prefer the safe conversion methods (Int.Bool(), Char.Int(), etc.) where possible.

Inline assembly (#asm)

For complete control over generated code, #asm embeds raw x86-64 NASM assembly:

#asm ReturnType "assembly string"

Must appear inside a function body. The assembly string must be a compile-time literal. For non-Void return types, the assembly must push the result onto the stack.

Parameters are available on the stack relative to rbp:

  • 1st parameter: [rbp + 16]
  • 2nd parameter: [rbp + 24]
  • nth parameter: [rbp + (8 + n*8)]
def asm_passthrough(arg: Str) Str {
    return #asm Str "
        push qword [rbp + 16]
    "
}
print(asm_passthrough("hi"))    // hi

Using #asm makes your code non-portable to architectures other than x86-64.

Examples

These are a few simple examples which showcase some of the language's features.

Hello, World!

print("Hello, World!")

Fibonacci

const N = 10;

def fib (n: Int) Int {
    if n <= 1 {
        return n;
    }
    return fib(n - 1) + fib(n - 2);
}

print(fib(N).Str()); // 55

FizzBuzz

def main() {
    for i in range(100) {
        print(fizzbuzz(i));
    }
}

def fizzbuzz (n: Int) Str {
    if n % 3 == 0 && n % 5 == 0 {
        return "FizzBuzz";
    }
    if n % 3 == 0 -> return "Fizz";
    if n % 5 == 0 -> return "Buzz";
    return n.Str();
}

Generic classes

class LoggedQueue<T> {
    backing_list: List<T>,
    
    def make_empty<S>()
        -> new LoggedQueue<S> { backing_list: List.empty!<S>() },
    
    def push(self, item: T) LoggedQueue<T> {
        self.backing_list.push(item);
        print("added item " + self.len().Str());
        return self;
    }
    
    def pop(self) T {
        if self.len() < 1 -> panic("pop on empty list");
        let val = self.backing_list.remove_at(0).unwrap();
        print("popped item " + self.len().Str());
        return val;
    }
    
    def len(self) ->
        self.backing_list.len()
}

def main() {
    let q = LoggedQueue.make_empty!<Int>()
                       .push(5)
                       .push(6);
    
    print("process: " + q.pop().Str()); // 5
    print("then: " + q.pop().Str()); // 6
}