Skip to content

Oxie language reference

Oxie is a programming language that compiles to Rust. Every construct below lowers to ordinary Rust, and the semantics (ownership, borrowing, traits, generics, Option/Result, iterators, macros) are Rust’s. oxie emit file.oxi prints the generated Rust for any file.

Contents: Lexical structure · Declarations · Functions · Types · Classes · Enums · Interfaces · Extensions and operators · Statements · Expressions · Strings · Null safety · Errors · Control flow · Lambdas · Collections · Modules · Visibility · Attributes · Tests · Async · Rust escape hatches · Keywords


  • Comments: // line, /* block (nestable) */, /// doc comment (becomes a Rust doc comment), //! module doc.

  • Statements end at a newline. ; may separate statements on one line. A line continues when it ends with an operator, ,, ., (, [ or {, when the next line starts with ., ?., ?:, && or ||, and everywhere inside (...) and [...]:

    val total = items
    .iter()
    .map { it.price }
    .sum::<f64>()
  • Identifiers are Rust identifiers. Rust keywords that Oxie does not reserve (type, match, loop, …) can be field and method names; r#name works as in Rust.

  • Numbers: Rust literals (1_000, 0xff, 1e-3, 5u8, 2.0f32) plus Kotlin’s 10L (i64).

  • Characters: 'a', '\n', '\u{1F600}', byte b'a'.

  • Strings: see Strings. Raw strings r"...", r#"..."# and byte strings b"..." are Rust’s.

Oxie Rust
val x = 1 let x = 1;
var x = 1 let mut x = 1;
val x: T = v let x: T = v;
val (a, b) = pair let (a, b) = pair;
var (a, b) = pair let (mut a, mut b) = pair;
let/let mut (Rust style) unchanged
val Some(x) = opt else { return } let Some(x) = opt else { return };
top-level const NAME = 1 / val NAME = 1 const NAME: i32 = 1; (type inferred from literals)
static NAME: T = v / static var NAME: T = v static / static mut
typealias Name = T / type Name = T type Name = T;
fun name<T: Bound>(param: Type, other: Type = default): ReturnType throws Error where T: Other {
body
}
fun short(x: i32) = x * 2
pub(crate) fn name<T: Bound>(param: Type, other: Type) -> ::std::result::Result<ReturnType, Box<dyn Error>> where T: Other {
Ok(body)
}
pub(crate) fn short(x: i32) -> i32 {
x * 2
}
  • Return type: : T (or Rust’s -> T). Omitted means (), except for an expression body whose type is obvious (a literal, a template, arithmetic on typed values, a call to a function with a declared type, a constructor, …), which Oxie infers.

  • Block value: the last expression of a block is its value. In a function returning () the last expression is a statement.

  • Default arguments and named arguments (f(x = 1)) work for functions, methods and constructors declared in Oxie; defaults are filled in at the call site.

  • Generics and bounds are Rust’s: <T: Display + Clone>, where T: Into<String>, <'a>, <const N: usize>. Kotlin’s fun <T> name() order is accepted too.

  • Inferred bounds: bounds a function body needs are added to the generated signature (methods of generic classes get them as a where clause on the method):

    use of a T value (or an element of &[T], Vec<T>) bound added
    "$x", println(x), x.to_string() Display
    "${x:?}" Debug
    a < b, a.partial_cmp(b) / a == b, x in xs PartialOrd / PartialEq
    a.cmp(b), a.max(b) Ord
    a + b, a * b, … with both T Add<Output = T> + Copy, …
    a += b AddAssign, …
    x.clone() Clone
    a key of a HashSet/HashMap variable Eq + Hash

    Bounds already written (or implied, like Ord for PartialOrd) are not repeated. Interface methods keep the interface’s signature.

  • Automatic borrowing: an argument for a &T or &mut T parameter of a function or method declared in Oxie is borrowed: total(v) → total(&v), add_one(v) → add_one(&mut v). For a &str or &[T] parameter any owned argument is lent (greet(name.to_uppercase())).

  • Borrowed parameters: a String or Vec<T> parameter of a top-level function that isn’t pub becomes &str or &[T] when the body only reads it (calls len, contains, iter, map, …, prints it, compares it, indexes it, or passes it on to another borrowed parameter), so callers keep their value. A function that stores, returns or consumes the value keeps the owned type, and so do functions that are pub, async, used as values (xs.map(greet)) or called through a path (util.greet(x)). Arguments that already are references (&v, reference-typed variables, self, string literals) are passed as they are. Passing a field to a &mut parameter makes the method &mut self.

  • Modifiers: async fun, unsafe fun, const fun, extern "C" fun.

  • Automatic lifetimes: when a function returns a reference and Rust cannot infer which parameter it borrows from, Oxie ties the result to all reference parameters (fun longest(a: &str, b: &str): &str → fn longest<'a>(a: &'a str, b: &'a str) -> &'a str). With no reference parameters the result is 'static. Types with lifetime parameters get <'_> in signatures, and count as references here (fun first(p: &Parser): Token → fn first<'a>(p: &'a Parser<'a>) -> Token<'a>).

  • Function-typed parameters: f: (i32) -> i32 becomes impl FnMut(i32) -> i32 (or impl Fn when the function is called from inside a closure); as a field, a return value stored in a variable, or a generic argument it becomes Box<dyn Fn(i32) -> i32>, and lambdas passed to it are boxed automatically.

  • fun main() throws prints the error with its causes and exits with status 1.

Oxie Rust
i32, u8, f64, bool, char, str, String, … same
Int, Long, Short, Byte, UInt, ULong, Double, Float, Boolean, Char, Unit i32, i64, i16, i8, u32, u64, f64, f32, bool, char, ()
T? Option<T> (&T? is Option<&T>)
(A, B) -> R impl FnMut(A, B) -> R, Box<dyn Fn(A, B) -> R> or dyn Fn(..) depending on position
Shape (an interface used as a type) impl Shape (parameters, returns), dyn Shape (behind &, Box, Rc, Arc), Box<dyn Shape> (fields, collections)
&T, &mut T, [T], [T; N], (A, B), Vec<T>, dyn T, impl T, *const T same
std.io.Error std::io::Error (dots or :: in paths)

The standard traits used as types the same way are Display, Debug, Error, Iterator, IntoIterator, Read, Write, BufRead, Future, Any, ToString and every interface declared in Oxie.

class Name<T>(val a: A, var b: B = default, private c: C) : Interface, Hash {
var d: D = default // body property (needs a default)
init { ... } // runs in the constructor after fields are set
fun read(): A = a // &self
fun write(x: B) { b = x } // &mut self (inferred)
static fun create(): Name<T> // no self
companion object { fun other(): Name<T> = ... } // also static
override fun method() { ... } // implements `Interface`
}
  • Generates struct Name<T> { a, b, c, d } and impl with fn new(a, b, c) -> Self.
  • Construction: Name(a, b) → Name::new(a, b); named arguments and defaults work. Zero-argument calls of external types (HashMap(), String()) become Type::new().
  • val/var before constructor parameters are accepted (Kotlin habit) but don’t matter: mutability belongs to the variable holding the object, as in Rust.
  • Implicit this: inside methods, fields and methods of the class (and default methods of its interfaces) can be used without this.; a parameter or local with the same name wins. this and self are the same.
  • Receivers are inferred:
    • &mut self if the method assigns a field, calls a mutating method on a field (push, insert, clear, entry, …), takes &mut of a field, or calls a &mut self method;
    • static if it never uses self and returns Self/the class, or is named new, default, from, from_str, try_from or from_iter;
    • &self otherwise. Override with mut fun, static fun, or an explicit self, &self, &mut self, mut self first parameter. Interface methods take the receiver their implementations need.
  • data class additionally derives Debug, Clone, PartialEq, and Copy, Eq, Hash when every field type supports them, and supports x.copy(field = value).
  • Supertypes after : are traits to implement. Derivable traits (Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord) without methods in the body become derives. override fun toString() = "..." implements Display.
  • @derive(...) adds derives; @no_auto_derive turns the automatic ones off.
  • Reference fields: a class, struct or enum whose fields hold references without a lifetime (&str, Vec<&T>, &str?, or another such type) gets one: class Parser(input: &str) → struct Parser<'a> { input: &'a str }. Write the type as Parser; signatures spell it Parser<'_>. Declaring lifetimes yourself (class Pair<'a, 'b>) turns this off.
  • Rust-style struct Point { x: f64, y: f64 }, tuple structs struct Meters(f64) and unit structs struct Marker are supported. A type without fields is also its own value, so Marker.describe() calls a method on it (static functions are still Marker.create()).
  • union Bits { i: u32, f: f32 } is a Rust union: built with Bits { f: 1.0 }, read in unsafe.
  • shared class — see Shared classes.
  • object Name { ... } is a namespace (a Rust module) of constants and functions: Name.function().
  • Classes, enums and interfaces declared inside a class body are placed next to the class; refer to them as Inner or Outer.Inner.
shared class Name(var a: A) : Interface { ... } // Rc<RefCell<..>> handle
@sync shared class Name(var a: A) { ... } // Arc<Mutex<..>>, usable across threads
  • The class is generated as NameData (the struct, constructor and methods as for a class) and Name, a handle: struct Name(Rc<RefCell<NameData>>).
  • The handle has the same constructor, one method per instance method that borrows the data for the call (self.0.borrow_mut().deposit(n)), a getter a() and setter set_a(v) per field, and implements the class’s Oxie interfaces and toString (Display) by forwarding.
  • Static methods and companion object functions are on the handle.
  • Clone copies the handle; == and Hash use identity (Rc::ptr_eq).
  • Using a shared variable or field by value copies the handle (val b = a → a.clone()), so the original stays usable.
  • From outside the class: a.f reads through the getter (a copy of the field), a.f = v uses the setter, a.f += v, a.f[i] = v and mutating calls like a.f.push(x) borrow the object mutably for that statement (arguments are evaluated first), and other chains (a.f.len(), a.f[0]) borrow it for reading.
  • Methods returning references into the object can’t be forwarded (the borrow ends with the call); return owned values. Inside its methods, this is the data, not a handle.
enum Shape : Display {
Circle(radius: f64) // named fields → Circle { radius: f64 }
Pair(i32, i32) // positional fields
Empty // unit variant
Coded = 3 // explicit discriminant (unit-only enums)
fun area(): f64 = when (this) { Circle(r) -> 3.14 * r * r; else -> 0.0 }
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) enum Shape {
Circle { radius: f64 },
Pair(i32, i32),
Empty,
Coded = 3,
}
impl Shape {
pub(crate) fn area(&self) -> f64 {
match self {
Shape::Circle { radius: r } => 3.14 * r * r,
_ => 0.0,
}
}
}
impl Display for Shape {}
  • Variants are separated by newlines or commas; methods follow in the same body.
  • Shape.Circle(1.0) or Circle(1.0) (when the name is unique, or declared in the same file) constructs; named-field variants accept positional or named arguments.
  • In patterns, Circle(r) matches named-field variants positionally (→ Circle { radius: r }), and a bare Circle matches any Circle.
  • Enums derive Debug, Clone, PartialEq (and Copy, Eq, Hash when possible).
  • error enum — see Errors.
interface Named : Display {
fun name(): String // required
fun greet(): String = "hi ${name()}" // default
type Id // associated type
type Item<'a> where Self: 'a // generic associated type
const MAX: u32 = 10 // associated constant
fun borrow<'a>(&'a self): &'a str // explicit receiver lifetime
}
pub(crate) trait Named: Display {
fn name(&self) -> String;
fn greet(&self) -> String {
format!("hi {}", self.name())
}
type Id;
type Item<'a> where Self: 'a;
const MAX: u32 = 10;
fn borrow<'a>(self: &'a Self) -> &'a str;
}
  • interface (or trait) → trait. Methods take &self unless declared otherwise (or an implementation needs &mut self).
  • A class lists interfaces after : and implements methods with override fun; the matching impl Trait for Class is generated. Methods named like the trait’s (and type members) go to the right trait even without override.
  • Rust impl Type { } and impl Trait for Type { } blocks are supported, also for types not declared in Oxie (impl Display for Point { fun fmt(f: &mut Formatter): fmt::Result = ... }). Inside, methods follow the same receiver rules and fields are available implicitly when the type is an Oxie class.
