Skip to content

A tour of Oxie

This tour introduces the Oxie programming language by example. Every program here runs as-is: save it as tour.oxi and run oxie run tour.oxi. If you know Kotlin or TypeScript you can read most Oxie code already; the notes point out where Oxie follows Rust instead.

fun main() {
val name = "World"
println("Hello, $name!")
}
fn main() {
let name = "World";
println!("Hello, {name}!");
}
Hello, World!

fun main is the entry point. Statements end at the end of the line; ; is optional.

fun main() {
val answer = 42 // immutable (Rust `let`)
var count = 0 // mutable (Rust `let mut`)
count += 1
val pi: f64 = 3.14159 // explicit type
val big: Long = 10_000_000L // Kotlin names work too: Int, Long, Double, Boolean, Char
val flag = true
val letter = 'x'
val (q, r) = (17 / 5, 17 % 5) // destructuring
println("$answer $count $pi $big $flag $letter $q $r")
}
fn main() {
let answer = 42;
let mut count = 0;
count += 1;
let pi: f64 = 3.14159;
let big: i64 = 10_000_000i64;
let flag = true;
let letter = 'x';
let (q, r) = (17 / 5, 17 % 5);
println!("{answer} {count} {pi} {big} {flag} {letter} {q} {r}");
}
42 1 3.14159 10000000 true x 3 2

The Rust primitive types (i32, u64, f64, bool, char, usize, …) are the native names; Int, Long, Double, Float, Boolean, Char, Unit are accepted as aliases. Integer literals default to i32, floats to f64, as in Rust.

fun add(a: i32, b: i32): i32 {
return a + b
}
// expression body; the return type is inferred when it is obvious
fun square(x: i32) = x * x
// the last expression of a block is its value, like Rust
fun max(a: i32, b: i32): i32 {
if (a > b) a else b
}
// default and named arguments
fun greet(name: &str, greeting: &str = "Hello"): String = "$greeting, $name!"
fun main() {
println(add(2, 3))
println(square(7))
println(max(4, 9))
println(greet("Ada"))
println(greet(name = "Bob", greeting = "Hi"))
}
pub(crate) fn add(a: i32, b: i32) -> i32 {
a + b
}
pub(crate) fn square(x: i32) -> i32 {
x * x
}
pub(crate) fn max(a: i32, b: i32) -> i32 {
if a > b {
a
} else {
b
}
}
pub(crate) fn greet(name: &str, greeting: &str) -> String {
format!("{greeting}, {name}!")
}
fn main() {
println!("{}", add(2, 3));
println!("{}", square(7));
println!("{}", max(4, 9));
println!("{}", greet("Ada", "Hello"));
println!("{}", greet("Bob", "Hi"));
}
5
49
9
Hello, Ada!
Hi, Bob!

Parameters are written name: Type, the return type after : (Rust’s -> also works).

fun main() {
val name = "Oxie"
val version = 1.25
println("$name ${version:.1}") // format spec after `:` (Rust format syntax)
println("next: ${version + 0.75}")
println("debug: ${listOf(1, 2):?}") // `:?` is Debug formatting
val greeting = "Hello, " + name + "!" // `+` with strings builds a String
println(greeting)
val poem = """
Roses are red,
$name is new.
""" // indentation is trimmed automatically
println(poem)
println("a dollar: \$5")
}
fn main() {
let name = "Oxie";
let version = 1.25;
println!("{name} {version:.1}");
println!("next: {}", version + 0.75);
println!("debug: {:?}", vec![1, 2]);
let greeting = format!("Hello, {}!", name);
println!("{}", greeting);
let poem = format!("Roses are red,\n {name} is new.");
println!("{}", poem);
println!("a dollar: $5");
}
Oxie 1.2
next: 2
debug: [1, 2]
Hello, Oxie!
Roses are red,
Oxie is new.
a dollar: $5

String literals are &str (borrowed, free). Templates and + produce String. Where a String is expected, a literal or &str is converted for you.

if and when are expressions.

