ortfero
almac is the language you write programs in on aldan. it's small, imperative, and tries not to do anything you didn't ask for — nothing is allocated, nothing is collected, and nothing fails without saying so. this walks you through it in fifteen short sections. when you want the exact rules, the formal grammar lives in => almac_report.moff almac language report.
every .alm file is one module. its name is the file's basename — `notes.alm` becomes a module called `notes`, and that's how other modules refer to it.
the layout is fixed. imports first if you have any. then your public declarations. then a line with just `...`, and below that the bodies of those declarations plus anything else that's nobody else's business.
[ import ... ]
{ declarations } -- public
[ ...
{ definitions } ] -- private
importing is a list of paths without the `.alm` extension:
import 'core/random, 'core/tty
a const is a name for a value known at compile time — a literal, another const, or a type (in which case you get the type's size in bytes).
const max_items = 64 const greeting = "hello, aldan" const pi_third = 1.047197551 const u8_size = u8 -- s64, value 1
a failure is a closed set of kinds you can raise. failures are module-local, just like types: you declare one in your module and others reach it qualified as `yourmod.thefault`. the standard library keeps the common faults in small owner modules, each declaring one failure named `fault` — `storage.fault`, `logic.fault`, `bytes.fault`, `resource.fault` (see appendix b.2 of the report) — so you import the owner and write `storage.fault`. declaring your own is the same shape:
failure account { disk_full, no_such_user }
a failure can also be an alias to another, written with `as` — the failure twin of `type x as y`. `failure io as storage.fault` makes `io` a second name for `storage.fault`, the same failure under two names: you can `raise` under one and handle `on` the other, and `io` borrows the target's kinds (`io.not_found`). like a type alias, it takes no `{ ... }` of its own.
failure io as storage.fault
`type` names a type. you only have to do this when the type refers to itself by pointer — otherwise the structure is fine inline wherever you use it.
type byte_count as s64 type bitset as b64 type buffer [256]u8 type lineview []u8
an alias to another *named* type takes `as` — `type byte_count as s64`. a structural right-hand side (an array, slice, record, enum, union, pointer, or `fn` type) takes none, as above. the `as` is only for the declaration; everywhere else you name a type bare (`var n byte_count`), and `as` there is the cast operator.
function prototypes go up here too. the bodies live below the `...`.
fn add s64 (a s64, b s64) fn fill (buffer& []u8, c u8) fn read_line (h file.id, out& string)
a prototype may end with `as module.fn` to *re-export* an imported function — the function twin of `type x as y`. it binds the name to that imported function (the signature must match), so importers of your module call it as if you wrote it. no body follows; the import supplies the code.
import 'core/file fn open file.id (path string) raises storage.fault as file.open
everything from the `...` down is private. you can mix declarations and definitions freely below it; this is where most of the code lives in any non-trivial module. the separator is the first top-level `...`, written alone on its own line:
...
identifiers are a letter followed by letters or digits. underscores count as letters. a handful of words are reserved and can't be used as identifiers — the report's section 3 has the full list.
the operators:
= == != < <= > >= + - * / += -=
^ & | . , ; : ( ) [ ] { }
a few do double duty:
function bodies, `for` bodies, `if` branches, and record field lists are all brace-delimited: `{ ... }`. inside the braces every simple statement ends with its own `;`. the `:` appears only in slice syntax (`a[lo:hi]`) and after a handler's failure list (`on f:`).
bool 1 byte s64 signed 64-bit integer u8 unsigned 8-bit integer f64 ieee-754 double address untyped 8-byte machine address b64 64-bit bitfield string alias for []u8 integer the type of an unconverted number literal
`true`, `false`, and `none` are the literals you have. `none` is the nil pointer — it slots into any pointer type or into `address`.
`integer` is the type a number wears until something pins it down. `count = 0` works whatever `count`'s type is, because the `0` adopts that type.
fn scalar_demo ()
var flag bool, count s64, byte u8,
ratio f64, bits b64 {
flag = true;
count = -123;
byte = 0xff;
ratio = 6.28;
bits = 0xdeadbeef;
}
a record is a struct with named fields and natural alignment. it can hold a pointer to itself — and that's the one case where you have to give the type a name before using it (otherwise the recursion has nothing to refer to).
pointers are 8 bytes. they're either `none` or the address of a value of their base type. deref is postfix `^`.
type node record {
value s64,
next ^node -- self-pointer
}
fn list_head_value s64 (head ^node) {
if head == none { return 0; }
return head^.value; -- deref, then field
}
`[n]t` is a fixed-length array. `[]t` is a slice — a (data, length) pair, 16 bytes, 8-byte aligned. an array becomes a slice wherever a slice is expected; no cast needed.
`a[lo:hi]` slices a range. the storage is shared — the slice is a view, not a copy. on a `b64` the same syntax pulls out the bits in positions lo..hi-1.
assigning slices copies the (data, length) pair, not the elements.
fn slice_demo ()
var fixed [4]s64, view []s64, tail []s64 {
fixed[0] = 1; fixed[1] = 2;
fixed[2] = 3; fixed[3] = 4;
view = fixed; -- array -> slice
tail = view[1:3]; -- borrows fixed[1..3]
tty.say "view len: ", (length view), "tail len: ", (length tail);
}
an enum is a list of named tags. it's `u8`-sized, and distinct from every other enum — you can't accidentally mix them.
a tagged union is the same idea with a payload per variant: tag at offset 0, payload area after, sized to the biggest variant. each variant `v` names a subtype `u.v` — the full union size, but fixed to that variant. a variable of a subtype exposes only that variant's fields; a variable of the union type exposes none. build a value through a subtype, then pass it where the union is expected (by reference, no copy). to go the other way — union to subtype — narrow with the `guard` statement: `guard u u.v | escape`. on a tag match it retypes `u` to the subtype for the rest of the block, so `u.field` works directly; on a mismatch it takes the escape (`return` or `raise`).
type colour enum red | green | blue
type shape union
point
| circle { radius f64 }
| rect { width f64, height f64 }
fn make_circle (c& shape.circle, r f64) {
c.radius = r; -- subtype: only circle's fields
}
fn area f64 (s shape) { -- a union: guard before use
guard s shape.circle | return 0.0;
return float.pi * s.radius * s.radius;
}
module-level `var` lives in the module's bss, zero-initialised. function locals appear between the signature and the body's `{`, introduced by the same `var` keyword — one `var` then a comma-separated list of `name type` pairs. locals are zero-initialised, like module vars — which is also why neither may contain a non-null pointer (`^t`): a zeroed one would be null. use `^t|none` and `guard`.
var instances s64 var palette [3]colour
a function is `[inline] fn name signature [var locals] body`. the signature is an optional return type, the parameter list in parens, and an optional `raises` marker. an `inline` marker, if present, leads the whole thing (`inline fn ...`, see 10.3). locals, introduced by `var`, come right after the signature, then the body.
fndef = [ "inline" ] "fn" ident signature
( "..." | [ "var" vardefs ] ( body | asmbody ) ) .
body = "{" [ stmts ] { handler } "}" .
asmbody = "asm" [ "naked" ] compound .
handler = "on" ident { "," ident } ":" stmts .
compound = "{" [ stmts ] "}" .
stmts = stmt { stmt } .
stmt = return | defer | raise | for | if | guard | break
| continue | ( expr ";" ) .
return = "return" expr ";" .
vardefs = vardef { "," vardef } .
vardef = ident type .
a body-less signature is a forward declaration. in the public section (before the `...`) that is a plain prototype: `fn foo ()`, defined later below the `...`. inside the body section a definition normally carries a body, so a forward declaration there — a *deferred definition*, defined later in the section — is marked with a trailing `...`: `fn foo () ...`. note the two uses are distinct: a standalone `...` on its own line is the section separator, while a `...` trailing a signature is a deferred definition. this lets you declare a group and define them top to bottom in calling order. a body-less signature without `...` in the body section (or at the prompt) is incomplete — the prompt simply asks for the next line.
remember: each simple statement carries its own `;`; the braces carry none. compound-form statements (`for`, `if`) end at their body's closing `}` and don't carry a `;` of their own.
fn add s64 (a s64, b s64) {
a + b;
}
fn greet (who string) {
tty.say "hi,", who;
}
put `&` after a parameter name and the caller must supply a writable lvalue with `&`. without the `&`, the parameter is read-only inside the function.
scalars pass by value, aggregates always pass by reference — the `&` is about *writability*, not how things get there.
fn fill (buffer& []u8, c u8)
var i s64 {
i = 0; for i != length buffer; i += 1 {
buffer[i] = c;
}
}
fn fill_caller ()
var page [256]u8 {
fill page&, 0x20; -- `&` at the call site too
}
a function can't return an aggregate (array, record, slice, or tagged union) by value. when you need one out, pass it as an out-param and write into it.
type point record { x f64, y f64 }
fn make_point (p& point, x f64, y f64) {
p.x = x;
p.y = y;
}
a leading `inline` expands the body at the call site. that means the body can't declare locals or use `return`, `defer`, `raise`, `for`, `break`, or `continue` — just an expression. `inline` isn't part of the function type, so it leads the declaration rather than trailing the signature: you can't have a first-class `inline fn` value, can't take its address, and can't alias it.
inline fn min2 s64 (a s64, b s64) {
if a < b { a; } else { b; }
}
a signature-only declaration binds the name with no code; a later definition with the same signature fills in the body (a *different* signature replaces the binding — handy at the repl, a footgun elsewhere). in the public section it is a plain prototype. inside the body section a definition normally carries a body, so a forward declaration there — a *deferred definition* — is marked with a trailing `...`. that lets you write callers above callees, in reading order:
fn helper s64 (x s64) ... -- deferred: defined below
fn run s64 () { helper 41; } -- calls helper before it appears
fn helper s64 (x s64) { x + 1; } -- the body, filled in here
don't confuse the trailing `...` (deferred: a body follows later) with a trailing `as target` (a *function alias*: no body at all, the target is the code). an alias names an existing function — a local one already defined, or an imported `m.f` — the function analogue of a type or failure alias. the target must already be defined, and the signature must match it exactly.
fn inc s64 (x s64) { return x + 1; }
fn bump s64 (x s64) as inc -- alias: bump is inc
`asm { ... }` hands the body to the rv64 assembler verbatim. normally the compiler wraps it in a prologue and epilogue — unless the body needs no frame (it touches neither `fp` nor `ra`, and the function has no locals or `with`), in which case it's emitted as a frameless leaf: the arguments stay in their registers and a lone `ret` is appended.
`asm naked` forces that frameless form even when the body *does* use `fp` or `ra`. a naked body owns its frame end to end — it saves and restores `ra`/`sp` itself and supplies its own `ret` (or a tail jump into another context), and the compiler adds nothing around it to corrupt. that's what a context switch needs. `naked` isn't a reserved word; it's recognised only right after `asm`.
fn nop () asm { nop }
operator precedence, tightest first: postfix (call, index, field, deref), cast (`as`), unary `-`, multiplicative (`*`, `/`, `mod`), additive (`+`, `-`), relational (`==`, `!=`, `<`, `<=`, `>`, `>=`), assignment (`=`, `+=`, `-=`).
function calls have no parentheses around their arguments — the arg list starts at the first atom after the callee and ends at the first token that isn't an atom or a comma:
and b0, b1, ... or b0, b1, ... not b
a few rules:
fn expr_demo s64 (x s64, y s64)
var z s64, ok bool {
z = (x + y) * 2;
z += 1;
ok = and (x > 0), (y > 0);
if not ok { return -1; }
return z mod 10;
}
`if` is both a statement and an expression. as an expression every branch has to be there and they all have to yield the same type:
fn sign s64 (x s64) {
return if x > 0 { 1; }
else if x < 0 { -1; }
else { 0; };
}
`as` is the explicit conversion. it covers:
integer <-> integer integer <-> f64 (f64 -> int truncates toward zero) scalar -> bool (non-zero is true) [n]t -> []t (array to slice) pointer <-> address
widening from `s64` sign-extends; widening from `u8` zero-extends. narrowing to `u8` keeps the low 8 bits.
fn cast_demo ()
var c u8, n s64, x f64, flag bool {
c = 200;
n = c as s64; -- zero-extended
x = n as f64;
flag = c as bool; -- non-zero -> true
}
`b64` supports two postfix forms — `b[i]` for a single bit (yields `bool`) and `b[i:j]` for a range of bits (yields a `b64`). the `bit` module has the rest: `shil`, `shir`, `incl`, `excl`.
fn bitfield_demo b64 (b b64) {
if b[7] { return b[0:8]; } -- low byte
return 0;
}
`for` is the only loop. both the condition and the step are optional. dropping the condition gives an infinite loop. `break` exits; `continue` jumps to the step (or the condition, if there's no step).
fn count_down (n s64)
var i s64 {
i = n; for i > 0; i -= 1 {
tty.say i;
}
}
fn global_should_stop bool () {
return false;
}
fn forever () {
for {
if global_should_stop { break; }
}
}
`defer expr` schedules `expr` to run when the function returns — whether normally or via a failure. defers run in reverse order, last registered first. up to eight per function. if the deferred expression is a function value, it's auto-called with no args.
fn open_and_print (path string) raises storage.fault
var h file.id {
h = file.open path;
defer file.close h;
cat path;
}
`raise f.k` abandons the current function, marking failure `f` active with kind `k`. registered defers run first, then control transfers to a handler — if there is one — or propagates to the caller.
`raises` lists the failures — never kinds — a function can propagate, and is part of its type. you mark a function `raises f` if (and only if) some kind of `f` can escape its body, whether from a `raise` or a call to a raising function. the compiler tells you if you forget a failure or declare one that can't escape.
handlers live at the end of a function body, before the body's closing `}`. each one is `on f: stmts` (a whole failure) or `on f.k: stmts` (one kind) — no own close, the body's `}` closes both the last handler's stmts and the body. completing a handler converts the failure into a normal return.
fn parse_or_zero u8 (s string)
var v u8 {
v = byte.parse s;
return v;
on bytes.fault:
return 0;
}
fn save_user (name string) raises logic.fault, account {
if length name == 0 { raise logic.fault.invalid_argument; }
if instances >= max_items { raise account.disk_full; }
instances += 1;
}
`parse_or_zero` needs no `raises`: `byte.parse` can raise `bytes.fault`, but the `on bytes.fault` handler discharges it. `save_user` declares the failures it lets escape — the library's `logic.fault` (from `core/logic`) and your own `account`. a failure name in `raise`/`raises`/`on` may carry a module qualifier (`logic.fault`) or be bare when it is your own module's failure, exactly like a type name.
the built-in vocabulary is small. these four forms, and the primitive type names, are the only things in scope without an import:
length x element count of an array or slice and / or / not short-circuit logical ops
everything else that used to be a compiler builtin is now an ordinary standard-library module written in almac — you import it and qualify its members. printing is the one that catches people out: `say`, `sayin`, and `ask` are not keywords, they live in `tty`, so it's `tty.say` after `import 'core/tty`. allocation is in `heap` (`heap.claim` returns the address of a fresh block, `heap.release` frees it). the shell-style file ops — cd, ls, mkdir, rmdir, rm, cat, touch, cp, mv — aren't builtins either: the repl defines them in rc.alm over the file and directory modules.
import 'core/tty, 'core/heap
fn predef_demo () raises resource.fault
var buffer address, reply [64]u8 {
buffer = heap.claim 1024;
defer heap.release buffer;
tty.say "hello,", (length "hello,"), " bytes long";
tty.sayin (tty.ask "your name? ", reply&);
}
these all live under core/ in the source tree, so you import them by path — `import 'core/file` — and the local name is the basename (`file.open`). they're written in almac, not magic: the only thing the host supplies is the firmware and kernel boundary, which two interface modules (core/machine/aldan and core/platform/aldos) wrap, and the rest is layered on top. the report has the full signatures; here's the elevator pitch for each:
byte single-byte access and conversion ('x')
bytes operations on byte slices -- clear, fill, copy, move,
equal, compare, parse, format. copy is a forward
memcpy; move is safe for overlapping ranges.
bit b64 shift, xor, and bit/range include/exclude
signed s64 arithmetic, conversion, limits
float f64 arithmetic, trig/log/exp, constants
boolean parse / format for bool, and []bool fills
heap claim / release memory; query bytes available
random splitmix64 prng
clock monotonic timing
time wall clock and calendar decomposition
file open, read, write, seek, close, etc.
directory make, remove, iterate, query
tty console output and input -- say / sayin / ask
text formattable union; format values into a buffer
debug trace one diagnostic line to the tracer device
screen off-screen text canvas + keyboard
sound pcm audio playback
task cooperative yield
storage the storage.fault failure (files / directories)
logic the logic.fault failure (contract violations)
resource the resource.fault failure (out of memory)
bytes also owns bytes.fault (parse / format errors)
read a whole file into a buffer:
fn read_all (path string, out& string) raises storage.fault, bytes.fault
var h file.id, size s64 {
size = file.size path;
if size > length out { raise bytes.fault.buffer_overflow; }
h = file.open path;
defer file.close h;
file.read h, out&;
}
time a block:
fn time_loop s64 ()
var t clock.tick, i s64 {
t = clock.now;
i = 0; for i < 1000; i += 1 {
-- ... work ...
}
return clock.elapsed_us t;
}
a program is a module with an entry function the host calls — by convention, `run`. this one reads a name, greets, and stops on empty input. `tty.ask` reads a line into the buffer and returns the bytes read; neither it nor `tty.say` raises, so `run` needs no `raises`:
import 'core/tty
fn run ()
var name [64]u8, line string {
for {
line = tty.ask "name? ", name&;
if length line == 0 { break; }
tty.say "welcome, ", line;
}
}
that's the language. when you want chapter and verse, the formal grammar is in => almac_report.moff almac language report.