fun String.shout(): String = self.to_uppercase()
fun <T: Clone> Vec<T>.second(): T? = self.get(1).cloned()
mut fun Vec<i32>.bump(by: i32) { for (x in self.iter_mut()) *x += by }
#[allow(non_camel_case_types)]
pub(crate) trait __OxieExt_shout {
fn shout(&self) -> String;
}
impl __OxieExt_shout for String {
fn shout(&self) -> String {
self.to_uppercase()
}
}
#[allow(non_camel_case_types)]
pub(crate) trait __OxieExt_second<T: Clone> {
fn second(&self) -> Option<T>;
}
impl<T: Clone> __OxieExt_second<T> for Vec<T> {
fn second(&self) -> Option<T> {
self.get(1).cloned()
}
}
#[allow(non_camel_case_types)]
pub(crate) trait __OxieExt_bump {
fn bump(&mut self, by: i32);
}
impl __OxieExt_bump for Vec<i32> {
fn bump(&mut self, by: i32) {
for x in self.iter_mut() {
*x += by;
}
}
}

Each becomes a trait with one method plus its impl for the receiver type; every file of the crate imports these traits automatically. Inside, this/self is the receiver; for Oxie classes their fields can be used directly.

operator fun inside a class or enum implements a std::ops trait:

Oxie trait Oxie trait
plus Add plusAssign AddAssign
minus Sub minusAssign SubAssign
times Mul timesAssign MulAssign
div Div divAssign DivAssign
rem Rem remAssign RemAssign
unaryMinus Neg not Not
shl, shr, and, or, xor Shl, Shr, BitAnd, BitOr, BitXor

