Sitelet https://aldan4.github.io/docs/learn_almac.html

learn almac

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.

1. modules

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

2. public declarations

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 b8_size   = b8           -- 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 each common fault in the module whose operations raise it, declared as one failure named `fault` — `file.fault`, `heap.fault`, `bytes.fault`, `socket.fault`, and `boolean.fault` for contract violations (see appendix b.2 of the report) — so you import the owner and write `file.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 file.fault` makes `io` a second name for `file.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 file.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 id         from s64
type buffer     [256]b8
type lineview   []b8

an alias to another *named* type takes `as` — `type byte_count as s64` makes `byte_count` a second name for `s64`, fully interchangeable with it. a *distinct* type takes `from` — `type id from s64` shares s64's representation but is a type of its own: an `id` won't mix with a plain `s64` (or with another distinct type over s64), so a file handle can't be added to a byte count by accident. it's the tool for opaque handles. crossing between an `id` and its base is an explicit `as` cast (`h as s64`, `n as id`); an integer literal still assigns straight in (`x = 0`). a structural right-hand side (an array, slice, record, enum, union, pointer, or `fn` type) takes neither marker, as above; a bare `type id s64` with no marker is rejected. the marker 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 (var buffer []b8, c b8)
fn read_line (h file.id, var line 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 file.fault as file.open

3. body

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:

...

3.1 setup and teardown

two names in a module mean something to the system. if you define `setup`, it runs once your module has finished loading. if you define `teardown`, it runs before your module is thrown away — at `reset`, or when a line that imported you fails to compile.

fn setup () {
  machine.patch_slot 54, fn tick;
}

fn teardown () {
  machine.restore_slot 54;
}

they're not keywords and nothing declares them; they're ordinary functions the loader looks up by name. each one takes nothing and returns nothing. teardown can't raise. setup can: if a fault leaves setup, the import fails just as if your module didn't compile, and your teardown isn't run. write them below the `...` unless you want them callable from outside.

setup is also where a module variable holding a slice, a pointer or an address gets its first value. zero isn't a valid one — a zeroed slice views address zero — so such a variable can't be used until setup has assigned it, and every function that uses it goes below setup:

var names []b8

fn setup () {
  names = "";
}

otherwise you probably don't need either. your module's variables are wiped for you when the image is. reach for them when your module puts something *outside* the image — a firmware slot, a running service, a claimed port — that would otherwise still be pointing at your code after it's gone. setup puts it there, teardown takes it back.

your imports are loaded and set up before your setup runs, so it can call them. teardowns go in the opposite order — you're torn down before anything you import, so yours can still call them too.

4. vocabulary

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:

the keywords `var` and `into` lead a parameter in a signature and the matching argument at the call site — `var` for a parameter the callee reads and writes, `into` for one the callee constructs (see 10.1).

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:`).

5. scalar types

bool       1 byte
s64        signed 64-bit integer
b8         8-bit field of bits -- unsigned; also the byte
b64        64-bit field of bits -- unsigned
f64        ieee-754 double
address    untyped 8-byte machine address
string     alias for []b8
const []t  a view: a slice you may read but not write through
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 b8,
    ratio f64, bits b64 {
    flag  = true;
    count = -123;
    byte  = 0xff;
    ratio = 6.28;
    bits  = 0xdeadbeef;
}

6. records, pointers, self-reference

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
}

pointers are affine: single-owner. assigning a pointer you own (a local, or a from/into parameter) moves it — the source goes dead, so you can't end up with two owners of one node and free it twice. an optional field you take out (`rest = head^.next`) is taken with null: the field is read and left `none`.

