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
Lexical structure
Section titled “Lexical structure”-
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#nameworks as in Rust. -
Numbers: Rust literals (
1_000,0xff,1e-3,5u8,2.0f32) plus Kotlin’s10L(i64). -
Characters:
'a','\n','\u{1F600}', byteb'a'. -
Strings: see Strings. Raw strings
r"...",r#"..."#and byte stringsb"..."are Rust’s.
Declarations
Section titled “Declarations”| 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; |
Functions
Section titled “Functions”fun name<T: Bound>(param: Type, other: Type = default): ReturnType throws Error where T: Other { body}fun short(x: i32) = x * 2pub(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’sfun <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
whereclause on the method):use of a Tvalue (or an element of&[T],Vec<T>)bound added "$x",println(x),x.to_string()Display"${x:?}"Debuga < b,a.partial_cmp(b)/a == b,x in xsPartialOrd/PartialEqa.cmp(b),a.max(b)Orda + b,a * b, … with bothTAdd<Output = T> + Copy, …a += bAddAssign, …x.clone()Clonea key of a HashSet/HashMapvariableEq + HashBounds already written (or implied, like
OrdforPartialOrd) are not repeated. Interface methods keep the interface’s signature. -
Automatic borrowing: an argument for a
&Tor&mut Tparameter of a function or method declared in Oxie is borrowed:total(v)→total(&v),add_one(v)→add_one(&mut v). For a&stror&[T]parameter any owned argument is lent (greet(name.to_uppercase())). -
Borrowed parameters: a
StringorVec<T>parameter of a top-level function that isn’tpubbecomes&stror&[T]when the body only reads it (callslen,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 arepub,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&mutparameter 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) -> i32becomesimpl FnMut(i32) -> i32(orimpl Fnwhen the function is called from inside a closure); as a field, a return value stored in a variable, or a generic argument it becomesBox<dyn Fn(i32) -> i32>, and lambdas passed to it are boxed automatically. -
fun main() throwsprints 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.
Classes
Section titled “Classes”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 }andimplwithfn 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()) becomeType::new(). val/varbefore 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 withoutthis.; a parameter or local with the same name wins.thisandselfare the same. - Receivers are inferred:
&mut selfif the method assigns a field, calls a mutating method on a field (push,insert,clear,entry, …), takes&mutof a field, or calls a&mut selfmethod;- static if it never uses
selfand returnsSelf/the class, or is namednew,default,from,from_str,try_fromorfrom_iter; &selfotherwise. Override withmut fun,static fun, or an explicitself,&self,&mut self,mut selffirst parameter. Interface methods take the receiver their implementations need.
data classadditionally derivesDebug,Clone,PartialEq, andCopy,Eq,Hashwhen every field type supports them, and supportsx.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() = "..."implementsDisplay. @derive(...)adds derives;@no_auto_deriveturns 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 asParser; signatures spell itParser<'_>. Declaring lifetimes yourself (class Pair<'a, 'b>) turns this off. - Rust-style
struct Point { x: f64, y: f64 }, tuple structsstruct Meters(f64)and unit structsstruct Markerare supported. A type without fields is also its own value, soMarker.describe()calls a method on it (static functions are stillMarker.create()). union Bits { i: u32, f: f32 }is a Rust union: built withBits { f: 1.0 }, read inunsafe.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
InnerorOuter.Inner.
Shared classes
Section titled “Shared classes”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 aclass) andName, 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 gettera()and setterset_a(v)per field, and implements the class’s Oxie interfaces andtoString(Display) by forwarding. - Static methods and
companion objectfunctions are on the handle. Clonecopies the handle;==andHashuse 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.freads through the getter (a copy of the field),a.f = vuses the setter,a.f += v,a.f[i] = vand mutating calls likea.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,
thisis 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)orCircle(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 bareCirclematches anyCircle. - Enums derive
Debug,Clone,PartialEq(andCopy,Eq,Hashwhen possible). error enum— see Errors.
Interfaces and impl blocks
Section titled “Interfaces and impl blocks”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(ortrait) →trait. Methods take&selfunless declared otherwise (or an implementation needs&mut self).- A class lists interfaces after
:and implements methods withoverride fun; the matchingimpl Trait for Classis generated. Methods named like the trait’s (andtypemembers) go to the right trait even withoutoverride. - Rust
impl Type { }andimpl 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.
Extension functions
Section titled “Extension functions”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.
Operators
Section titled “Operators”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)).
Statements
Section titled “Statements”- 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.
Expressions
Section titled “Expressions”| 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&strvalue to aStringparameter (.into()); - a value to
T?(Some(...), or.into()for variables),nulltoNone; - a lambda to a function-typed field (
Box::new(...)), a constructed value to an interface-typed field or collection (Box::new(...)); - a
vec[...]toVec<T>element by element.
Strings
Section titled “Strings”"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 aString. - 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 anyDisplayvalue.
Null safety
Section titled “Null safety”T?isOption<T>,nullisNone.a?.b,a?.m(),a?.b?.c— safe navigation whenais optional: the result isNoneifais. Chains flatten like Kotlin (?.on aT?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 athrowsfunction,.await,parse(),File::open(...), …),a?.bis Rust’s?followed by.b: the error is propagated. For a left side of unknown type,?.propagates errors inside functions that returnResultand navigates otherwise. a ?: b—aif present (Some/Ok), elseb, evaluated only when needed.bmay bereturn,throw,break,continueorpanic(...). For a literalb,Option<&T> ?: 0yieldsT,Option<String> ?: "text"yieldsStringandOption<&String> ?: "text"yields&str. So through a borrowed value (u: &User),u.address?.city ?: "unknown"is borrowed text; where aStringis 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 whenxis present.
Errors
Section titled “Errors”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 |
Control flow
Section titled “Control flow”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 chainwhen (val r = compute()) { ... } // binds the subjectmatch x { Some(v) => v, None => 0 } // Rust match syntax works toofor (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 valueval n = const { SIZE * 2 } // evaluated at compile time- A statement
whenon literals doesn’t needelse(Oxie adds_ => {}); awhenused as a value must be exhaustive, as in Rust. if/for/whilebodies can be a single statement without braces.for (x in xs)consumesxs, as in Rust, unlessxsis 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), andxis a reference to the element (Copyelements are bound by value:for &n in &nums). A loop over a collection field of the class always borrows it, except in methods takingself.
Lambdas
Section titled “Lambdas”{ 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 stylex => x * 2|a, b| a + b // Rust stylemove { ... } // capture by value{ [counter, tx] -> ... } // capture list: moves in clones of `counter` and `tx`{ [n = items.len()] x -> ... } // a computed capture, then parametersf(x) { trailing lambda } // last argument outside the parentheses- A brace lambda without
->has no parameters, oritif it usesit. Closures with an explicit empty list (|| it + 1,() => it) take no parameter and use an outerit. When a standard method always passes a value (map,filter,map_err,unwrap_or_elseon 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
valthat assigns to captured variables is declaredmutautomatically. - A capture list
[a, b = expr]at the start of a brace lambda (followed by parameters or->) gives the lambda its own values:aisa.clone(),bisexpr, and the lambda ismove.thread.spawn { [counter, tx] -> ... }becomesthread::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 (&&Tforiter().filter). Comparing or computing it with a number,charorboolliteral reads the value through the references:filter { it > 2 },filter { it % 2 == 0 }. run { ... }is a block expression;repeat(n) { ... }loopsntimes withit.{ ... }on its own as a statement is a lambda too, so it would never run: Oxie reports it as an error and suggestsrun { ... }. As the last expression of a block it is the block’s value (a function returning a lambda can end with{ 42 }).
Collections
Section titled “Collections”| 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.
Collection operations
Section titled “Collection operations”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().
Modules and imports
Section titled “Modules and imports”- Each
.oxifile undersrc/is a module:src/util.oxi→util,src/net/http.oxi→net::http,src/net/mod.oxi→net.src/main.oxi(orsrc/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, soimport util::helperworks 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().
Visibility
Section titled “Visibility”| 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.
Attributes
Section titled “Attributes”@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 insrc/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::testTests 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 escape hatches
Section titled “Rust escape hatches”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,matchwith=>,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/coveragehas one program per Rust feature.
Keywords
Section titled “Keywords”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.