Rust names (add, sub, mul, neg, add_assign, …) work as well. The parameter type is the right-hand side (impl Mul<f64> for Vec2 for operator fun times(k: f64)).

  • Variable declarations (see Declarations).
  • Expressions, including assignments a = b, a += b (all compound operators).
  • Local fun, class, enum, … declarations.
  • return, break ['label] [value], continue ['label], throw e.
  • Attributes on a statement: @cfg(debug_assertions) println("debug"), #[allow(unused_variables)] val x = 1.
Oxie Rust
a == b, a < b, a && b, a + b, … same (Rust precedence)
x in coll / x !in coll coll.contains(&x) / !coll.contains(&x)
x in a..b a <= x && x < b
x is Pattern / x !is Pattern matches!(x, Pattern) / !matches!(..)
a to b (a, b)
a until b, a downTo b, r step n a..b, (b..=a).rev(), r.step_by(n)
a..b, a..=b, a..<b a..b, a..=b, a..b
x as T x as T
x? x?
x!! x.unwrap()
a ?: b a.unwrap_or(b)-like; see Null safety
a?.b safe navigation or error propagation; see Null safety
await f() / f().await f().await
Type.member, module.item Type::member, module::item
f<T>(x), mutableListOf<i32>() f::<T>(x), Vec::<i32>::new()
Point { x: 1, y: 2 } struct literal (with ..base)
x.let { it + 1 } { let it = x; it + 1 }
p.copy(x = 1) clone with x replaced
if (c) a else b, when, try, blocks expressions with values

