Oak is a small dynamically typed scripting language. Source is parsed into bytecode and executed by a stack-based VM written in C17. The CLI and the browser playground use the same core library; the web version runs it through WebAssembly.
meson setup build
meson compile -C build
./build/oak path/to/script.oak [script args...]Useful CLI flags:
| Flag | Purpose |
|---|---|
--disassemble |
print bytecode before running |
--no-debug |
compile without debug metadata |
--track-memory |
fail if tracked runtime allocations leak |
--help |
show usage |
Run tests with:
meson test -C buildThe test suite includes C harnesses under tests/ and smoke tests for the
top-level examples in examples/.
meson setup build_wasm --cross-file meson/cross/emscripten.ini
meson compile -C build_wasm
npm install
npm run devlet x = 42;
let mut y = 10;
y += 5;
if y > x { print(y); } else { print(x); }
while y > 0 { y -= 1; }
for i from 0 to 10 { print(i); }
for value in [2, 3, 5] { print(value); }
for i, value in [2, 3, 5] { print(i + value); }
Core value types are number, string, bool, arrays, maps, records, enums,
functions, none, and weak references. Strings are single-quoted.
Operators include arithmetic (+ - * / // %), comparison, !, and short-circuit
logic (&&, ||, and, or, not).
Functions live at module scope:
fn add(a : number, b : number) -> number {
return a + b;
}
Records are data-first. Methods are declared separately:
record Point {
x : number;
y : number;
}
fn Point.dist_sq(self, other : Point) -> number {
let dx = self.x - other.x;
let dy = self.y - other.y;
return dx * dx + dy * dy;
}
fn Point.move_by(mut self, dx : number, dy : number) {
self.x += dx;
self.y += dy;
}
Enums lower to integers:
enum Color { Red, Green, Blue }
print(Color.Green); /* 1 */
Arrays and maps are mutable through methods and indexing:
let mut nums = [] as number[];
nums.push(10);
nums.push(20);
print(nums[0]);
let mut scores = [:] as [string:number];
scores['alice'] = 95;
print(scores.has('alice'));
for name, score in scores { print(name + ': {}'.format([score])); }
scores.delete('alice');
Traits describe required methods and support dynamic dispatch:
trait Shape {
fn area(self) -> number;
}
record Rect { w : number; h : number; }
fn Rect.area(self) -> number { return self.w * self.h; }
let r = new Rect { w: 4, h: 5 };
let mut shapes = [] as Shape[];
shapes.push(r);
print(shapes[0].area());
none is a first-class empty value. Weak references use Type weak; expired
weak references compare like none.
record Node { next : Node weak; }
let empty = new Node { next: none };
print(empty.next == none);
Modules use selective or wildcard imports; all top-level declarations are public:
/* In util/math.oak */
fn add(a : number, b : number) -> number { return a + b; }
/* In main.oak */
import { add } from util.math; /* selective */
import * from io; /* wildcard — all declarations */
import { add as sum } from util.math; /* rename on import */
Attributes attach metadata to declarations and are interpreted by embedding C code:
@Deprecated
fn old_name() {}
Built-in collection and string methods:
| Method | Receiver | Purpose |
|---|---|---|
.size() |
array, map, string | length |
.push(v) |
array | append and return new size |
.has(k) |
map | key lookup |
.delete(k) |
map | remove key |
.format(args) |
string | {} / {n} substitution |
Number helpers:
| Function | Purpose |
|---|---|
to_int(v) / to_float(v) |
numeric conversion |
is_int(v) / is_float(v) |
inspect numeric storage |
sqrt, sin, cos, tan |
basic math (import * from math;) |
abs, fmod, min, max, random |
more math helpers |
File I/O lives in io:
import * from io;
let f = File.open('notes.txt', FileMode.Read);
let text = f.read_all();
f.close();
print(text);
File also exposes read(), write(value), eof(), and close().
Oak source uses:
| Thing | Style | Example |
|---|---|---|
| variables, functions, methods, fields | snake_case |
collect_primes, point_count |
| records, traits, enums | PascalCase |
FileHandle, FileMode |
| enum variants, attributes | PascalCase |
FileMode.Read, @Deprecated |
Native bindings are registered on oak_compile_options_t before
oak_compile_ex(). Use oak_bind_type* for native records,
oak_bind_fn_global() for global or module functions, oak_bind_fn() for
methods, oak_bind_enum* for native enums, and oak_bind_attr() for
attributes.
struct oak_compile_options_t opts;
oak_compile_options_init(&opts, allocator);
struct oak_bind_type_t* file_type =
oak_bind_type_in_module(&opts, "io", OAK_BIND_TYPE_RECORD, "File");
oak_bind_fn(&opts, &(struct oak_bind_fn_t){
.kind = OAK_BIND_FN_INSTANCE_METHOD,
.receiver_type_id = file_type->type_id,
.name = "read_all",
.impl = file_read_all,
.arity = 0,
.return_type = OAK_BIND_SCALAR(OAK_TYPE_STRING),
});
struct oak_compile_result_t result;
oak_compile_ex(ast_root, &opts, &result);
oak_compile_options_free(&opts);Field, parameter, and return types are described by oak_bind_type_ref_t,
built with the OAK_BIND_* helpers: OAK_BIND_SCALAR(id),
OAK_BIND_ARRAY(elem), and OAK_BIND_MAP(key, value).
Native functions and methods may optionally describe their parameter types with
the param_types / param_count fields so the compiler can type-check call
sites. param_types is borrowed by the compiler and must outlive
oak_compile_ex() (use arrays with static or function-body lifetime, not
temporaries that go out of scope before compilation).
oak_bind_type(&opts, OAK_BIND_TYPE_VALUE, name) registers a non-refcountable
value type: its payload (an opaque pointer or handle) lives directly inside
the 16-byte oak_value_t with no heap wrapper, no reference counting, and no
destructor. Copies are bitwise. Because inline values carry no runtime type
identity, value types expose data through methods only — oak_bind_field
rejects them.
struct oak_bind_type_t* handle =
oak_bind_type(&opts, OAK_BIND_TYPE_VALUE, "Handle");
oak_bind_fn(&opts, &(struct oak_bind_fn_t){
.kind = OAK_BIND_FN_INSTANCE_METHOD,
.receiver_type_id = handle->type_id,
.name = "id", .impl = handle_id, .arity = 0,
.return_type = OAK_BIND_SCALAR(OAK_TYPE_NUMBER),
});Build a value with oak_native_value_new(payload) and recover the payload
inside methods with oak_native_value(args[0]) (the analogues of
oak_native_record_new / oak_native_instance for heap records). Two inline
values compare equal when their payloads are identical.
Native functions use this shape:
enum oak_fn_call_result_t fn(struct oak_native_ctx_t* ctx,
const struct oak_value_t* args,
int argc,
struct oak_value_t* out);Return OAK_FN_CALL_OK on success or OAK_FN_CALL_RUNTIME_ERROR to raise a VM
runtime error. OAK_TYPE_VOID functions may leave *out untouched; the VM
treats the result as none.
Wrap native instances with oak_native_record_new(ctx->allocator, type, ptr);
inside getters, setters, and methods, recover the C pointer with
oak_native_instance(value). If type->destructor is set, it runs when the Oak
wrapper's final reference is released.
Native module types need a matching bodyless Oak stub, for example
stdlib/io.oak:
enum FileMode { Read, Write, Append }
record File;
fn File.open(path : string, mode : FileMode) -> File;
fn File.read(self) -> string;
fn File.read_all(self) -> string;
fn File.write(mut self, value : string);
fn File.eof(self) -> bool;
fn File.close(self);
source -> lexer -> parser -> compiler -> bytecode -> VM