Oxide Programming Language
Interpreted scripting language with a Rust influenced syntax. Latest release
Example programs
/// structs
struct Circle { // struct declaration
pub radius: float, // public field
center: Point, // private field
}
impl Circle { // struct implementation
const PI: float = 3.14159; // private associated constant
pub fn new(r: float, c: Point) -> Self { // public static method
return Self {
radius: r,
center: c,
};
}
}
struct Point {
pub x: int,
pub y: int,
}
/// traits
trait Shape { // trait declaration
fn calc_area(self) -> float; // trait methods are always public
}
impl Shape for Circle { // trait implementation
fn calc_area(self) -> float {
return Self::PI * self.radius * self.radius;
}
}
// program entry point
fn main() {
let a: Shape = Circle::new(200.0, Point { x: 1, y: 5 });
let area = a.calc_area();
println("circle area: " + area as str); // type casting
}
/// enums
enum Ordering {
Less,
Equal,
Greater
}
impl Ordering {
pub fn compare(a: int|float, b: int|float) -> Self {
return match true {
a < b => Self::Less,
a == b => Self::Equal,
a > b => Self::Greater,
};
}
}
fn main() {
let order = Ordering::compare(10, 5); // Ordering::Greater
}
/// sorting a vector using
/// insertion sort
fn insertion_sort(input: vec<int>) {
for i in 1..input.len() {
let cur = input[i];
let mut j = i - 1;
while input[j] > cur {
let temp = input[j + 1];
input[j + 1] = input[j];
input[j] = temp;
if j == 0 {
break;
}
j -= 1;
}
}
}
fn main() {
let input: vec<int> = vec[4, 13, 0, 3, -3, 4, 19, 1];
insertion_sort(input);
dbg(input); // [vec] [-3, 0, 1, 3, 4, 4, 13, 19]
}
Usage
Download the latest release and put the executable in your PATH.
USAGE:
oxide [FLAGS] [ARGS]
FLAGS:
-h, --help Prints help
-v, --version Prints version
-r, --repl Run REPL
-t, --allow-top-level Allow top-level instructions
ARGS:
<FILE> Script file to run
<ARGV> Arguments passed to script
EXAMPLE:
oxide script.ox arg1 arg2
Building from source
If your architecture is not supported by the pre-built binaries you can build the interpreter from the source code yourself. Make sure you have Rust installed.
git clone https://github.com/tuqqu/oxide-lang.git
cd oxide-lang
cargo +nightly install --path oxide-cli # creates a binary /.cargo/bin/oxide
# to uninstall run `cargo uninstall oxide-cli`
# you can now run it with
oxide script.ox
Quick Overview
- Program Structure
- Variables and Type System
- Control Flow and Loops
- Functions
- Structs
- Traits
- Enums
- Vectors
- Constants
- Operators
- Comments
- Standard library
Program Structure
In Oxide, the entry point of a program is a function named main
.
fn main() {
// code goes here
}
On the top level only item (const
, fn
, struct
, enum
, trait
, impl
, type
) declarations are allowed.
struct Foo {}
trait Bar {}
enum Foo {}
impl Bar for Foo {}
const C: int = 0;
fn baz() {}
type Foo = int;
Variables and Type System
Types and example values:
nil
: onlynil
value itself,bool
:false
,true
,int
:1
,float
:0.56
,str
:"string"
,fn(T) -> T
: any function or lambda with this signature,vec<T>
:vec[1, 2, 3, 4]
,any
: any value,- union types
str | int | T | ...
: any value of the types composing the union
user-defined types
See type system
Variables are typed either explicitly:
let x: int; // type = int
let nums: vec<int> = vec[1, 2]; // type = vec<int>
let jane: Person|nil = Person { // union type = Person|nil
name: "Jane"
};
// functions are their own type
let double: fn(int) -> int = fn (x: int) -> int { return x * 2; };
//^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// type lambda
or their type implicitly inferred:
let x = vec["h", "i"]; // inferred as vec<str>
let dog = Dog::new("Good Boy"); // inferred as Dog
let ordering = Ordering::Less; // inferred as Ordering
let f = fn (x: int) { ... }; // inferred as fn(int)
let x; // inferred as vec<int>
x = 0..=100; // the first time it is being assigned
Mutability
Variables are immutable by default:
let x = 100;
x += 1; //! error, x is immutable
To become mutable they must be defined with mut
keyword.
let mut x: str = "hello";
x += " world"; // ok
Shadowing
Variables can be shadowed. Each variable declaration "shadows" the previous one:
let x: int|nil = 100;
let x: vec<int> = vec[1, 2];
Casting
Explicit type conversion, i.e. type casting, can be performed using the as
keyword.
Primitive types int
, float
, nil
, bool
, str
can be cast to other primitive types.
let x = 32 as str; // typeof(x) = str, x = "32"
let x = "350" as int; // typeof(x) = int, x = 350
let x = 0.0 as bool; // typeof(x) = bool, x = false
let x = 10;
"this is x: " + x as str; // values must be cast to str for concatenation
Using non-primitives (vector vec<T>
, function fn(T) -> T
, enum
, and struct
types) will result in a type error.
any
type must be explicitly cast to be used in expressions:
let x: any = 1;
let d = 100 + x as int; // omitting cast would produce an error
Union types
Types can be composed to form a union. Works similarly to Typescript's Union Types.
let mut x: str|nil = nil;
x = "string";
fn triple(n: int|float) -> int|float {
return n * 3;
}
triple(2);
triple(2.2);
struct Foo {
bar: str|int
}
Type aliases
Type aliases can be created with a type declaration statement.
type Celcius = int;
// works with union types as well
type IntStrVec = vec<int>|vec<str>;
Type alias behaves like a normal type and can be used wherever a type is expecte.
Control Flow and Loops
Parentheses are not needed around conditions. The statement body must be enclosed in curly braces.
If
if
statement is rather classic. It supports else if
and else
branches.
if x >= 100 {
println("x is more than 100");
} else if x <= 100 && x > 0 {
println("x is less than 100, but positive");
} else {
println("x a non-positive number");
}
Match
match
expression returns the first matching arm evaluated value.
Unlike other control flow statements, match
is an expression and therefore must be terminated with a semicolon.
let direction = match get_direction() {
"north" => 0,
"east" => 90,
"south" => 180,
"west" => 270,
};
match true
can be used to make more generalised comparisons.
let age = 40;
let description: str = match true {
age > 19 => "adult",
age >= 13 && x <= 19 => "teenager",
age < 13 => "kid",
};
match
can be used with enums
enum HttpStatus {
NotFound,
NotModified,
Ok
}
type Code = int;
impl HttpStatus {
fn code(status: Self) -> Code {
return match status {
Self::NotFound => 404,
Self::NotModified => 304,
Self::Ok => 200,
};
}
}
fn main() {
let status: Code = HttpStatus::code(HttpStatus::Ok); // 200
}
While
There are three loops in Oxide: while
, loop
and for
.
Loops support break
and continue
statements.
while
statement is rather usual.
while x != 100 {
x += 1;
}
Loop
loop
looks like Rust's loop
. Basically it is while true {}
with some nice looking syntax.
loop {
x.push(0);
if x.len() > 100 {
break;
}
}
For
for in
loops are used to iterate over a vector.
for x in 0..=100 {
println(x);
}
or with an index:
for pos, name in vec["John", "Johann", "Jane"] {
println(pos as str + ": " + name); // 0: "John" ...
}
There is also a good old C-like for
loop.
for let mut i = 0; i < v.len(); i += 1 {
println(v[i] as str);
}
Like in C, the first or the last parts can be omitted, or even all three of them for ;; {}
.
Functions
Functions are declared with a fn
keyword.
Function signature must explicitly list all argument types as well as a return type.
Functions that do not have a return
statement implicitly return nil
and the -> nil
may be omitted.
Each function is of fn(T) -> T
type.
fn add(x: int, y: int) -> int { // typeof(add) = fn(int, int) -> int
return x + y;
}
fn clone(c: Circle) -> Circle { // typeof(clone) = fn(Circle) -> Circle
return Circle {
radius: c.radius,
center: c.center,
};
}
fn log(level: int, msg: str) { // typeof(log) = fn(int, str)
println(
"Level: " + level as str + ", message: " + msg
);
}
Defining a function argument as mut
lets you mutate it in the function body. By default, it is immutable.
/// compute the greatest common divisor
/// of two integers using Euclids algorithm
fn gcd(mut n: int, mut m: int) -> int { // typeof(gcd) = fn(int, int) -> int
while m != 0 {
if m < n {
let t = m;
m = n;
n = t;
}
m = m % n;
}
return n;
}
gcd(15, 5); // 5
Closures
Functions are first-class citizens of the language, they can be stored in a variable of type fn(T) -> T
, passed to or/and returned from another function.
/// function returns closure
/// which captures the internal value i
/// each call to the closure increments the captured value
fn create_counter() -> fn() { // typeof(create_counter) = fn() -> fn()
let mut i = 0;
return fn () { // returns closure
i += 1;
println(i as str);
};
}
let counter = create_counter(); // type is inferred as fn() -> fn()
counter(); // 1
counter(); // 2
counter(); // 3
Functions can be passed by their name directly
fn str_concat(a: str, b: str) -> str {
return a + b;
}
fn str_transform(
callable: fn(str, str) -> str,
a: str,
b: str
) -> str {
return callable(a, b);
}
str_transform(str_concat, "hello", " world");
Immediately Invoked Function Expressions, short IIFE, are also supported for whatever reason.
(fn (names: vec<str>) {
for pos, name in names {
println(pos as str + ": " + name);
}
})(vec["Rob", "Sansa", "Arya", "Jon"]);
// 0: Rob
// 1: Sansa
// ...
Structs
Structs represent the user-defined types. Struct declaration starts with struct
keyword.
All struct properties are mutable by default.
You can make property public with a pub
keyword. Public fields can be accessed from outer scope.
struct StellarSystem {
pub name: str|nil, // public field
pub planets: vec<Planet>, // public vector of structs
pub star: Star,
age: int // private field
}
struct Star {
pub name: str,
mass: int,
}
struct Planet {
pub name: str,
mass: int,
belt: bool,
}
Struct implementation starts with impl
keyword.
While struct declaration defines its properties, struct implementation defines its methods, static methods and constants.
self
keyword can be used inside methods and points to the current struct instance. i.eself.field
Self
(capitalised) keyword can be used inside methods to point to the current struct name, i.e.Self::CONSTANT
orSelf::static_method(x)
, it can be used as a type as welllet p: Self = Self { .. };
You can make methods and constants public with a pub
keyword. Public methods and constants can be accessed from outer scope.
Methods with self
as the first argument are instance methods. Methods without it are static methods.
impl Star {
pub const WHITE_DWARF: float = 123.3; // public associated constants
pub const NEUTRON_STAR: float = 335.2; // lets pretend those values are real
pub const BLACK_HOLE: float = 9349.02;
const MAX_AGE: int = 99999; // private constant
pub fn new(name: str, mass: int) -> Self { // public static method
return Self {
name: name,
mass: mass,
};
}
pub fn get_description(self) -> str { // public instance method
return match true {
self.mass <= Self::WHITE_DWARF => "white dwarf",
self.mass > Self::WHITE_DWARF && self.mass <= Self::BLACK_HOLE => "neutron star",
self.mass >= Self::BLACK_HOLE => "black hole",
};
}
}
impl Planet {
pub fn set_new_mass(self, mass: int) {
self.mass = mass;
}
fn is_heavier(self, p: Self) -> bool { // private method
return self.mass > p.mass;
}
}
You need to initialize all structs properties on instantiation.
let planet_mars: Planet = Planet {
name: "Mars",
mass: 100,
belt: false,
};
let system = StellarSystem {
name: "Solar System",
star: Star { name: "Sun", mass: 9999 },
planets: vec[
planet_mars, // via variable
Planet { name: "Earth", belt: false, mass: 120 } // via inlined struct instantiation
],
age: 8934,
};
let arcturus = Planet::new("Arcturus", 4444); // creating instance via static method
Dot syntax .
is used to access structs fields and call its methods.
// set new value
system.name = "new name";
system.planets.push( Planet {
name: "Venus",
belt: false,
mass: 90,
});
// get value
let mars_name: str = system.planets[0].name;
// call method
let desc: str = system.star.get_description();
system.planets[0].set_new_mass(200);
::
is used to access constants and static methods:
impl Star {
pub const WHITE_DWARF: float = 123.3;
const MAX_AGE: int = 99999;
// inside methods Self:: can be used instead
pub fn get_max_age() -> int {
return Self::MAX_AGE;
}
}
let dwarf_mass: int = Star::WHITE_DWARF;
let max_age: int = Star::get_max_age();
Same as in Rust, non-static methods can be called using ::
as well:
let desc = Star::get_description(system.star);
// same as
let desc = system.star.get_description();
Immutable variable behave similarly to Javascript const
that holds an object, so it will still let you change the object fields.
system.name = "new name"; // valid
system.planets[0].name = "new name"; // also valid
system = StellarSystem { ... }; //! error, "system" cannot point to another struct
Structs are always passed by reference, consider:
fn rename_system(s: StellarSystem) {
s.name = "new name";
}
rename_system(system);
println(system.name); // "new name"
Public and Private
Only public properties, methods and constants can be accessed from outer code.
system.age = 100; //! access error, "age" is private
system.planets[0].mass; //! access error, "mass" is private
system.planets[0].is_heavier(p); //! access error, "is_heavier()" is private
Star::MAX_AGE; //! access error, "Star::MAX_AGE" is private
Traits
Traits are similar to Rust traits and are used to define shared behavior.
Because all trait methods are always public they are defined with no pub
keyword.
trait Shape {
fn calc_area(self) -> float;
fn calc_perimeter(self) -> float;
}
Trait body lists function signatures that must be implemented when implementing the trait:
impl Shape for RightTriangle {
fn calc_area(self) -> float {
return self.a * self.b / 2;
}
fn calc_perimeter(self) -> float {
return self.a + self.b + self.c;
}
}
impl Shape for Rectangle {
fn calc_area(self) -> float {
return self.a * self.b;
}
fn calc_perimeter(self) -> float {
return (self.a + self.b) * 2;
}
}
All structs that implement the Shape
trait can be used wherever a shape is expected:
fn print_shape_values(shape: Shape) {
let area = shape.calc_area();
let perimeter = shape.calc_perimeter();
println("The area is " + area as str);
println("The perimeter is " + perimeter as str);
}
let a = Rectangle::new(10, 30);
print_shape_values(a);
Enums
Same as structs, enums represent user-defined types. Enums are quite simple and similar to C-style enums.
enum TimeUnit {
Seconds,
Minutes,
Hours,
Days,
Months,
Years,
}
let days = TimeUnit::Days; // inferred type as "TimeUnit"
impl
blocks can be used to implement static methods and constants on enums.
impl TimeUnit {
pub fn plural(time: Self) -> str {
return match time {
Self::Seconds => "seconds",
Self::Minutes => "minutes",
Self::Hours => "hours",
Self::Days => "days",
Self::Months => "months",
Self::Years => "years"
};
}
}
TimeUnit::plural(days); // "days"
Equality of enum values can be checked with ==
and !=
or with match
days == TimeUnit::Days; // true
TimeUnit::Years == TimeUnit::Hours; // false
Different types of enum values are not compatible and comparing them will trigger an error
enum Ordering {
Less,
Equal,
Greater
}
Ordering::Less == TimeUnit::Days; //! type error
Vectors
Vectors, values of type vec<T>
, represent arrays of values and can be created using vec[]
syntax, where T
is any Oxide type.
Vectors have built-in methods:
vec.push(val: T)
push value to the end of the vectorvec.pop() -> T
remove value from the end of the vector and return itvec.len() -> int
get vectors length
let planets = vec["Mercury", "Venus", "Earth", "Mars"];
planents.push("Jupiter"); // "Jupiter" is now the last value in a vector
let jupiter = planets.pop(); // "Jupiter" is no longer in a vector.
let mars = planets[3]; // mars = "Mars"
planets[2] = "Uranus"; // "Earth" is gone. "Uranus" is on its place now
planets.len(); // 3
typeof(planets); // vec<str>
Variables can be either declared with the type
let v: vec<int> = 0..10; // typeof(v) = vec<int>
let v: vec<Dog>; // typeof(v) = vec<Dog>
or it can be inferred if the type is omitted:
let v = vec[true, false]; // typeof(v) = vec<bool>
let v = vec[ // typeof(v) = vec<Dog>
Dog { name: "dog1" },
Dog { name: "dog2" },
];
let v = vec[ // typeof(v) = vec<vec<Point>,
vec[
Point { x: 1, y: 1 },
Point { x: 0, y: 3 }
],
vec[
Point { x: 5, y: 2 },
Point { x: 3, y: 4 }
],
];
let matrix = vec[ // typeof(v) = vec<vec<int>>
vec[1, 2, 3, 4, 5],
0..=5,
0..6,
];
let things = vec[ // typeof(v) = vec<any>
Ordering::Less,
false,
Point {}
];
Like structs, vectors are passed by reference.
Consider this example of an in place sorting algorithm, selection sort, that accepts a vector and sorts it in place, without allocating memory for a new one.
fn selection_sort(input: vec<int>) {
if input.len() == 0 { return; }
let mut min: int;
for i in 0..(input.len() - 1) {
min = i;
for j in i..input.len() {
if input[j] < input[min] {
min = j;
}
}
if min != i {
let temp = input[i];
input[i] = input[min];
input[min] = temp;
}
}
}
Range Expressions
The ..
and ..=
operators will construct a vec<int>
and fill it will the sequential integers.
let x = 0..=5; // typeof(x) = vec<int>, [0, 2, 3, 4, 5]
let x = 0..5; // typeof(x) = vec<int>, [0, 2, 3, 4]
Ranges can be used in for in
loops:
for x in 1..15 {
println(x as str);
}
Constants
Constants are top-level instructions like fn
, trait
, struct
, impl
. Redeclaring a constant results in a runtime error. Constants must hold only a scalar value: str
, int
, float
, bool
.
const SOME_THRESHOLD = 100;
const EPSILON = 0.004;
const MESSAGE: str = "hello world";
Struct implementations (impl
blocks) can also define constants. Those constants can be either public or private.
struct Math {}
impl Math {
pub const PI = 3.14159265; // public const
const E = 2.71828182846; // private const
pub fn get_e() -> float {
return Self::E; // Self:: is the same as Math:: inside methods
}
}
Accessing private consts from outer scope will result in an error.
let pi = Math::PI; // ok
let e = Math::E; //! access error
let e = Math::get_e(); // ok
Operators
Unary
!
negates boolean value-
negates number
Binary
&&
,||
logic, operate onbool
values<
,>
,<=
,>=
, comparison, operate onint
,float
values==
,!=
equality, operate on values of the same type-
,/
,+
,*
,%
math operations on onint
,float
values&
,|
,^
bitwise operations on integers+
string concatenationas
type cast operator, used to convert primitives to some type:30 as bool
..
,..=
range operators, create avec<int>
value=
,+=
,-=
,/=
,%=
,*=
,&=
,|=
,^=
various corresponding assignment operators
Comments
Classic comments that exist in most other C-like languages.
// inline comments
/*
multiline comment
*/
let x = 100; /* inlined multiline comment */ let y = x;
Standard library
A small set of built-in functionality is available anywhere in the code.
typeof(val: any) -> str
returns type of given value or variableargs() -> vec<str>
returns an array of arguments passed to scriptdbg(val: any)
dumpsval
as a string to the standard output stream (stdout).print(msg: str)
printsmsg
to the stdout.println(msg: str)
same asprint
, but inserts a newline at the end of the string.eprint(err: str)
printserr
to the standard error (stderr).eprintln(err: str)
you got the idea.timestamp() -> int
returns current Unix Epoch timestamp in secondsread_line() -> str
reads user input from standard input (stdin) and returns it as astr
file_write(file: str, content: str) -> str
writecontent
to a file, creating it first, should it not exist