Strings and +: when an operand of a + chain is a string literal or template, the chain builds a String with format! ("total: " + n works for any Display value).

Implicit conversions happen only where Rust would otherwise reject the code, and only to the declared type of a variable, field, parameter or return value:

  • a string literal to String (String::from("...")), a &str value to a String parameter (.into());
  • a value to T? (Some(...), or .into() for variables), null to None;
  • a lambda to a function-typed field (Box::new(...)), a constructed value to an interface-typed field or collection (Box::new(...));
  • a vec[...] to Vec<T> element by element.
"Hello, $name!" // format!("Hello, {name}!")
"sum: ${a + b}" // format!("sum: {}", a + b)
"${value:.2} ${list:?}" // format specs after `:` (Rust's format syntax)
"cost: \$5" // literal dollar sign
"""
multi-line
text with $vars
""" // common indentation and the first/last empty lines are removed
  • A literal without $ is a &'static str; a template is a String.
  • Inside println, print, eprintln, format, panic, assert, write, writeln, … a template is spliced directly into the format string (no extra allocation).
  • A string literal passed to these with extra arguments is a Rust format string: println("{} items", n).
  • println(a, b) prints both separated by a space; println(x) prints any Display value.
  • T? is Option<T>, null is None.
  • a?.b, a?.m(), a?.b?.c — safe navigation when a is optional: the result is None if a is. Chains flatten like Kotlin (?. on a T? property continues the chain; ?. after a non-optional property acts like .). Reading a field through a borrowed value gives a reference (Option<&T>).
  • When the left side is a Result (a call to a throws function, .await, parse(), File::open(...), …), a?.b is Rust’s ? followed by .b: the error is propagated. For a left side of unknown type, ?. propagates errors inside functions that return Result and navigates otherwise.
  • a ?: b — a if present (Some/Ok), else b, evaluated only when needed. b may be return, throw, break, continue or panic(...). For a literal b, Option<&T> ?: 0 yields T, Option<String> ?: "text" yields String and Option<&String> ?: "text" yields &str. So through a borrowed value (u: &User), u.address?.city ?: "unknown" is borrowed text; where a String is needed, copy the field explicitly, as for any field read through a reference: u.address?.city.clone() ?: "unknown". The compiler error offers this fix.
  • x!! — unwrap.
  • if (val v = opt) { } / while (val v = opt) — if let Some(v) / while let Some(v); with a pattern (if (val Ok(v) = r)) the pattern is used as written.
  • x?.let { ... } runs the block when x is present.