the other half is the alias rule: a pointer is either an owner (a local, a from/into parameter, or something fresh from `claim`) or a borrow (a bare/`var` parameter, or a `with`/`for with` alias). an owner can be stored, returned, moved, and freed; a borrow can only be read, walked, or re-aliased — it may not be stored into a variable or field, returned, or taken. so a local always owns, which is why returning a local is always fine. to walk a borrowed list, use `for with` (it reborrows, never moves); to keep a pointer, receive it `from`. (`address` is exempt — it's a plain value at the trusted boundary.)

and since a borrow never outlives its call or block, the compiler can also promise one mutable path per place without a borrow checker. a call may not get the same place twice when it can write one of them (`swap var a[i], var a[j]` is refused — `i` might equal `j`; `swap var a[0], var a[1]` is fine), and while a `with` or `for with` alias is live, its place is reached only through the alias: after `with p = a[0];`, touching `a[0]` — or `a`, or `a[i]` — is an error until the block closes.

type node record { next ^node|none, value s64 }

fn insert ^node|none (from head ^node|none, v s64)
    raises heap.fault var n ^node {
    n = heap.claim node;
    n^.value = v;
    n^.next = head;              -- move: `head` is consumed into the list
    return n;
}

fn find bool (head ^node|none, v s64) {
    for with n = head; n.next {  -- borrow-walk: `head` stays intact
        if n.value == v { return true; }
    }
    return false;
}

fn pop ^node|none (from head ^node|none) var rest ^node|none {
    guard head | return none;
    rest = head^.next;           -- take: head^.next left none
    heap.release from head;      -- free the old head node (consumes it)
    return rest;                 -- the new head (ownership handed back)
}

`insert` and `pop` take `from head`: the caller hands the list over and its binding goes dead, so it cannot keep using a list it no longer owns (6, 10.1). `heap.release` itself takes `from` for the same reason — freeing a pointer consumes it, so a use-after-free or a double release is a compile error. a disposer that owns a resource just forwards it on: `fn dispose (from p ^node) { heap.release from p; }`.

7. arrays and slices

`[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 bit type — `b8` or `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);
}

8. enums and tagged unions

an enum is a list of named tags. it's `b8`-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 (var 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;
}

9. variables

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 value you compute once and never change is a `final`. declare it exactly like a `var` — at module scope, or among a function's locals, mixed with `var` clauses as you like — and give it its value with an ordinary assignment. that first assignment is the only one: before it the name can't be read, after it the name can't be written.

fn border s64 (w s64, h s64) final inner s64, var edge s64 {
  inner = (w - 2) * (h - 2);   -- the one write
  edge = w * h - inner;
  return edge;
}

a module `final` gets its value in `setup`. a record or array has no value to assign, so it gets its value from a call that builds it, `make into p;`. a `final` slice stays a slice: you can't point it somewhere else, but you can still write through it. if you want the bytes read-only too, say so in the type: `final name const string`.

10. functions

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;
}

10.1 variable and out parameters

a parameter has three kinds. with no marker it's read-only: the callee reads it and nothing more. lead it with `var` and the callee reads *and writes* the caller's storage; the call site marks the matching argument `var` too. lead it with `into` and the callee *constructs* the parameter — it starts unborn, and the callee must fill it before returning; the call site marks that argument `into`.

scalars pass by value, aggregates always pass by reference — the marker is about *writability*, not how things get there.

fn fill (var buffer []b8, c b8)
    var i s64 {
    i = 0; for i != length buffer; i += 1 {
        buffer[i] = c;
    }
}

fn fill_caller ()
    var page [256]b8 {
    fill var page, 0x20;      -- `var` at the call site too
}

a slice local, like a pointer, has to be assigned before you use it: a zeroed slice views address zero, so the compiler refuses to let you read it, index it or pass it `var` until something has been stored. an empty one is just `""`. a callee that only fills a slice takes it `into`, which counts as the assignment:

fn first_word (s string, into w const string)
    var n s64 {
    n = 0; for and (n < (length s)), (s[n] != ' '); n += 1 { }
    w = s[0:n];
}

fn caller ()
    var w const string {
    first_word "hello world", into w;   -- w is born here
    tty.say w;
}

`w` is a `const string` because what it holds is part of `s`, and a plain parameter like `s` is a view: the function may read it but not write through it, keep it writable, or hand it back writable. a string literal is a view too, and so is every constant, which is why `"hello world"` can be passed here at all. a slice you mean to write through — a buffer you fill — is passed `var`, as `fill` takes its page above.

10.2 returning aggregates

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 parameter (marked `into`) and build it in place — the caller supplies a fresh variable, and the callee fills it.

type point record { x f64, y f64 }

fn make_point (into p point, x f64, y f64) {
    p.x = x;
    p.y = y;
}

