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 `implementation`, and below that the bodies of those declarations plus anything else that's nobody else's business.
[ import ... ]
{ declarations } -- public
[ implementation
{ definitions } ] -- private
importing is a list of paths without the `.alm` extension:
import 'marshal, 'random
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
faults are named conditions you can raise. the language reserves nine of them — invalid_format, out_of_range, buffer_overflow, operation_failed, invalid_argument, open_failed, read_failed, write_failed, seek_failed — and you can add your own:
fault disk_full, no_such_user
`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 s64 type bitset b64 type buffer [256]u8 type lineview []u8
function prototypes go up here too. the bodies live below `implementation`.
fn add s64 (a s64, b s64) fn fill (buffer& []u8, c u8) fn read_line (h file.id, out& string)
everything from `implementation` 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.
implementation
identifiers are a letter followed by letters or digits. underscores count as letters.
there are 28 reserved words:
fn var const type record enum union fault raise raises defer on for if else return break continue inline import implementation as mod asm true false none
the operators:
= == != < <= > >= + - * / += -=
^ & | . , ; : ( ) [ ] { }
a few do double duty:
on values (`p^`).
argument at the call site.
bodies and statements use `:` and `;`. `:` opens a compound (a function body, a `for` body, an `if` branch). every simple statement ends with its own `;`. the compound itself also closes with `;`. so a one-statement body looks like `: stmt; ;` — the last `;` belongs to the body, not the statement. you can run them together: `stmt;;`. nested closes stack the same way: `;;;`.
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 ()
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 then
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 ()
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]
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. read the tag with `u.tag`. read or write a variant's payload with `u.v`. writing to `u.tag` switches the active variant.
type colour enum red | green | blue
type shape union
point
| circle : radius f64;
| rect : width f64, height f64;
fn area f64 (s shape):
if s.tag == shape.circle then
return float.pi * s.circle.radius * s.circle.radius;
else if s.tag == shape.rect then
return s.rect.width * s.rect.height;
return 0.0;;
module-level `var` lives in the module's bss, zero-initialised. function locals appear between the signature and the body's `:` as a plain comma-separated list of `name type` pairs — no `var` (which is reserved for module-level). locals aren't initialised; assign before use.
var instances s64 var palette [3]colour
a function is `fn name signature [locals] body`. the signature is an optional return type, the parameter list in parens, and optional `inline` and `raises` markers. locals come right after, then the body.
fndef = "fn" ident signature [ vardefs ]
( body | asmbody ) .
body = ":" [ stmts ] { handler } ";" .
handler = "on" ident ":" stmts .
stmts = stmt { stmt } .
stmt = return | defer | raise | for | if | break | continue
| ( expr ";" ) .
return = "return" expr ";" .
vardefs = vardef { "," vardef } .
vardef = ident type .
`var` is the giveaway between a function with no locals (`fn foo ():`) and a forward declaration followed by a module-level var (`fn foo () \n var x s64`). after a signature, a leading `var` means "this is a prototype; the var belongs to the surrounding scope."
remember: each simple statement carries its own `;`, and the compound's closing `;` is its own token. a one-statement body glues both: `: stmt;;`. compound-form statements (`for`, `if`) already include their close — they don't need an extra `;` in the enclosing compound.
fn add s64 (a s64, b s64):
a + b;;
fn greet (who string):
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)
i s64:
i = 0; for i != length buffer; i += 1:
buffer[i] = c;;;
fn fill_caller ()
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;;
`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` is only for definitions; you can't have a first-class `fn ... inline` type.
fn min2 s64 (a s64, b s64) inline:
if a < b then 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 later definition with a *different* signature replaces the binding — which is sometimes what you want at the REPL but a footgun elsewhere.
fn read_line (h file.id, out& string)
n s64:
n = length out;
say "read into buffer of length ", n;;
`asm : ... ;` hands the body to the rv64 assembler verbatim. the compiler emits prologue and epilogue around it.
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:
calls and binary operators need parens: `f (a + b)`, `f (g x)`.
fn expr_demo s64 (x s64, y s64)
z s64, ok bool:
z = (x + y) * 2;
z += 1;
ok = and (x > 0), (y > 0);
if not ok then
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 then 1
else if x < 0 then -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 ()
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] then
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)
i s64:
i = n; for i > 0; i -= 1:
say i;;;
fn global_should_stop bool ():
return false;;
fn forever ():
for:
if global_should_stop then
break;;;
`defer expr` schedules `expr` to run when the function returns — whether normally or via a fault. 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
h file.id:
h = file.open path;
defer file.close h;
cat path;;
`raise x` abandons the current function with fault `x`. registered defers run first, then control transfers to a handler — if there is one — or propagates to the caller.
`raises` is part of the function's type. you have to mark a function `raises` if (and only if) its body either raises or calls another raising function. the compiler will tell you if you forget or if you put one where it isn't needed.
handlers live at the end of a function body, before the body's closing `;`. each one is `on fault: stmts` — no own close, the body's `;` closes both the last handler's stmts and the body. completing a handler converts the fault into a normal return.
fn parse_or_zero u8 (s string) raises
v u8:
v = byte.parse s;
return v;
on invalid_format:
return 0;;
fn save_user (name string) raises:
if length name == 0 then
raise invalid_argument;
if instances >= max_items then
raise disk_full;
instances += 1;;
these are always in scope:
new n allocate n bytes, return the address
dispose p& free a block from new
length x element count of an array or slice
and / or / not short-circuit logical ops
say / sayin format and print, with or without a newline
ask prompt, buf&, a0&, ... prompt, read a line, parse
cd, ls, mkdir, rmdir, rm, cat, cp shell-style file ops; raise
operation_failed on failure
fn predef_demo () raises
buffer address, age s64, reply [64]u8:
buffer = new 1024;
defer dispose buffer&;
say "hello,", (length "hello,"), " bytes long";
ask "how old? ", reply&, age&;;
import these by name and you get their members. the report has the full signatures; here's the elevator pitch for each:
byte single-byte memory and conversion
bytes operations on byte slices — clear, copy, move,
equal, compare, parse, format. copy is a forward
memcpy; move is safe for overlapping ranges.
bit b64 shifts and range include/exclude
signed s64 arithmetic, conversion, limits
float f64 arithmetic, trig/log/exp, constants
boolean parse and format for bool
random splitmix64 prng
clock monotonic timing
time wall clock and calendar decomposition
file open, read, write, seek, close, etc.
directory make, remove, query
marshal variadic textual serialisation
screen alternate-screen tty, keyboard input
sound audio playback
read a whole file into a buffer:
fn read_all (path string, out& string) raises
h file.id, size s64:
size = file.size path;
if size > length out then
raise buffer_overflow;
h = file.open path;
defer file.close h;
file.read h, out&;;
time a block:
fn time_loop s64 ()
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:
fn run () raises
name [64]u8:
for:
ask "name? ", name&;
if length name == 0 then
break;
say "welcome, ", name;;;
that's the language. when you want chapter and verse, the formal grammar is in => almac_report.moff almac language report.