See Error handling for the full guide.

Oxie Rust
fun f(): T throws -> Result<T, AnyError> (Box<dyn Error + Send + Sync>)
fun f(): T throws E -> Result<T, E>
return v / tail v in a throwing function return Ok(v) / Ok(v)
throw e return Err(e.into())
calling a throwing Oxie function as a statement or result adds ?
try { a()?; b } catch (e: E) { c } labeled block; ?/throw inside break out with Err
error enum E { V(f: T) = "msg $f" } enum + Display + Error (+ source) + From
error class E(f: T) = "msg $f" struct + Display + Error
x.context("msg") / .with_context { ... } wrap the error with a message
if (cond) a else b // also `if cond { }` (Rust style)
if (val x = opt) { } else { }
if (val x = opt && x > 0 && val y = f(x)) { } // let chain (any Rust edition)
while (val x = stack.pop() && x != 0) { }
when (x) { // match
1, 2 -> "one or two" // alternatives
in 3..=9 -> "small" // range (or any `contains`)
is Some(n) if n > 0 -> "guard" // `is` optional, `if` guard
else -> "other" // `_`
}
when { a < b -> ...; else -> ... } // if/else chain
when (val r = compute()) { ... } // binds the subject
match x { Some(v) => v, None => 0 } // Rust match syntax works too
for (item in items) { } // also `for item in items { }`
for ((i, x) in xs.iter().enumerate()) { }
while (cond) { }
do { } while (cond)
loop { }
'outer: for (...) { break 'outer }
val v = 'found: { if (c) break 'found 1; 0 } // labeled block with a value
val n = const { SIZE * 2 } // evaluated at compile time
  • A statement when on literals doesn’t need else (Oxie adds _ => {}); a when used as a value must be exhaustive, as in Rust.
  • if/for/while bodies can be a single statement without braces.
  • for (x in xs) consumes xs, as in Rust, unless xs is a local collection (Vec, VecDeque, HashMap, HashSet, BTreeMap, …) that is used after the loop or by the next round of an enclosing loop: then the loop borrows it (for x in &xs), and x is a reference to the element (Copy elements are bound by value: for &n in &nums). A loop over a collection field of the class always borrows it, except in methods taking self.
{ it * 2 } // one implicit parameter `it`
{ a, b -> a + b } // Kotlin style
{ (key, value) -> key } // destructuring
{ x: i32 -> x * 2 } // typed
(a, b) => a + b // TypeScript style
x => x * 2
|a, b| a + b // Rust style
move { ... } // capture by value
{ [counter, tx] -> ... } // capture list: moves in clones of `counter` and `tx`
{ [n = items.len()] x -> ... } // a computed capture, then parameters
f(x) { trailing lambda } // last argument outside the parentheses
  • A brace lambda without -> has no parameters, or it if it uses it. Closures with an explicit empty list (|| it + 1, () => it) take no parameter and use an outer it. When a standard method always passes a value (map, filter, map_err, unwrap_or_else on results, …), an unused parameter is added for you (map_err { MyError() }).
  • A lambda body is a block; its last expression is the result.
  • A lambda stored in a val that assigns to captured variables is declared mut automatically.
  • A capture list [a, b = expr] at the start of a brace lambda (followed by parameters or ->) gives the lambda its own values: a is a.clone(), b is expr, and the lambda is move. thread.spawn { [counter, tx] -> ... } becomes thread::spawn({ let counter = counter.clone(); let tx = tx.clone(); move || ... }), the usual Rust pattern for handing shared handles and channel senders to threads and callbacks.
  • In predicates (filter, find, position, any, all, count, take_while, retain, …) the parameter is a reference to the element (&&T for iter().filter). Comparing or computing it with a number, char or bool literal reads the value through the references: filter { it > 2 }, filter { it % 2 == 0 }.
  • run { ... } is a block expression; repeat(n) { ... } loops n times with it.
  • { ... } on its own as a statement is a lambda too, so it would never run: Oxie reports it as an error and suggests run { ... }. As the last expression of a block it is the block’s value (a function returning a lambda can end with { 42 }).