fun describe(n: i32): &str = when {
n < 0 -> "negative"
n == 0 -> "zero"
n < 10 -> "small"
else -> "large"
}
fun main() {
val n = 7
val parity = if (n % 2 == 0) "even" else "odd"
println("$n is $parity and ${describe(n)}")
val kind = when (n) {
0 -> "zero"
1, 2, 3 -> "tiny"
in 4..=9 -> "single digit"
x if x < 0 -> "negative"
else -> "big"
}
println(kind)
for (i in 0 until 3) print("$i ") // 0 1 2
for (i in 10 downTo 0 step 5) print("$i ") // 10 5 0
for (i in 1..=3) print("$i ") // Rust ranges: `..` excludes the end, `..=` includes it
println()
var i = 0
while (i < 3) i += 1
do {
i -= 1
} while (i > 0)
val found = loop {
i += 7
if (i > 20) break i
}
println("found $found")
}
pub(crate) fn describe(n: i32) -> &'static str {
if n < 0 {
"negative"
} else if n == 0 {
"zero"
} else if n < 10 {
"small"
} else {
"large"
}
}
fn main() {
let n = 7;
let parity = if n % 2 == 0 {
"even"
} else {
"odd"
};
println!("{n} is {parity} and {}", describe(n));
let kind = match n {
0 => "zero",
1 | 2 | 3 => "tiny",
4..=9 => "single digit",
x if x < 0 => "negative",
_ => "big",
};
println!("{}", kind);
for i in 0..3 {
print!("{i} ");
}
for i in (0..=10).rev().step_by(5 as usize) {
print!("{i} ");
}
for i in 1..=3 {
print!("{i} ");
}
println!();
let mut i = 0;
while i < 3 {
i += 1;
}
loop {
{
i -= 1;
}
if !(i > 0) {
break;
}
}
let found = loop {
i += 7;
if i > 20 {
break i;
}
};
println!("found {found}");
}
7 is odd and small
single digit
0 1 2 10 5 0 1 2 3
found 21

when with a subject becomes a Rust match; patterns are Rust patterns (Some(x), (a, b), 1..=9, x if cond). Without a subject it is an if/else-if chain. Conditions are written in parentheses as in Kotlin; Rust’s style without them works too.

import std::collections::HashMap
fun main() {
val nums = listOf(5, 3, 8, 1) // Vec<i32>; `vec[5, 3, 8, 1]` is the same
val doubled = nums.map { it * 2 } // a new list: [10, 6, 16, 2]
val big = nums.count { it > 3 }
val sum = nums.iter().fold(0) { acc, x -> acc + x }
println("${doubled:?} $big $sum ${nums.sorted():?}")
val ages = mapOf("ada" to 36, "bob" to 25) // HashMap
println("ada is ${ages["ada"]}")
var counts = HashMap()
for (word in "a b a c a".split(' ')) {
*counts.entry(word).or_insert(0) += 1
}
println("a: ${counts["a"]}")
val add = (a: i32, b: i32) => a + b // TypeScript-style arrow function
val twice = { x: i32 -> x * 2 } // Kotlin-style lambda
println("${add(2, 3)} ${twice(4)}")
}
use std::collections::HashMap;
fn main() {
let nums = vec![5, 3, 8, 1];
let doubled = nums.map(|it| it * 2);
let big = nums.count(|it| crate::__oxie::val(it) > 3);
let sum = nums.iter().fold(0, |acc, x| acc + x);
println!("{doubled:?} {big} {sum} {:?}", nums.sorted());
let ages = ::std::collections::HashMap::from([("ada", 36), ("bob", 25)]);
println!("ada is {}", ages["ada"]);
let mut counts = HashMap::new();
for word in "a b a c a".split(' ') {
*counts.entry(word).or_insert(0) += 1;
}
println!("a: {}", counts["a"]);
let add = |a: i32, b: i32| a + b;
let twice = |x: i32| x * 2;
println!("{} {}", add(2, 3), twice(4));
}
[10, 6, 16, 2] 2 17 [1, 3, 5, 8]
ada is 36
a: 3
5 8

A lambda without parameters can use it. A lambda as the last argument goes outside the parentheses (xs.fold(0) { acc, x -> ... }).

Lists, arrays and slices have Kotlin’s collection operations, each returning a new collection: map, filter, filter_map, flat_map, sorted, sorted_by, reversed, distinct, group_by, associate_by, partition, chunked, sum, sum_of, max_of, count, any, all, none, find, join_to_string and more (Rust names, so sumOf is sum_of). They compile to the iterator chain you would write by hand, such as iter().map(..).collect(). For longer pipelines, Rust’s iterators are there too: nums.iter().filter { it > 3 }.map { it * 10 }.

