Skip to content

Specification §27

Syntax tour (the language on one page)

language-design.md §27 · 352 lines · 9 min read

The “part 2”: a walk through the whole syntax in commented code, grouped by theme, as a quick reference. Nothing is new, it is the consolidation of sections 1 to 15, written as you would write it. Each block is an executable example; the comments point to the decision behind it.

x := 5 // declares immutable, runtime (default): shortcut for 'let'
let y := 5 // immutable, explicit form (identical to 'x := 5')
var z := 5 // mutable
const N := 64 // compile-time constant (section 10)
name: string = "ana" // explicit type Odin-style; ':=' is ': =' with the type omitted
z = z + 1 // a bare '=' only assigns, and only to a 'var'; the ':' is what declares
// var w := compute(); return w // ERROR: 'w' is var but was never mutated. Use the immutable form
// A single pointer type; 'mut' is write-back. Both require a 'var' target.
p := &z // &place = address of a place
total := *p // *p = deref
fn bump(v: mut int) { v = v + 1 } // write-back parameter
bump(mut z) // mutates z in-place; bump(mut x) would be an ERROR (x is immutable)
loop { break } // no header → infinite
loop active { ... } // boolean expression → while
loop item in list { ... } // IDENT in iterable → for-each
loop i in 0..8 { ... } // exclusive range (..= is inclusive)
loop i := 0; i < 8; i = i+1 { ... } // C-style; ';' delimits the 3 parts (parentheses optional)
// match WITH a subject → matches patterns (the "switch")
match parse_port(input) {
Ok(p) => listen(p)
Err(error.Empty) => use_default()
Err(error.NotNumber) => abort()
Err(error.OutOfRange) => abort() // closed domain: all the variants, '_' would be an error
}
// match WITHOUT a subject → waits for several channels, reacts to the first ready (the "select")
match {
msg := -> chan_a => handle_a(msg)
msg := -> chan_b => handle_b(msg)
} timeout(1s) {
give_up()
}
defer file.close() // deterministic cleanup; runs on scope exit, LIFO
// Modifiers: pub? comptime? unsafe? fn ('@generator' is a decorator: above or inline)
pub fn add(a: int, b: int) -> int { return a + b }
// UFCS: x.f(y) is sugar for f(x, y): pipeline with '.', and lambdas are 'fn(x) => expr'
errors := lines
.filter(fn(l) => l.contains("ERROR"))
.map(fn(l) => parse_entry(l))
// Generic with uniform logic:
fn show[T + Display](x: T) -> string { return x.to_string() }
// Dispatch by type, small variations together → internal 'match T' (closed domain, no '_'):
fn encode[T: {int, bool}](x: T) -> []byte {
match T {
int => return x.to_bytes()
bool => return x.to_bytes()
}
}
// Large or separate variations → external set (router by type, resolved at compile time):
fn to_string = { int_to_string, bool_to_string }
decl User {
id: int // private to the module by default (encapsulated)
name: string
pub email: string // field exposed individually to other modules
}
// enum WITHOUT a payload = simple enumeration / state machine (no iota; each variant is a value):
decl State { Idle; Running; Done }
// enum WITH a payload = sum type (the payload is optional per variant, and can mix):
decl Event { Tick; Click(int, int); Quit }
// exhaustive match over enum: covers all the variants and '_' is a compile error
// (adding a variant breaks the build until you handle it, the '_' would nullify that):
fn step(s: State) -> State {
match s {
Idle => return Running
Running => return Done
Done => return Done
}
}
// match with a payload binds the fields by position; closed domain → all the variants (no '_'):
fn describe(e: Event) -> string {
match e {
Tick => return "tick"
Click(x, y) => return "click at " + x.to_string()
Quit => return "quit"
}
}
// Method = external function with a receiver (Go style); the struct only holds data:
fn (u: User) display() -> string { return u.name + " <" + u.email + ">" }
// Interface = set of methods; implementing the methods ALREADY satisfies, without "implements":
decl Writeable {
fn write(data: []byte) -> error
}
// union = overlapped bytes, always unsafe (reinterpretation):
unsafe union Bits { f: f32; raw: u32 }
flag: Optional[int] // explicit absence; integers i8..usize, byte = u8, IEEE floats
s := "Hello {{name}}!" // UTF-8 string; named interpolation (via Display)
n := s.char_at(0) // one grapheme; there is NO pure 's[i]' (ambiguous → error)
view := s.chars[2..6] // VIEW by graphemes ('..'), keeps s alive; .bytes/.codepoints same
cp := s.chars_copy(2, 6) // sibling method; ':' / '_copy' = O(n) copy
raw := s.to[u16]() // boundary conversion, explicit → []byte
eq := a == b // '==' is byte equality; '.equals(_, .semantic)' for Unicode
var b := "x"; b.append("y") // mutable only with 'var'; growth by doubling
fn parse_port(s: string) -> Result[u16, error{Empty, NotNumber, OutOfRange}] {
if s == "" { return Err(error.Empty) }
// ...
}
// 'catch |e|' handles the error as a unit and ESCAPES (return/panic): propagates or returns a fallback:
port := parse_port(input) catch |e| { return default_port() }
// 'match |e|' branches by variant (trailer; '|e|' is the error), and like catch each arm escapes.
// Single variant → use catch. Recovering WITH a value (giving cfg a default) is match over the Result.
cfg := load() match |e| {
error.NotFound => return e // propagates (sugar → Err(e))
error.Corrupted => runtime.panic("unreadable config") // aborts
}
// Three forms, no overlap: only-values → tuple (string, string); only-error → error-set alone;
// value(s) + error → Result. There is no positional return with an error (an error entered, it is Result):
fn split_pair(s: string) -> (string, string) { ... }
// Channel typed by the perspective of whoever holds the handle: <-T = "I send T", ->T = "I receive T"
c: Channel[<-int, ->string]
spawn worker(c) // the compiler verifies that the sides are complementary
fn worker(c: Channel[<-string, ->int]) {
val := -> c // receives (blocks until it arrives)
c <- val.to_string() // sends (async, fire-and-forget)
}
reply := server <-> request timeout(5s) // sync request-reply; timeout MANDATORY (without it, error)
spawn task(data) catch |e| { log(e) } // process with a failure handler
spawn { // group of processes (supervision emerges from the structure)
indexer(docs) catch |e| { ... }
notifier(subs) catch |e| { ... }
}
fn build_report() -> Report {
@mm(arena) // allocation strategy of this scope: arena | gc | none
buf: HugeBuffer @mm(arena) = alloc_buffer() // (postfix form fixes by-value)
defer free_arena() // @mm(none)/manual frees with defer
// ...
}
@mm(arena) spawn producer(data @transfer) // @transfer invalidates 'data' here; arrives at the destination with its mm
@mm(gc) spawn consumer(chan) // Arena → GC via @transfer is valid
promoted := node @promote(gc, deep) // copies to another MM (shallow is the default; 'deep' explicit)
// 'type' is a first-class value; a generic is a function that receives/returns 'type':
decl List[T] { items: *T, len: usize } // sugar for the comptime fn below
comptime fn List(T: type) -> type {
return decl { items: *T; len: usize }
}
const nums := List[int] // compile-time call → concrete type (monomorphized)
buffer: [N]u8 // '[...]' accepts comptime values, not just types
comptime { // block that runs in the compiler
assert (N <= 1024)
}
const backend := use(if target.arch == x86 { return "backend_x86" } else { return "backend_wasm" })
@generator fn fibonacci() -> int {
var a := 0
var b := 1
loop {
yield a
next := a + b
a = b
b = next
}
}
loop n in fibonacci().take(10) { io.print(n) } // consumed like any iterable
// Boundary contract: pre-condition stacks; post-condition has two equivalent forms.
@requires(i < buf.len)
@requires(buf.len > 0)
@ensures(u8 == buf.bytes[i]) // directive form (stacks, references the return by type)
fn at(buf: Buffer, i: usize) -> u8 { // or terse, in the return type: '-> u8 == buf.bytes[i]'
// the contracted pre-condition makes 'at' SAFE to call (the compiler checks at the call)
return unsafe raw_read(buf.ptr + i) assume "valid ptr: Buffer invariant + bounds from @requires"
}
b := at(buffer, 3) // safe call: the compiler checks 3 < buf.len
// named return: mandatory ONLY when a return type repeats and there is @ensures over it
@ensures(lo > 0)
@ensures(hi > 50)
fn split(...) -> (lo: u8, hi: u8) { return (a, b) } // names = slot labels; return free
// Killing an operation in the body: strong (checks, panic) vs weak (case-B, non-empty reason, audited)
y := unsafe x.raw assume "intentional reinterpretation float→bits"
result := unsafe { // block = expression-scope
v := raw_read(p) assert (p_valid) // later assert sees what came before ('p_valid')
return v * 2 // value goes out by return (expression → goes to 'result')
} assert (p_valid) // shared trailer = entry pre-condition
const c := @cimport("stdio.h") // C enters as a module (comptime) → c.stdio
@cimport("gcc") // fundamental types → c.gcc.int, c.gcc.size_t (the compiler's ABI)
buf: *c.gcc.char = c.stdio.malloc(n) // all qualified under 'c.'; runtime type + header fn
unsafe c.stdio.printf("%d", count) // calling C is unsafe; the variadic is case-B (types on your account)
@repr(c) decl Point { x: c.gcc.int } // C-faithful layout at the boundary (internal structs are optimized)
@pin handle // a @mm(gc) object crossing to C requires @pin (otherwise it does not compile)
mine := data @promote(gc) // C memory (@mm(c)) repatriated to my GC
@callback fn cmp(a: *c.gcc.void, b: *c.gcc.void) -> c.gcc.int { ... } // fn callable by C
@asm(x86, intel) // assembly-function: body is asm, returns a value
unsafe fn add10(v: u32 @asm(in, rax)) -> u32 @asm(out, rbx) {
mov rbx, rax
add rbx, 10
}
var lo: u32; var hi: u32 // inline block: runs asm in the middle of normal code
@asm(x86, intel)
@asm(out, rax) lo
@asm(out, rdx) hi
@asm(clobber, "memory") // EXPLICIT clobbers in the block (the function uses the ABI)
unsafe { rdtsc }
@asm(wasm, wasm) // WASM has no registers: binds by param/local
unsafe fn add(a: u32, b: u32) -> u32 { local.get a local.get b i32.add }
procs := runtime.processes() // cheap snapshot: processes, memory, tree, @mm per process
p := runtime.process("db_writer") // by registered NAME (restart-stable); runtime.group("h") for groups
me := runtime.self() // its own reference, from inside any process
@register("db_writer") // names the process (string, stable); @register("h", group) for groups
spawn db_writer(conn) // a loose 'spawn' registers nothing (fire-and-forget, zero cost)
ev := runtime.trace(p, [.send, .receive, .channel_block], sink: .channel, sample: 100)
loop e in ev { match e { ... } } // capture = ring buffer (cheap); consumption via channel/callback/endpoint
@component
fn Counter(start: int) {
@state n := start // reactive state (decorator, not keyword)
@derived label := "clicks: {{n}}" // recomputes when 'n' changes
fn inc() { n = n + 1 } // named handler
return { // markup in the return block (no implicit return)
<button class="btn" onclick={inc}>{{label}}</button> // handler: { } ; text: {{ }}
<style>
.btn { padding: 8px 16px; } // scoped CSS: '.btn' → '.btn_a3f8' (hashing); typo = error
</style>
}
}
@jsimport("dom", browser) // FFI of JS: all under 'js.'; the environment propagates @server/@client
@css("tailwind.css") // external CSS: global, names preserved, enters the check
@server fn load(id: int) -> Post { return db.get(id) } // @server/@client = restriction; the rest crosses
// islands: only the interactive parts become WASM; the rest is static HTML
// server/client boundary = network channel (net.connect[T], T + Serializable)
// where WASM does not run: transpile of the .mkoui to JS via build --target=js (total parity)
// a file inside a project with mk.project at the root
use myapp/store // local: prefix = package name (absolute, never '../')
use json // stdlib: bare (comes with the compiler)
use github.com/acme/http as web // external: URL form + alias
use json.{parse, decode} // brings specific names into scope
priv fn helper() { ... } // visible only in this FILE
fn internal() { ... } // visible in the MODULE (default)
pub fn api() { ... } // visible to other MODULES
module quickscript // (alternative) transportable single-file module, embedded checksums