Oxie Rust
vec[1, 2, 3], listOf(1, 2, 3), mutableListOf() vec![1, 2, 3], Vec::new()
vec[0; n] vec![0; n]
[1, 2, 3], arrayOf(...) array [1, 2, 3]
mapOf(k to v, ...) HashMap::from([(k, v), ...])
setOf(a, b) HashSet::from([a, b])
m[key], v[i], &v[1..3] indexing and slicing

All standard library types and methods are used with their Rust names.

Lists, arrays and slices (anything that derefs to [T]) also have Kotlin-style operations. Each one is the iterator chain you would write by hand; the element is passed by reference.

Oxie Result Rust
xs.map { f(it) }, map_indexed { i, x -> } Vec<U> xs.iter().map(f).collect()
xs.filter { p(it) }, filter_not Vec<T> (copies kept elements, T: Clone) xs.iter().filter(p).cloned().collect()
xs.filter_map { }, flat_map { } Vec<U> filter_map / flat_map + collect
xs.for_each { }, for_each_indexed { i, x -> } — iter().for_each(..)
xs.find { }, find_last { } T? (a reference) iter().find(..)
xs.index_of_first { } usize? iter().position(..)
xs.any { }, all { }, none { }, count { } bool / usize iter().any(..), …
xs.sum(), sum_of { } number iter().sum(), iter().map(..).sum()
xs.max_of { }, min_of { } S? iter().map(..).max_by(..)
xs.max_by_key { }, min_by_key { } T? (a reference) works with f64 keys too
xs.sorted(), sorted_descending(), sorted_by { }, sorted_by_descending { } Vec<T> to_vec() + sort
xs.reversed(), distinct(), distinct_by { }, chunked(n) Vec<T> / Vec<Vec<T>>
xs.group_by { key } HashMap<K, Vec<T>>
xs.associate_by { key } HashMap<K, T>
xs.partition { } (Vec<T>, Vec<T>)
xs.join_to_string(", "), join_with(", ") { it.name } String
xs.is_not_empty() bool !xs.is_empty()

