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.
Hello, world
Section titled “Hello, world”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.
Variables and types
Section titled “Variables and types”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 2The 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.
Functions
Section titled “Functions”fun add(a: i32, b: i32): i32 { return a + b}
// expression body; the return type is inferred when it is obviousfun square(x: i32) = x * x
// the last expression of a block is its value, like Rustfun max(a: i32, b: i32): i32 { if (a > b) a else b}
// default and named argumentsfun 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"));}5499Hello, Ada!Hi, Bob!Parameters are written name: Type, the return type after : (Rust’s -> also works).
Strings
Section titled “Strings”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.2next: 2debug: [1, 2]Hello, Oxie!Roses are red, Oxie is new.a dollar: $5String literals are &str (borrowed, free). Templates and + produce String. Where a
String is expected, a literal or &str is converted for you.
Control flow
Section titled “Control flow”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 smallsingle digit0 1 2 10 5 0 1 2 3found 21when 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.
Collections and lambdas
Section titled “Collections and lambdas”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 36a: 35 8A 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, Gracetotal age 109, oldest GraceLinus < Ada < GraceLondon: 2In 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.
Null safety
Section titled “Null safety”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")0found Bobhello Adatrue6a ?: b evaluates b only when a is null (or an error). The right side can also be
return, throw, break or continue.
Classes
Section titled “Classes”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]10Point { 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.(writethis.xwhen a parameter has the same name). - Oxie decides how each method receives the object:
&selfif it only reads,&mut selfif it assigns a field or calls something that does, and static if it never uses the object and returnsSelf. Writemut fun,static funor an explicitselfparameter to choose. data classderivesDebug,Clone,PartialEq— plusCopy,EqandHashwhen all fields allow it — and hascopy(field = value).init { }runs after the fields are set; body properties need a default value.
Shared classes
Section titled “Shared classes”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 classusesArc<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 asHashMapkeys.- Reading a whole field (
val h = a.history) copies it;a.history.len()anda.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
nextis 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());}6enum 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.14Rect { w: 2.0, h: 3.0 } has area 6.00Empty has area 0.00falseVariants 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).
Interfaces
Section titled “Interfaces”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 woofTom says meowAn 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.
Extension functions and operators
Section titled “Extension functions and operators”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).
Errors
Section titled “Errors”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);}8080error: invalid value 'eighty' for port80throws 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.
Modules and visibility
Section titled “Modules and visibility”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 tooimport { Deserialize, Serialize } from serdeimport model::user::User // your own moduleuse 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.
Ownership
Section titled “Ownership”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
&Tand&mut Tparameters of Oxie functions are borrowed for you (writing&vyourself works too); - a
StringorVec<T>parameter that a function only reads becomes&stror&[T], sogreet(name)lendsnameinstead of giving it away (for functions that aren’tpub); - 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
&strgets a lifetime parameter); &self/&mut selfare inferred, and closures that modify captured variables are declaredmutfor you;for (x in xs)borrowsxswhen 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>"becomesfn 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()` here7 | 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(()) }}2oxie 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::mainasync 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 2item 3Rust inside Oxie
Section titled “Rust inside Oxie”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 8plain Rust macro: 2Next: the language reference lists every construct with the Rust it compiles to.