data class User(name: String, age: u32, city: String)
fun main() {
val users = vec[User("Ada", 36, "London"), User("Linus", 28, "Helsinki"), User("Grace", 45, "London")]
println(users.filter { it.age > 30 }.map { it.name.clone() }.join_to_string(", "))
println("total age ${users.sum_of { it.age }}, oldest ${users.max_by_key { it.age }?.name ?: "-"}")
println(users.sorted_by { it.age }.join_with(" < ") { it.name.as_str() })
val by_city = users.group_by { it.city.clone() }
println("London: ${by_city["London"].len()}")
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct User {
pub(crate) name: String,
pub(crate) age: u32,
pub(crate) city: String,
}
impl User {
#[allow(dead_code)]
pub(crate) fn new(name: String, age: u32, city: String) -> Self {
Self {
name,
age,
city,
}
}
}
fn main() {
let users = vec![User::new(String::from("Ada"), 36, String::from("London")), User::new(String::from("Linus"), 28, String::from("Helsinki")), User::new(String::from("Grace"), 45, String::from("London"))];
println!("{}", users.filter(|it| it.age > 30).map(|it| it.name.clone()).join_to_string(", "));
println!("total age {}, oldest {}", users.sum_of(|it| it.age), crate::__oxie::ElvisOr::or_val(users.max_by_key(|it| it.age).map(|__oxie_v1| &__oxie_v1.name), "-"));
println!("{}", users.sorted_by(|it| it.age).join_with(" < ", |it| it.name.as_str()));
let by_city = users.group_by(|it| it.city.clone());
println!("London: {}", by_city["London"].len());
}
Ada, Grace
total age 109, oldest Grace
Linus < Ada < Grace
London: 2

In filter, find, any, count and the other predicates, the element is a reference, as in Rust; comparing it with a literal (it > 3, it == 'a', it % 2 == 0) reads the value for you.

T? is Option<T>; null is None.

data class Address(city: String)
data class Person(name: String, address: Address?)
fun find(people: &[Person], name: &str): &Person? = people.iter().find { it.name == name }
fun main() {
val people = listOf(Person("Ada", Address("London")), Person("Bob", null))
val city = find(&people, "Ada")?.address?.city // safe navigation
println("${city:?}")
val len = find(&people, "Zed")?.name?.len() ?: 0 // elvis: default when null
println(len)
if (val p = find(&people, "Bob")) { // unwraps (Rust `if let Some(p)`)
println("found ${p.name}")
}
find(&people, "Ada")?.let { println("hello ${it.name}") }
var x: i32? = null
println(x.is_none())
x = 5 // wrapped in Some automatically
println("${x!! + 1}") // `!!` unwraps (panics on null)
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct Address {
pub(crate) city: String,
}
impl Address {
#[allow(dead_code)]
pub(crate) fn new(city: String) -> Self {
Self {
city,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub(crate) struct Person {
pub(crate) name: String,
pub(crate) address: Option<Address>,
}
impl Person {
#[allow(dead_code)]
pub(crate) fn new(name: String, address: Option<Address>) -> Self {
Self {
name,
address,
}
}
}
pub(crate) fn find<'a>(people: &'a [Person], name: &'a str) -> Option<&'a Person> {
people.iter().find(|it| it.name == name)
}
fn main() {
let people = vec![Person::new(String::from("Ada"), Some(Address::new(String::from("London")))), Person::new(String::from("Bob"), None)];
let city = find(&people, "Ada").and_then(|__oxie_v1| __oxie_v1.address.as_ref()).map(|__oxie_v2| &__oxie_v2.city);
println!("{city:?}");
let len = crate::__oxie::ElvisOr::or_val(find(&people, "Zed").map(|__oxie_v3| __oxie_v3.name.len()), 0);
println!("{}", len);
if let Some(p) = find(&people, "Bob") {
println!("found {}", p.name);
}
if let Some(it) = find(&people, "Ada") {
println!("hello {}", it.name);
}
let mut x: Option<i32> = None;
println!("{}", x.is_none());
x = Some(5);
println!("{}", x.unwrap() + 1);
}
Some("London")
0
found Bob
hello Ada
true
6

a ?: b evaluates b only when a is null (or an error). The right side can also be return, throw, break or continue.

class Counter(var count: i32 = 0, step: i32 = 1) {
var history: Vec<i32> = vec[]
fun increment() { // changes fields, so it takes `&mut self` automatically
count += step
history.push(count)
}
fun current(): i32 = count // only reads: `&self`
companion object {
fun starting_at(n: i32): Counter = Counter(n) // static
}
}
data class Point(x: f64, y: f64) {
fun distance_to(other: Point): f64 = ((x - other.x).powi(2) + (y - other.y).powi(2)).sqrt()
}
fun main() {
var c = Counter(step = 5)
c.increment()
c.increment()
println("${c.current()} ${c.history:?}")
println(Counter.starting_at(10).current())
val p = Point(0.0, 0.0)
val q = p.copy(x = 3.0, y = 4.0)
println("${q:?} ${p.distance_to(q)} ${p == Point(0.0, 0.0)}")
}
pub(crate) struct Counter {
pub(crate) count: i32,
pub(crate) step: i32,
pub(crate) history: Vec<i32>,
}
impl Counter {
#[allow(dead_code)]
pub(crate) fn new(count: i32, step: i32) -> Self {
Self {
count,
step,
history: vec![],
}
}
pub(crate) fn increment(&mut self) {
self.count += self.step;
self.history.push(self.count);
}
pub(crate) fn current(&self) -> i32 {
self.count
}
pub(crate) fn starting_at(n: i32) -> Counter {
Counter::new(n, 1)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Point {
pub(crate) x: f64,
pub(crate) y: f64,
}
impl Point {
#[allow(dead_code)]
pub(crate) fn new(x: f64, y: f64) -> Self {
Self {
x,
y,
}
}
pub(crate) fn distance_to(&self, other: Point) -> f64 {
((self.x - other.x).powi(2) + (self.y - other.y).powi(2)).sqrt()
}
}
fn main() {
let mut c = Counter::new(0, 5);
c.increment();
c.increment();
println!("{} {:?}", c.current(), c.history);
println!("{}", Counter::starting_at(10).current());
let p = Point::new(0.0, 0.0);
let q = { let mut __oxie_copy1 = p.clone(); __oxie_copy1.x = 3.0; __oxie_copy1.y = 4.0; __oxie_copy1 };
println!("{q:?} {} {}", p.distance_to(q), p == Point::new(0.0, 0.0));
}
10 [5, 10]
10
Point { x: 3.0, y: 4.0 } 5 true
  • Constructor parameters are fields. Call the constructor like a function: Counter().
  • Inside methods, fields and methods are used without this. (write this.x when a parameter has the same name).
  • Oxie decides how each method receives the object: &self if it only reads, &mut self if it assigns a field or calls something that does, and static if it never uses the object and returns Self. Write mut fun, static fun or an explicit self parameter to choose.
  • data class derives Debug, Clone, PartialEq — plus Copy, Eq and Hash when all fields allow it — and has copy(field = value).
  • init { } runs after the fields are set; body properties need a default value.

A plain class value has one owner, like every Rust value. When several parts of a program need the same object — a graph node, an account two customers share, application state — declare it shared. Copying a shared value copies a handle to the object, as in Kotlin, Java or TypeScript:

shared class Account(var owner: String, var balance: i64 = 0) {
var history: Vec<i64> = vec[]
fun deposit(amount: i64) {
balance += amount
history.push(amount)
}
}
fun main() {
val a = Account("Ada")
val b = a // the same account
b.deposit(50)
a.balance += 5 // fields can be read and changed through any handle
a.history.push(5)
println("${a.balance} ${b.history:?} ${a == b}")
}
pub(crate) struct AccountData {
pub(crate) owner: String,
pub(crate) balance: i64,
pub(crate) history: Vec<i64>,
}
impl AccountData {
#[allow(dead_code)]
pub(crate) fn new(owner: String, balance: i64) -> Self {
Self {
owner,
balance,
history: vec![],
}
}
pub(crate) fn deposit(&mut self, amount: i64) {
self.balance += amount;
self.history.push(amount);
}
}
pub(crate) struct Account(pub(crate) ::std::rc::Rc<::std::cell::RefCell<AccountData>>);
impl ::std::clone::Clone for Account {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl ::std::cmp::PartialEq for Account {
fn eq(&self, other: &Self) -> bool {
::std::rc::Rc::ptr_eq(&self.0, &other.0)
}
}
impl ::std::cmp::Eq for Account {}
impl ::std::hash::Hash for Account {
fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) {
(::std::rc::Rc::as_ptr(&self.0) as *const () as usize).hash(state)
}
}
impl Account {
#[allow(dead_code)]
pub(crate) fn new(owner: String, balance: i64) -> Self {
Self(::std::rc::Rc::new(::std::cell::RefCell::new(AccountData::new(owner, balance))))
}
#[allow(dead_code)]
pub(crate) fn owner(&self) -> String {
::std::clone::Clone::clone(&self.0.borrow().owner)
}
#[allow(dead_code)]
pub(crate) fn set_owner(&self, value: String) {
self.0.borrow_mut().owner = value;
}
#[allow(dead_code)]
pub(crate) fn balance(&self) -> i64 {
::std::clone::Clone::clone(&self.0.borrow().balance)
}
#[allow(dead_code)]
pub(crate) fn set_balance(&self, value: i64) {
self.0.borrow_mut().balance = value;
}
#[allow(dead_code)]
pub(crate) fn history(&self) -> Vec<i64> {
::std::clone::Clone::clone(&self.0.borrow().history)
}
#[allow(dead_code)]
pub(crate) fn set_history(&self, value: Vec<i64>) {
self.0.borrow_mut().history = value;
}
#[allow(dead_code)]
pub(crate) fn deposit(&self, amount: i64) {
self.0.borrow_mut().deposit(amount)
}
}
fn main() {
let a = Account::new(String::from("Ada"), 0);
let b = a.clone();
b.deposit(50);
{ let __value = 5; a.0.borrow_mut().balance += __value; };
a.0.borrow_mut().history.push(5);
println!("{} {:?} {}", a.balance(), b.history(), a == b);
}
55 [50, 5] true
  • A shared class is a Rc<RefCell<..>> handle; @sync shared class uses Arc<Mutex<..>> so handles can be sent to other threads.
  • Methods and handles never need var: changes go to the shared object.
  • == compares identity (the same object), and handles work as HashMap keys.
  • Reading a whole field (val h = a.history) copies it; a.history.len() and a.history.push(x) work on the object itself.
  • Each method call borrows the object once for the whole call, so methods can call each other freely. A method that reaches the same object again through another handle (a node whose next is itself) panics with “already borrowed” instead of corrupting memory.

A lambda that should keep its own handle lists it in a capture list: { [hits] -> ... } clones the handle into the lambda (and makes it move), which is how threads and callbacks get one:

@sync shared class Stats(var hits: i32 = 0)
fun main() {
val stats = Stats()
val workers = (1..=3).map { n -> std.thread.spawn { [stats] -> stats.hits += n } }.collect::<Vec<_>>()
for (w in workers) w.join().unwrap()
println(stats.hits) // 6
}
pub(crate) struct StatsData {
pub(crate) hits: i32,
}
impl StatsData {
#[allow(dead_code)]
pub(crate) fn new(hits: i32) -> Self {
Self {
hits,
}
}
}
pub(crate) struct Stats(pub(crate) ::std::sync::Arc<::std::sync::Mutex<StatsData>>);
impl ::std::clone::Clone for Stats {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl ::std::cmp::PartialEq for Stats {
fn eq(&self, other: &Self) -> bool {
::std::sync::Arc::ptr_eq(&self.0, &other.0)
}
}
impl ::std::cmp::Eq for Stats {}
impl ::std::hash::Hash for Stats {
fn hash<H: ::std::hash::Hasher>(&self, state: &mut H) {
(::std::sync::Arc::as_ptr(&self.0) as *const () as usize).hash(state)
}
}
impl Stats {
#[allow(dead_code)]
pub(crate) fn new(hits: i32) -> Self {
Self(::std::sync::Arc::new(::std::sync::Mutex::new(StatsData::new(hits))))
}
#[allow(dead_code)]
pub(crate) fn hits(&self) -> i32 {
::std::clone::Clone::clone(&self.0.lock().unwrap().hits)
}
#[allow(dead_code)]
pub(crate) fn set_hits(&self, value: i32) {
self.0.lock().unwrap().hits = value;
}
}
fn main() {
let stats = Stats::new(0);
let workers = (1..=3).map(|n| std::thread::spawn({
let stats = stats.clone();
move || { let __value = n; stats.0.lock().unwrap().hits += __value; }
})).collect::<Vec<_>>();
for w in workers {
w.join().unwrap();
}
println!("{}", stats.hits());
}
6
enum Shape {
Circle(radius: f64)
Rect(w: f64, h: f64)
Empty
fun area(): f64 = when (this) {
Circle(r) -> 3.14159 * r * r // variants can be used without `Shape.`
Rect(w, h) -> w * h
Empty -> 0.0
}
}
enum Direction { North, East, South, West }
fun main() {
val shapes = listOf(Shape.Circle(1.0), Shape.Rect(2.0, 3.0), Shape.Empty)
for (s in &shapes) println("${s:?} has area ${s.area():.2}")
println(Direction.North == Direction.East)
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Shape {
Circle { radius: f64 },
Rect { w: f64, h: f64 },
Empty,
}
impl Shape {
pub(crate) fn area(&self) -> f64 {
match self {
Shape::Circle { radius: r } => 3.14159 * r * r,
Shape::Rect { w, h } => w * h,
Shape::Empty => 0.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum Direction {
North,
East,
South,
West,
}
fn main() {
let shapes = vec![Shape::Circle { radius: 1.0 }, Shape::Rect { w: 2.0, h: 3.0 }, Shape::Empty];
for s in &shapes {
println!("{s:?} has area {:.2}", s.area());
}
println!("{}", Direction::North == Direction::East);
}
Circle { radius: 1.0 } has area 3.14
Rect { w: 2.0, h: 3.0 } has area 6.00
Empty has area 0.00
false

Variants can carry named fields Circle(radius: f64), positional fields Pair(i32, i32), or nothing. Enums derive Debug, Clone, PartialEq (and Copy/Eq/Hash when possible).

interface Animal {
fun name(): String
fun sound(): String
fun speak(): String = "${name()} says ${sound()}" // default method
}
class Dog : Animal {
override fun name() = "Dog"
override fun sound() = "woof"
}
class Cat(name: String) : Animal {
override fun name() = name.clone()
override fun sound() = "meow"
}
fun introduce(a: &Animal) = println(a.speak()) // `&dyn Animal`
fun main() {
val zoo: Vec<Box<Animal>> = vec[Box.new(Dog()), Box.new(Cat("Tom".to_string()))]
for (a in &zoo) introduce(a.as_ref())
}
pub(crate) trait Animal {
fn name(&self) -> String;
fn sound(&self) -> String;
fn speak(&self) -> String {
format!("{} says {}", self.name(), self.sound())
}
}
pub(crate) struct Dog;
impl Dog {
#[allow(dead_code)]
pub(crate) fn new() -> Self {
Self
}
}
impl Animal for Dog {
fn name(&self) -> String {
String::from("Dog")
}
fn sound(&self) -> String {
String::from("woof")
}
}
pub(crate) struct Cat {
pub(crate) name: String,
}
impl Cat {
#[allow(dead_code)]
pub(crate) fn new(name: String) -> Self {
Self {
name,
}
}
}
impl Animal for Cat {
fn name(&self) -> String {
self.name.clone()
}
fn sound(&self) -> String {
String::from("meow")
}
}
pub(crate) fn introduce(a: &dyn Animal) {
println!("{}", a.speak());
}
fn main() {
let zoo: Vec<Box<dyn Animal>> = vec![Box::new(Dog::new()), Box::new(Cat::new("Tom".to_string()))];
for a in &zoo {
introduce(a.as_ref());
}
}
Dog says woof
Tom says meow

An interface is a Rust trait. Used as a type it becomes a trait object behind a reference or Box (&Animal → &dyn Animal), or impl Animal for a parameter passed by value. : Display or any other trait works the same way; for derivable traits (: Hash, : Ord, …) Oxie adds a derive. Rust’s impl Trait for Type { } blocks work too.

fun str.shout(): String = self.to_uppercase() + "!"
fun i32.is_even(): bool = this % 2 == 0
data class Vec2(x: f64, y: f64) {
operator fun plus(o: Vec2): Vec2 = Vec2(x + o.x, y + o.y)
operator fun times(k: f64): Vec2 = Vec2(x * k, y * k)
override fun toString() = "($x, $y)" // implements Display
}
fun main() {
println("hey".shout())
println(4.is_even())
println(Vec2(1.0, 2.0) + Vec2(3.0, 4.0) * 2.0)
}
#[allow(non_camel_case_types)]
pub(crate) trait __OxieExt_shout {
fn shout(&self) -> String;
}
impl __OxieExt_shout for str {
fn shout(&self) -> String {
format!("{}!", self.to_uppercase())
}
}
#[allow(non_camel_case_types)]
pub(crate) trait __OxieExt_is_even {
fn is_even(&self) -> bool;
}
impl __OxieExt_is_even for i32 {
fn is_even(&self) -> bool {
self % 2 == 0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Vec2 {
pub(crate) x: f64,
pub(crate) y: f64,
}
impl Vec2 {
#[allow(dead_code)]
pub(crate) fn new(x: f64, y: f64) -> Self {
Self {
x,
y,
}
}
}
impl ::std::ops::Add for Vec2 {
type Output = Vec2;
#[inline]
fn add(self, o: Vec2) -> Vec2 {
Vec2::new(self.x + o.x, self.y + o.y)
}
}
impl ::std::ops::Mul<f64> for Vec2 {
type Output = Vec2;
#[inline]
fn mul(self, k: f64) -> Vec2 {
Vec2::new(self.x * k, self.y * k)
}
}
impl ::std::fmt::Display for Vec2 {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
write!(f, "({}, {})", self.x, self.y)
}
}
fn main() {
println!("{}", "hey".shout());
println!("{}", 4.is_even());
println!("{}", Vec2::new(1.0, 2.0) + Vec2::new(3.0, 4.0) * 2.0);
}
HEY!
true
(7, 10)

Operator names follow Kotlin: plus, minus, times, div, rem, unaryMinus, not, plusAssign, … (Rust names like add work too).

error enum ConfigError {
Missing(key: String) = "missing key: $key"
Invalid(key: String, value: String) = "invalid value '$value' for $key"
}
fun port(text: &str): u16 throws ConfigError {
if (text.is_empty()) throw ConfigError.Missing("port".to_string())
text.parse().map_err { ConfigError.Invalid("port".to_string(), text.to_string()) }?
}
fun main() {
println(port("8080").unwrap())
when (val r = port("eighty")) {
Ok(p) -> println("port $p")
Err(e) -> println("error: $e")
}
val fallback = port("") ?: 80
println(fallback)
}
#[derive(Debug)]
pub(crate) enum ConfigError {
Missing { key: String },
Invalid { key: String, value: String },
}
impl ::std::fmt::Display for ConfigError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match self {
Self::Missing { key } => write!(f, "missing key: {key}"),
Self::Invalid { key, value } => write!(f, "invalid value '{value}' for {key}"),
}
}
}
impl ::std::error::Error for ConfigError {}
pub(crate) fn port(text: &str) -> ::std::result::Result<u16, ConfigError> {
if text.is_empty() {
return Err(ConfigError::Missing { key: "port".to_string() }.into());
}
Ok(text.parse().map_err(|_| ConfigError::Invalid { key: "port".to_string(), value: text.to_string() })?)
}
fn main() {
println!("{}", port("8080").unwrap());
{
let r = port("eighty");
match r {
Ok(p) => {
println!("port {p}");
}
Err(e) => {
println!("error: {e}");
}
}
}
let fallback = crate::__oxie::ElvisOr::or_val(port(""), 80);
println!("{}", fallback);
}
8080
error: invalid value 'eighty' for port
80

throws turns the return type into a Result; returned values are wrapped in Ok, throw returns an error, and ? propagates one. Read Error handling for try/catch, context, AnyError and main() throws.

Every .oxi file in src/ is a module named after the file (src/net/http.oxi is net::http). Import with import:

import std::collections::{HashMap, HashSet}
import std.fs // dots work in paths too
import { Deserialize, Serialize } from serde
import model::user::User // your own module
use std::collections::{HashMap, HashSet};
use std::fs;
use serde::{Deserialize, Serialize};
use model::user::User;

Declarations are visible in the whole project by default. Mark them private to keep them in their file, or pub to export them from a library. Members of a pub class are pub too.

Oxie keeps Rust’s ownership model, so &, &mut, moves and clone() work exactly as in Rust:

fun total(xs: &[i32]): i32 = xs.iter().sum()
fun add_one(xs: &mut Vec<i32>) = xs.push(1)
fun main() {
var v = vec[1, 2, 3]
add_one(v) // the parameter is `&mut`, so `v` is lent mutably: add_one(&mut v)
println(total(v)) // ... and here lent for reading: total(&v)
val w = v.clone() // explicit copy
val moved = v // `v` can't be used after this
println("${w:?} ${moved:?}")
}
pub(crate) fn total(xs: &[i32]) -> i32 {
xs.iter().sum()
}
pub(crate) fn add_one(xs: &mut Vec<i32>) {
xs.push(1);
}
fn main() {
let mut v = vec![1, 2, 3];
add_one(&mut v);
println!("{}", total(&v));
let w = v.clone();
let moved = v;
println!("{w:?} {moved:?}");
}
7
[1, 2, 3, 1] [1, 2, 3, 1]

What Oxie removes is the bookkeeping around it:

  • arguments for &T and &mut T parameters of Oxie functions are borrowed for you (writing &v yourself works too);
  • a String or Vec<T> parameter that a function only reads becomes &str or &[T], so greet(name) lends name instead of giving it away (for functions that aren’t pub);
  • lifetimes are filled in when Rust can’t infer them (a function returning a reference gets one lifetime tied to its reference parameters, and a class holding a &str gets a lifetime parameter);
  • &self/&mut self are inferred, and closures that modify captured variables are declared mut for you;
  • for (x in xs) borrows xs when it is used after the loop, so iterating doesn’t use up the list;
  • trait bounds on type parameters are inferred from the body: fun show<T>(x: T) = "<$x>" becomes fn show<T>(x: T) -> String where T: Display.

When the borrow checker still says no, the error is explained in Oxie terms, with fixes. Here register stores the name in a list, so it has to own it:

error[E0382]: `name` was moved, so it can't be used anymore
--> main.oxi:7:30
|
6 | register(names, name)
| ---- `name` moved into `register()` here
7 | println("${names.len()} $name")
| ^^^^ used here after the move
= help: giving a value to a function moves it: `register` keeps it, and `name` can't be used
afterwards. Pass a copy with `name.clone()`.
= fix: pass a copy of `name`: `register(names, name.clone())`

In the editor the fixes are quick fixes (code actions).

fun add(a: i32, b: i32) = a + b
test "adds numbers" {
assert_eq(add(2, 2), 4)
}
test "parsing fails on letters" throws {
val r = "x".parse::<i32>()
assert(r.is_err())
}
fun main() = println(add(1, 1))
pub(crate) fn add(a: i32, b: i32) -> i32 {
a + b
}
fn main() {
println!("{}", add(1, 1));
}
#[cfg(test)]
mod __oxie_tests {
#[allow(unused_imports)]
use super::*;
/// adds numbers
#[test]
fn test_adds_numbers() {
assert_eq!(add(2, 2), 4);
}
/// parsing fails on letters
#[test]
fn test_parsing_fails_on_letters() -> ::std::result::Result<(), crate::__oxie::AnyError> {
let r = "x".parse::<i32>();
assert!(r.is_err());
Ok(())
}
}
2

oxie test runs them (oxie test "adds numbers" runs one).

// dep: tokio = { version = "1", features = ["full"] }
import std::time::Duration
async fun fetch(id: u32): String {
tokio.time.sleep(Duration.from_millis(10)).await
"item $id"
}
@tokio::main
async fun main() {
val a = fetch(1)
val b = fetch(2)
val (x, y) = tokio::join!(a, b)
println("$x, $y")
println(await fetch(3)) // TypeScript-style `await` works too
}
use std::time::Duration;
pub(crate) async fn fetch(id: u32) -> String {
tokio::time::sleep(Duration::from_millis(10)).await;
format!("item {id}")
}
#[tokio::main]
async fn main() {
let a = fetch(1);
let b = fetch(2);
let (x, y) = tokio::join!(a, b);
println!("{x}, {y}");
println!("{}", fetch(3).await);
}
item 1, item 2
item 3

Any Rust syntax Oxie doesn’t cover is still available:

macro_rules! square {
($x:expr) => { $x * $x };
}
rust {
fn rust_only(x: u32) -> u32 {
x.rotate_left(3)
}
}
fun main() {
println("${square!(5)} ${rust_only(1)}")
println!("plain Rust macro: {}", 1 + 1)
}
macro_rules! square {
($x:expr) => { $x * $x };
}
fn rust_only(x: u32) -> u32 {
x.rotate_left(3)
}
fn main() {
println!("{} {}", square!(5), rust_only(1));
println!("plain Rust macro: {}", 1 + 1);
}
25 8
plain Rust macro: 2

Next: the language reference lists every construct with the Rust it compiles to.