Inherent Rust methods win over these (arrays keep Rust’s by-value map), and Rust’s iterator methods are always available: xs.iter().filter { .. }.map { .. }.collect().

  • Each .oxi file under src/ is a module: src/util.oxi → util, src/net/http.oxi → net::http, src/net/mod.oxi → net. src/main.oxi (or src/lib.oxi) is the crate root.
  • import a::b::C, import a.b.C, import a::b::{C, D as E}, import a::b::*, import { C, D } from a::b (TypeScript), use ... (Rust). Paths that start with one of your modules are resolved from the crate root, so import util::helper works in every file.
  • mod name { ... } declares an inline module; mod name (without a body) declares a hand-written Rust module file next to the source.
  • Paths may use . instead of :: when the left side is a type, module or crate name: std.mem.swap(a, b), Duration.from_secs(1), Color.Red, math.double(2) (inline module), Stack::<i32>.new().
Oxie Rust
(default) pub(crate) — visible in the whole project
pub / public pub — exported from a library
private private to the file (module)
internal pub(crate)
pub(super), pub(in path) same

Members of a pub class or enum default to pub. Keeping items unexported by default lets the optimizer treat them like private Rust functions; mark library APIs pub.

@name and @name(args) become #[name] / #[name(args)]: @derive(Serialize), @serde(rename_all = "camelCase"), @inline, @cfg(test), @tokio::main, … Rust’s #[...] syntax works too. Oxie-specific: @no_auto_derive (classes/enums), @from (error enum cases). Attributes also work on statements (see Statements).

#![...] at the top of a file:

  • in a module file, it applies to that module (#![allow(dead_code)]);
  • in the crate root (main.oxi/lib.oxi), lint attributes (allow, warn, deny, forbid, expect) apply to the whole crate. Other crate attributes (#![recursion_limit = "..."], #![no_std], …) work in single-file scripts; in a project, put them in src/main.rs / src/lib.rs.
test "addition works" {
assert_eq(1 + 1, 2)
}
test "can fail" throws { "1".parse::<i32>()? }
async test "needs tokio" { ... } // add @tokio::test

Tests go into a #[cfg(test)] module with #[test] functions named after the description. Run with oxie test (or cargo test).

async fun, async { }, async move { }, .await and TypeScript-style prefix await expr. Use any runtime: @tokio::main async fun main(). A throwing async main works too.

  • rust { ... } — raw Rust items (at top level) or a raw Rust expression/statements.
  • name!(...), name![...], name!{...} — any Rust macro, passed through unchanged. Common std macros can also be called without ! (println(...), assert_eq(...), vec[...], format(...), panic(...), todo(), dbg(...), matches(...), …).
  • macro_rules! definitions at top level.
  • Rust syntax that Oxie shares (let, match with =>, impl, fn, |x| closures, ::<T> turbofish, if let, while let, let chains, 'label: loops and blocks, const { }, union, extern "C" { }, #[attr], &'a self) is accepted directly. tests/coverage has one program per Rust feature.

Reserved: fun fn val var let if else when match for while do loop return break continue throw try catch class interface trait enum struct impl import use const static typealias mod true false null this self Self in is as where async move unsafe dyn mut ref extern pub.

Contextual (usable as names elsewhere): data, error, shared, object, companion, init, union, override, operator, private, internal, public, test, rust, throws, to, until, downTo, step, await, it.