10.3 inline functions

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; }
}

10.4 forward declarations

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

10.5 assembly bodies

`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 }

11. expressions

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;
}

11.1 if as an expression

`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; };
}

11.2 casts

`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
distinct <-> its base     (type x from y, 6 -- and between two such)

widening from `s64` sign-extends; widening from `b8` zero-extends. narrowing to `b8` keeps the low 8 bits.

fn cast_demo ()
    var c b8, 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
}

11.3 bit fields

the two bit types, `b8` and `b64`, support two postfix forms — `b[i]` for a single bit (yields `bool`) and `b[i:j]` for a range of bits (yields the operand's own type). both are places, so either can be assigned to, and the store leaves the surrounding bits alone. bitwise and and or are the integer forms of `and` and `or`; the `bit` module has the rest: `shil`, `shir`, `xor`, `clz`, `ctz`, `ones`.

fn bitfield_demo b64 (b b64) {
  if b[7] { return b[0:8]; }   -- low byte
  return 0;
}

fn nibbles (v b8) {
  tty.say "low: ", (v[0:4]), " high: ", (v[4:8]);
}

-- pack two fields into one word, then replace just one of them
fn repack b64 (size s64, shift s64) var x b64 {
  x[0:40] = size as b64;
  x[40:45] = shift as b64;
  x[0:40] = 0 as b64;          -- shift survives
  return x;
}

12. statements

12.1 for

`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; }
    }
}

`for with n = seed; advance { ... }` is the walking-alias form: it follows a chain of pointers, binding `n` to each pointee, and stops at `none`. `seed` and `advance` are pointer expressions; the loop reborrows each step non-destructively, so the list you walk is left exactly as you found it (see 6). `n` is a mutable alias of the current node — read or write `n.value`, `n.next` — and is gone once the body ends.

type node record { next ^node|none, value s64 }

fn sum s64 (head ^node|none) var total s64 {
    total = 0;
    for with n = head; n.next {
        total += n.value;
    }
    return total;
}

12.2 defer

`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. like any bare function name, a deferred `defer note` calls note with no args.

fn open_and_print (path string) raises file.fault
    var h file.id {
    h = file.open path;
    defer file.close h;
    cat path;
}

12.3 raise and handlers

`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. several failures are separated by `|` (`raises f

g`), not by a comma, so the list reads unambiguously even when the

function type sits inside a comma-separated parameter or field list. 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), and one handler may name several targets separated by `|` (`on f

g: stmts`) — 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 b8 (s string)
    var v b8 {
    v = byte.parse s;
    return v;
on bytes.fault:
    return 0;
}

fn save_user (name string) raises boolean.fault | account {
    if length name == 0 { raise boolean.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 `boolean.fault` (from `core/boolean`) and your own `account`. a failure name in `raise`/`raises`/`on` may carry a module qualifier (`boolean.fault`) or be bare when it is your own module's failure, exactly like a type name.

13. things you don't have to import

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      logical over bool, bitwise over integers

`and` and `or` short-circuit over `bool` and don't over integers (there is nothing to skip — every argument is needed for the result). `not` is logical negation over `bool` and the bitwise complement over an integer, within that integer's own width.

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 from p` frees it and consumes `p`). 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 heap.fault
    var buffer address, reply [64]b8 {
    buffer = heap.claim 1024;
    defer heap.release from buffer;
    tty.say "hello,", (length "hello,"), " bytes long";
    tty.sayin (tty.ask "your name? ", var reply);
}

14. standard modules

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; boolean.fault
heap       claim / release memory; query bytes available; heap.fault
random     splitmix64 prng
clock      monotonic timing
time       wall clock and calendar decomposition
file       open, read, write, seek, close, etc.; file.fault
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
bytes      also owns bytes.fault (parse / format errors)

read a whole file into a buffer:

fn read_all (path string, var dst string) raises file.fault | bytes.fault
    var h file.id, size s64 {
    size = file.size path;
    if size > length dst { raise bytes.fault.buffer_overflow; }
    h = file.open path;
    defer file.close h;
    file.read h, var dst;
}

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;
}

15. putting it together

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]b8, line string {
    for {
        line = tty.ask "name? ", var 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.