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 kind | When it occurs |
|---|---|
SyntaxError | The source text cannot be parsed |
TypeError | A type rule is violated |
UnknownSymbol | A name is used that has not been declared |
IoError | An external function is declared but not linked |
NumericOverflow | An 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.
| Operator | Example | Equivalent |
|---|---|---|
+= | a += 3 | a = a + 3 |
-= | a -= 3 | a = a - 3 |
*= | a *= 3 | a = a * 3 |
/= | a /= 3 | a = a / 3 |
%= | a %= 3 | a = 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
| Type | Description | Default (new T) | Size |
|---|---|---|---|
Int | 64-bit signed integer | 0 | 8 bytes |
Bool | Boolean | false | 8 bytes |
Char | Single Unicode character | null char (0) | 8 bytes |
Str | Immutable character string | "" (empty) | pointer |
Void | Unit / 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.
| Operator | Description | Example | Result |
|---|---|---|---|
+ | Addition | 3 + 4 | 7 |
- | Subtraction | 10 - 3 | 7 |
* | Multiplication | 3 * 4 | 12 |
/ | Integer division | 7 / 2 | 3 |
% | Modulo | 7 % 2 | 1 |
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
| Operator | Description | Example | Result |
|---|---|---|---|
- | Arithmetic negation | -5 | -5 |
! | Logical NOT | !true | false |
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.
| Operator | Description |
|---|---|
== | Equal |
!= | Not equal |
< | Less than |
> | Greater than |
<= | Less than or equal |
>= | Greater than or equal |
Comparison operators are not chainable — 1 < 2 < 3 is a TypeError because
the result of 1 < 2 is Bool, and Bool < Int is not defined.
Logical operators
| Operator | Description | Operand types | Return type |
|---|---|---|---|
&& | Logical AND | Bool, Bool | Bool |
\|\| | Logical OR | Bool, Bool | Bool |
! | Logical NOT | Bool | Bool |
&& 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)
| Level | Operators | Associativity |
|---|---|---|
| 1 | unary -, !, typeof | right |
| 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
| Method | Description |
|---|---|
Str | Convert to decimal string |
Bool | 0 → false, 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 |
abs | Absolute 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.
| Method | Description |
|---|---|
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).
| Method | Description |
|---|---|
Str | Convert to a one-character string |
Int | Code point as integer |
from_int(i) | Create Char from a code point (static) |
is_digit | True for '0'–'9' |
is_alphabetic | True for 'A'–'Z' or 'a'–'z' |
is_uppercase | True for 'A'–'Z' |
is_lowercase | True for 'a'–'z' |
is_alphanumeric | True for alphabetic or digit |
is_ascii | True if code point < 128 |
is_whitespace | True 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:
typeofalone with nothing after ittypeof 1 2(two expressions)typeof ()(empty parentheses)typeof while {}ortypeof 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 / Function | Description |
|---|---|
Int | 64-bit signed integer |
Bool | Boolean value |
Char | Single Unicode character |
Str | Immutable string with rich methods |
Utf8Str | Raw UTF-8 string for OS interop |
List<T> | Dynamic resizable array |
Option<T> | Optional / nullable value |
Result<T, E> | Success-or-error value |
Range | Lazy 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", "\\", "\"".
| Method | Signature | Description |
|---|---|---|
len | (self) Int | Number of characters (not bytes) |
at | (self, i: Int) Option<Char> | Character at index; supports negative indices |
at_raw | (self, i: Int) Char | Character at index; unchecked |
+ | (self, other: Str) Str | Concatenate |
contains | (self, other: Str) Bool | True if other appears as a substring |
find | (self, other: Str) Int | Index of first occurrence, or -1 |
starts_with | (self, other: Str) Bool | True if string begins with other |
ends_with | (self, other: Str) Bool | True if string ends with other |
substr | (self, start=0, end=MAX_INT) Str | Substring (supports negative indices) |
repeat | (self, n: Int) Str | Repeat n times |
reversed | (self) Str | Reversed copy |
replace | (self, search: Str, replace_with="", max=-1) Str | Replace occurrences |
insert | (self, index: Int, other: Str) Str | Insert at position |
remove | (self, index: Int, count=1) Str | Remove characters |
split | (self, separator: Str) List<Str> | Split into substrings |
join | (self, l: List<Str>) Str | Join 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) Utf8Str | Convert to raw UTF-8 string |
utf8_size | (self) Int | Size 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)
| Method | Description |
|---|---|
len | Number of elements |
push(value) | Append an element |
pop | Remove 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 |
clone | Shallow 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)
| Method | Description |
|---|---|
is_some | Bool field — true if this holds a value |
unwrap | Get 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")
}
| Method | Description |
|---|---|
ok | Bool field — true if success |
unwrap | Return the success value, or panic |
Option | Convert to Option<T> (discards the error) |
error | Return 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
| Method | Description |
|---|---|
len | Number of elements in the range |
at_raw | Value at position i (0-based) |
List | Materialise 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
| Method | Signature | Description |
|---|---|---|
make | static <From>(val: From) Ptr<From> | Allocate and store a value |
unwrap | (self) T | Dereference the pointer; no null check |
is_null | (self) Bool | True 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
}