ortfero
almac (algorithmic language for machine computation) is a small imperative programming language for the aldan system. it emphasises a small grammar, explicit memory layout, value semantics for aggregates, and explicit propagation of failures.
the compilation unit is the module: a sequence of declarations followed by an optional body section (see 11). every module begins with a small fixed vocabulary built into the compiler: the primitive types and four predefined forms (length, and, or, not), listed in 10.5. everything else — the failures and the standard library — is itself written in almac, supplied as ordinary modules under core/ in the source tree and gained by importing them (11.2). the predefined forms appear in 10.5; the built-in types and standard modules (the failures among them) in appendix b.
this report is a concise reference for programmers and implementors. what is left unsaid is either derivable from the stated rules or deliberately left to the implementation.
the syntax is given in an extended backus-naur form. a production has the form
production = expression .
the meta-symbols are
lower-case identifiers denote non-terminals; terminals are quoted literals or the token names of section 3. the complete grammar is collected in appendix a.
a token is a keyword, an identifier, a number, a character, a string, an operator, or a delimiter. tokens are separated by blanks, line breaks, and comments. a comment begins with the digraph — and extends to the end of the line.
an identifier is a letter followed by letters and digits. letters are the lower-case latin letters; the underscore counts as a letter.
ident = letter { letter | digit } .
the following 30 words are reserved and may not be used as identifiers:
as asm break const continue defer else enum failure false fn for from guard if import inline into mod none on raise raises record return true type union var when with
a number is an integer, a hexadecimal, or a floating-point literal. in the optional exponent of a floating-point literal the letter e is read "times ten to the power of".
integer = digit { digit } .
hex = "0" "x" hexdigit { hexdigit } .
fp = digit { digit } "." digit { digit }
[ "e" [ "+" | "-" ] digit { digit } ] .
a character literal is a single byte in apostrophes and has type u8. a string literal is a byte sequence in quotation marks and has type string; it cannot contain the delimiting quotation mark. a short-string literal abbreviates a string without whitespace: a leading apostrophe followed by the content, with no closing apostrophe. the content ends at the first whitespace character or at one of the closing tokens , ) ] } . the characters ' " ` ( [ { inside a short-string are errors; the verbose "..." form must be used instead. examples:
'hello 'out/build.alm 'http://x.io:8080/q
character and short-string literals are distinguished by the byte after the content: 'x' is the character x; 'xy is the short-string "xy".
the boolean literals are true and false. the literal none denotes the absent value of an optional pointer; it converts implicitly to optional pointer types only (see 6.4, 8.2.5), and serves as the return value of no-result functions (see 9.7).
the operators and delimiters are
= == != < <= > >= + - * / += -=
^ | . ... , ; : ( ) [ ] { }
the roles of ^ (pointer / dereference), | (variant separator, case prefix), and ... (variadic parameter marker inside a parameter list, deferred-definition marker after a signature, and the module interface/body separator) are defined with the constructs that use them (6.4, 6.5, 9.5, 9.10, 10.1, 10.3, 11.1). the parameter-kind markers var and into are reserved words, not operators, defined in 10.1. the three-dot ... is read greedily, distinct from the field selector . (6, 8.1). statement punctuation is defined in 9.
a minus sign immediately followed by a digit begins a negative number literal; followed by another minus sign it begins a comment; otherwise it denotes subtraction or negation. the expression x-5 must therefore be written with a blank: x - 5.
every identifier must be declared before use. top-level declarations are visible throughout the module; those preceding the ... separator are also exported (see 11.1). the body of a function opens a scope; a nested scope may redefine an outer name for the remainder of the body.
declaration = functionproto | typedecl | vardecl | constdecl
| failuredecl .
a function prototype is a name and a signature without a body; its relation to a later definition is described in 10.3. a signature may instead carry an as suffix naming an existing function — a function alias (10.3), which in the interface section re-exports an imported function under this module's name (11.1).
a constant declaration binds a name to a compile-time value.
constdecl = "const" ident "=" constexpr .
constexpr = "true" | "false" | "none"
| integer | hex | fp | character | string
| qualident .
if the right-hand side is a type identifier, the constant is the size of that type in bytes, of type s64. if it is another constant, the new constant receives its value and type.
const limit = 100 const origin = none const node_size = node -- size of type node in bytes
a type declaration binds a name to a type. a named type may refer to itself only through a pointer.
typedecl = "type" ident ( "as" typename | "from" typename | structtype ) .
type = typename | structtype .
structtype = "^" type [ "|" "none" ]
| "[" [ constexpr ] "]" type
| recordtype
| enumtype
| uniontype
| functiontype .
typename = qualident [ "." ident ] .
a type declaration names another type through as or from. type byte_count as s64 is a transparent alias: it introduces no type of its own, binding the name to the target instead, so byte_count and s64 are one type rather than two that agree (and where the target is itself a distinct type, the alias names that same distinct type). type id from s64 is a distinct nominal type: it shares s64's representation but is a type of its own, compatible only with itself — an id neither combines with an s64 nor with another distinct type over s64, and crossing to or from the base takes an explicit as cast (8.2.5). the distinctness is nominal, like an enumeration's (6.5): two separate from declarations over the same base are unrelated types. an integer literal still coerces to a distinct type over an integer base (var x id; x = 0), and an operator on two values of one distinct type yields that same type. a bare typename with neither marker (type id s64) is rejected as a typo guard. a structural right-hand side — a pointer, array, slice, record, enum, union, or function type — carries no marker. from targeting a union-variant subtype is rejected (use as). the marker is confined to the declaration: in every other type position (a variable, parameter, field, array element, or pointer base) a named type is still written bare, and as there is the cast operator (8.2.5).
because an alias binds through, its interchangeability is total rather than a matter of what a comparison of types will accept: an alias reaches everywhere its target does, including the positions that read a type's structure rather than its identity. type name as string is a string wherever one is written — indexed, sliced, passed and laid out as one — and an alias of a named record or array is likewise that record or array. two aliases of one target are the same type. from is the opposite in each of these: it does introduce a type, which is what it is for.
a typename is a qualified name with an optional trailing variant selector: a bare ident names a type in scope, m.t names a type exported by module m, and the trailing ident in u.v or m.u.v names a variant subtype of the union u (see 6.5).
bool and u8 occupy 1 byte; s64, f64, address, and b64 occupy 8 bytes and are aligned to 8.
an array is a fixed-length sequence of elements of one type; a slice is a fat pointer — a data address and an element count — referring to such a sequence.
arraytype = "[" constexpr "]" type . slicetype = "[" "]" type .
the array length is a positive s64 constant. elements are contiguous; the array's alignment is that of its element type. a slice occupies 16 bytes, aligned to 8. an array converts implicitly to a slice of the same element type wherever a slice is required — in assignment, in an argument position, or under an explicit as.
an element type may never be non-null — a non-null pointer or an address — since per-element initialisation is unverifiable. arrays of pointers are declared optional (^t|none) and narrowed at use (see 6.4, 9.5); tables of raw addresses are kept as s64 and forged at use (9.3).
a record is a fixed sequence of named fields. the field list, a recdef, is a possibly empty comma-separated list of name-and-type pairs in braces.
recordtype = "record" recdef .
recdef = "{" [ vardefs ] "}" .
vardefs = vardef { "," vardef } .
vardef = ident type .
fields are laid out in order with natural alignment; the record is aligned to the strictest alignment of its fields. a record may contain pointers to itself.
a field may be non-null — a non-null pointer or an address. such a record is legal and instantiable: its non-null fields are deferred obligations, born one by one by assignment before the record is used as a whole (the field-level birth rule, 7). the exception is an untrackable member — a non-null under an array element or a union payload, where per-member birth cannot be verified; a record whose members are all trackable may itself be a local (7).
type point record { x s64, y s64 }
type cell record { p ^s64, v s64 } -- p is a deferred obligation (7)
a pointer is non-null by default: ^t holds the address of a value of type t. the optional pointer ^t|none adds the literal none; it admits only assignment, == and != (against none or a pointer of the same base), and narrowing (9.5). both occupy 8 bytes; none is address zero, so the option costs no storage.
pointertype = "^" type [ "|" "none" ] .
dereferencing, written p^, yields the pointed-to value as an lvalue. it applies to ^t only; an optional pointer is first narrowed to ^t by a guard (9.5). the lvalue is writable even where the pointer itself is not — an immutable parameter, or a pointer field of an immutable aggregate — since ^t addresses independent, mutable storage; immutability of the pointer bars only rebinding it (p = ...), not writing through it (10.1).
the three non-null / optional pointer forms — ^t, ^t|none, and address — are affine owning types: single-owner, with no reference type and no lifetimes. what this governs is assignment of a pointer this function owns. when the right-hand side is a whole binding the function owns — a local, or a from/into parameter — the assignment moves it: the value is copied and the source dies (it becomes unusable until reassigned, exactly as a from argument dies, 10.1). when the right-hand side is a mutable optional lvalue the function does not own outright — an optional heap or record field (q = p^.next), or an optional var/borrow — the assignment takes it with null: the link is read and the source slot is left none at run time (the runtime none is the death). extracting a non-null pointer out of an aggregate slot (q = p^.link, a ^t field) is rejected — there is no none to leave behind, so it would silently alias; borrow it instead (with, 9.10, or for with, 9.6). a move prevents two owners of one resource; a plain read for comparison, for passing as a bare or var borrow, or for a with / for with alias is non-consuming and leaves the owner usable. dropping a live owner is permitted (a leak, not an error); double-ownership and use-after-move are errors.
the second half of single-ownership is the alias rule, which keeps a borrow from escaping. a typed pointer (^t/^t|none) is an owner when it comes from a local, a from/into parameter, or an rvalue (claim, a returning call, a move); it is a borrow (an alias) when it comes from a bare or var parameter, or a with / for with binding, and the taint follows every selector — what you reach through a borrow is itself a borrow. an owner has full rights: assign (move), return, store into a field, hand over from/into, free. a borrow may only be dereferenced, compared, passed as a bare/var argument (re-borrow), or re-aliased with a with / for with; it may not be assigned into a binding or field, returned, taken out of an aliased structure, or consumed. so a local always owns (an alias can never be assigned into it), which is exactly why returning a local, or a local record with pointer fields, is always sound. move ownership into a structure with an owner (n^.next = head over a from head); alias a place to read it with with / for with; never store a borrow. address is exempt — value-like at the trusted boundary (9.3), it copies freely — so an address carries no alias taint.
an enumeration lists a fixed set of named variants separated by the bar operator; at least one variant is required. its representation is u8, distinct from any other enumeration — the same nominal distinctness a from declaration (6) confers on a type over any base.
enumtype = "enum" ident { "|" ident } .
a tagged union is a discriminated variant type; each variant carries an optional record of payload fields, written as a recdef (see 6.3). the tag is stored at offset zero; the payload area follows, aligned and sized to the strictest variant.
uniontype = "union" unionvariant { "|" unionvariant } .
unionvariant = ident [ recdef ] .
type color enum red | green | blue
type shape union circle { r s64 } | rect { w s64, h s64 }
each variant v of a union u names a subtype u.v. a subtype has the size and alignment of u but is fixed to one variant: a variable of type u.v has its tag initialised to v, and only v's payload fields are reachable through it. a variable of the union type u itself exposes no fields; it must first be narrowed to a subtype by a guard statement (9.5) or by a when statement over the union (9.4).
a subtype converts to its parent union in one direction only: a subtype value may be supplied where the union type is expected — in particular as an argument, where it is passed by reference without copying (see 10.1). the reverse conversion, assignment between subtypes, and whole-union assignment are all rejected; a union value is built through a subtype variable and shared by passing it.
the compiler infers, from a union's shape alone, whether it is liftable — whether a plain value may be lifted into it in an argument position (see 8.2.5). a union is liftable if and only if every variant carries exactly one payload field and the variant field types are pairwise distinct. there is no source syntax and no marker token: the property is read off the declaration. a union that does not meet the shape — one with a multi-field variant, a payload-less variant, or two variants of the same field type — is simply non-liftable. this is never an error: such unions are perfectly legal and behave exactly as before; they just cannot be a lift target. shape above (variants of differing arity) and a union celsius { v f64 } | fahrenheit { v f64 } (two variants of one field type) are both valid and non-liftable. because liftability is inferred, distinctness is never diagnosed at the declaration; the only diagnostic appears at a call site (8.2.5).
a function type describes a call signature.
functiontype = "fn" signature .
signature = [ type ] "(" [ param { "," param } ] ")" [ raises ] .
raises = "raises" qualident { "|" qualident } .
param = ( "into" | "var" | "from" ) ident type | ident [ "..." ] type .
the leading type is the return type; it is omitted when the function returns nothing. the raises clause is part of the type: it names the exact set of failures — never kinds (see 9.9) — the function may propagate. the failures are separated by `|`, not by a comma: the list must stay closed under the comma that separates the enclosing parameter, record-field, or variable list, so that an inline function type carrying a raises clause can stand in any of those positions. a value of raising type may be called only where those failures are declared or handled. the inline marker is not part of the signature: it leads a declaration (`inline fn`, see 10.2), so a first-class function type — introduced by a bare `fn` — can never carry it. the ... marker (10.1) is likewise a property of a declaration, not of the type: it may not appear in a first-class function type, and the parameter it marks enters the type as an ordinary slice.
a variable declaration introduces one or more named locations, each of a given type. a single var heads a comma-separated list of name-and-type pairs; a trailing comma is permitted. this is exactly the form function locals take (see 10), so the two scopes read alike.
vardecl = "var" vardefs .
module-scope variables reside in the module's bss segment and are zero-initialised. a zeroed non-null value — a non-null pointer or an address — is unsound, so such a variable's type may not contain one — not directly, not as a record field, not as an array element; use an optional pointer, or s64 for raw address bits. function locals (see 10) reside on the activation frame and are likewise zero-initialised.
var count s64 var origin point var names [16]string var first s64, last s64
every variable, module-scope and local, scalar and aggregate, is zero-initialised, and zero is a valid value everywhere — with one exception: a local of non-null type — a pointer ^t or an address — which is born only by assignment or by being passed as the argument to an out parameter (10.1), and by no other means. it may be used — read, dereferenced, or passed with or without a var/into marker — only where such a birth stands earlier in the same or an enclosing statement sequence. this is sound without flow analysis: no construct jumps into the middle of a statement sequence, and after birth the type system preserves validity, since only a non-null value assigns to such a variable and an out parameter is verified to build one. its conditional birth is written with the if expression, not with assignments inside branches:
p = heap.claim node; -- born here; ^t for the rest of the block
p = if first { a; } else { b; };
make into p; -- also a birth: `make` constructs p (10.1)
the birth rule has a mirror, the death: an owned pointer whose value has been moved out (assigned or passed to a from parameter, 6.4, 10.1) is dead — unusable until reassigned — for the rest of the sequence and all following paths (a death, unlike a birth, is not undone at block close). an optional pointer ^t|none is affine too, but carries no birth rule: a fresh one is validly none, so it is usable at once (compared, guarded, borrowed); only a moved-dead optional is blocked, and reassigning it revives it. guarding a moved-dead optional is thus a use-after-move (9.5).
this extends field by field to an aggregate. a local record with non-null members, and a ^rec local assigned an address (the trusted boundary, 9.3, hands back storage whose non-null fields are not yet valid — as heap.claim does), carry one obligation per non-null leaf: a non-null member reached without crossing another pointer (a pointer terminates the recursion — a ^inner field is one leaf, its pointee proven born wherever it was made). each leaf is born by a direct one-path assignment to it — c.p = ..., q^.p = ..., o.in.p = ... — and the aggregate is born as a whole once every leaf is. while any leaf is pending, no whole-use of the aggregate or a sub-object is allowed (read, copy, return, compare, pass by value, pass with var/out, or with-alias) and an unborn leaf may not be read; a plain scalar field stays readable, and writing a leaf is the birth. this is the same soundness argument: a partially-born aggregate cannot escape, so no alias to it exists, which is why assigning an already-existing ^rec value (a pointer, not an address) carries no obligation — it was proven born when it was made. a compound assignment (+=) is a read, so it is barred on a pending leaf.
a pending aggregate may also be born by passing it to an out parameter (an out argument, 10.1): the callee is verified to initialise it — an out parameter starts pending inside the callee and must birth every non-null leaf before any normal return — so the argument is born for the rest of the caller's statement sequence once the call returns (block-scoped, like any birth). this is what lets a constructor, written create into self, n; over an into self parameter, build a record with non-null fields and hand it back valid. a variable parameter (var) is by contrast inout: its fields are taken as born on entry, so a mutator (set self, i; over the same var self) reads them freely — and the caller must pass a fully born aggregate to it. the marker chosen at the call site (into vs var) therefore states, and the compiler checks, whether a call constructs or mutates.
inside an on handler body (9.9) such a local may not be used, since a failure may fire before any birth; all other locals and the parameters remain accessible there. because zero is observable everywhere else, no other location may be born: a module-scope variable may not be non-null (a ^t or an address) or contain one, and an array or slice element may never be non-null nor contain a non-null member (6.2, 6.3). an untrackable aggregate — one with a non-null under an array element or a union payload, where per-member birth cannot be verified — may not be a local at all, nor the target of the address-to-pointer conversion (9.3). all other scalars keep plain zeroing with no birth obligation, so variable-parameter idioms like float.decompose x, var m, var e; need no prior assignment.
an expression denotes the computation of a value. an expression is an lvalue when its principal operand is.
primary = atom | ifexpr .
atom = "none" | "true" | "false"
| integer | hex | fp | character | string
| qualident
| "(" expr ")" .
ifexpr = "if" expr compound [ "else" ( ifexpr | compound ) ] .
qualident = ident [ "." ident ] .
a qualified identifier selects a name from an imported module, a record field, a union variant, or an enumeration variant. the if expression is described with the if statement (9.3).
index, slice, field selection, and dereference are postfix operators on any primary and combine freely. a trailing argument list makes the postfix expression a function call; its form is defined in 9.2.
postfix = primary { index | field | deref } [ args ] .
index = "[" expr [ ":" expr ] "]" .
field = "." ident .
deref = "^" .
a[lo:hi] on an array or slice yields a slice borrowing the same storage, from lo up to but excluding hi. the same form on a b64 extracts a bit-field (see 8.2.3).
the precedence classes, from highest to lowest:
arithmetic and relational operators require both operands to have the same type after implicit coercion (see 8.2.5). a distinct type (type x from y, 6) matches only itself here — not its base y, nor another distinct type over y — though an integer literal still coerces to it; an arithmetic operator on two values of one distinct type yields that same distinct type.
conjunction, disjunction, and negation are written as calls of the predefined names and, or, and not; all arguments and results are bool.
and ( b0, b1, ... ) short-circuit conjunction or ( b0, b1, ... ) short-circuit disjunction not ( b ) negation
and and or are forms, not functions: they evaluate their arguments left to right and stop at the first false or true argument respectively — an exception to the call rule of 9.2. consequently they do not denote function values: they cannot be assigned, passed as arguments, or auto-called. not is an ordinary function. since an argument is an atom (9.2), composite conditions are parenthesised:
and (0 <= i), (i < limit)
the binary +, -, *, / apply to integer and f64 operands; mod applies to integer operands only. unary - applies to s64, integer, and f64.
integer arithmetic wraps around: a result is reduced modulo 2^64 for s64 (two's complement) and modulo 2^8 for u8; overflow never raises a failure.
integer division truncates toward zero; x mod y has the sign of the dividend x, so that x = (x / y) * y + (x mod y) holds. division by zero follows the rv64 processor and raises no failure: x / 0 yields -1, x mod 0 yields x. f64 division follows ieee-754: x / 0.0 yields infinity of the sign of x, and 0.0 / 0.0 yields nan.
for b of type b64 and integer expressions i, j:
further bit operations are exported by the standard module bit (appendix b).
the six relational operators compare two values of the same type and yield bool. f64 comparison follows ieee-754 ordering; u8 is unsigned; all other integer types are signed.
pointers compare with == and != only — no ordering. the operands must share a base type, ^t and ^t|none mutually comparable; a non-null pointer against none is a compile-time error (it can never be none).
an explicit conversion has the form
cast = unary [ "as" type ] .
the permitted conversions are
neither conversion out of bytes may name a target that carries an address anywhere within it — a pointer of either kind, an address, or a record, array or union holding one at any depth. a pointer is born from a claim, from another pointer, or from the trusted forge below, and reading one out of data would be a fourth way in with nothing at the site to mark it. the restriction is on this direction alone: taking the bytes of a value that holds a pointer stays permitted, since that only reveals bits already readable through address and s64, and the bytes so taken are inert — they can come back as integers, and a program that means to make an address of one writes the forge and says so.
a u8 target keeps the low 8 bits; wider integer targets sign-extend from s64 and zero-extend from u8. a cast may not target a union subtype, nor produce a non-null pointer except from address or a non-null pointer of the same base. narrowing — a union to a subtype, an optional pointer to ^t — is performed only by guard (9.5) or when (9.4).
implicit conversions occur in assignment, argument passing, and comparison: an integer literal adopts the type it meets (never address — a literal zero must not become one; see the trusted forge above), an array converts to a slice where one is required, none converts to an optional pointer type, a non-null pointer widens to the matching optional pointer, and address converts to and from any pointer type. the address-pointer conversion loses no nullability — both sides are non-null — and asserts only the typing, exactly as the explicit cast does; it is what lets heap.claim's address result initialise the ^t it meets (appendix b).
a further conversion, value-to-union lifting, occurs only in argument passing. where a parameter type is a liftable union u (see 6.5) and the argument is neither u nor one of its subtypes, the argument is lifted: the compiler builds a temporary union value — a tag and the payload — at the call site and passes it by reference, exactly as any union argument is passed (10.1). the variant is selected from the argument's type: first, the variant whose single field type equals it; otherwise, the variant whose single field type it is implicitly convertible to by the conversions above (integer-literal to integer type, [n]t to []t, so a string or array reaches a slice-typed variant). a bare integer literal first takes its default type s64 and then matches a variant by s64 — a liftable union without an s64 variant does not accept integer literals implicitly, and an explicit conversion is written instead. zero matching variants, or more than one at the same level, is a compile-time error (no liftable variant for the argument type, or an ambiguous lift); an attempt to lift into a non-liftable union is likewise rejected. an argument already of the union type or a subtype is passed directly, with no lift. lifting is confined to argument passing — it is never an assignment, a cast, or a return path.
type item union i { v s64 } | r { v f64 } | s { v string }
fn show (b item) {
when b {
| item.i: tty.say "int ", b.v;
| item.r: tty.say "real ", b.v;
| item.s: tty.say "text ", b.v;
}
}
fn show_all (xs ...item) var i s64 {
i = 0;
for i < length xs; i += 1 {
with e = xs[i];
show e;
}
}
show 42; -- s64 -> item.i
show "hi"; -- [n]u8 string -> item.s
show_all 42, 3.14, "hi"; -- each pack slot lifted by its own type
examples of expressions (refer to 6 and 7):
count + 1 (s64) origin.x - origin.y (s64) names[i] (string) names[2:5] ([]string) flags[3] (bool, flags of type b64) p^.x (s64, p of type ^point) count as f64 (f64)
statements denote actions. a compound statement is a sequence of statements in braces; the braces introduce no new scope, and an empty compound { } is permitted.
compound = "{" [ stmts ] "}" .
stmts = stmt { stmt } .
stmt = return | defer | raise | for | break | continue
| if | guard | when | with | ( expr ";" ) .
each simple statement — expression (including assignment), return, defer, raise, break, continue, guard, with — is terminated by its own semicolon; no separator stands between statements. a compound-form statement (if, when, for) ends at the closing brace of its body and carries no semicolon. braces are never elided: an input such as `for cond stmt;` is rejected rather than absorbed into the next statement. a function body (see 10) is itself brace-delimited, so its closing } is distinct from that of its last compound-form statement.
assignment is an expression; its result is the value of the right-hand side. chained assignment is not permitted.
assign = compare [ ( "=" | "+=" | "-=" ) compare ] .
the left-hand side must be an lvalue: a local or module variable, or a location obtained by indexing, field selection, or dereference. aggregates of array or record type are not assignable as a whole; they are updated field by field. a slice assignment copies the data pointer and length only, not the elements. the compound assignments += and -= apply to scalars only.
a function call is a postfix operator on a function-valued primary. arguments follow the callee directly, with no surrounding parentheses; the list ends at the first token that is neither a comma, a var/out argument marker, nor an atom-starter (literal, qualident, or `(` ).
args = arg { "," arg } .
arg = ( "into" | "var" | "from" ) atom { index | field | deref }
| atom { index | field | deref } .
an argument is an atom optionally followed by non-call postfix operators; binary operators and nested calls must be parenthesised. arguments are evaluated from left to right, before the transfer of control. thus `f a + b` parses as `(f a) + b`; to call f with the sum, write `f (a + b)`. but `f a[i]`, `f a.x.y`, and `f a^.x` are valid and pass the derived lvalue.
a zero-argument call has no syntactic form. when a postfix expression resolves to a function value where a non-function value is expected, the function is auto-called with zero arguments: `last = clock.now` calls clock.now, while `fp = clock.now`, with fp of function type, assigns the function value itself.
a variable parameter must be supplied with a writable lvalue, marked var before the argument at the call site (see 10.1); an out parameter likewise, marked into. the marker must match the parameter kind exactly: a var or into argument to an immutable parameter is a mismatch, and an immutable parameter takes a bare argument. when the callee's last parameter is variadic (10.1), the trailing arguments past the fixed ones form that parameter — either an existing slice passed directly or a pack collected into a caller-built array. a call to a raising function propagates that function's declared failures through the call site; each must appear in the caller's raises clause or be discharged by a handler (see 9.9).
if = "if" expr compound [ "else" ( if | compound ) ] .
the condition is of type bool; every branch is a brace-delimited compound, so there is no dangling-else ambiguity. a multi-way conditional is an else-if chain. the closing } of the last branch terminates the statement; no semicolon follows.
an if construct is also an expression (ifexpr, 8.1). used as an expression it must have an else, and all branches must yield values of one type; a branch yields the value of its last statement. an expression statement built from an ifexpr follows the (expr ";") rule and so carries a terminating semicolon:
x = if y > 0 { 1; } else { 2; };
a when statement selects a statement sequence by comparing a value against constant labels.
when = "when" expr "{" { case } [ elsecase ] "}" .
case = "|" caselabel { "," caselabel } ":" stmts .
caselabel = integer | hex | character | qualident .
elsecase = "|" "else" ":" stmts .
the scrutinee is evaluated once; its type must be s64, u8, b64, an enumeration, or a tagged union. each label is a compile-time value of the scrutinee's type under the rules of 8.2.4: an enumeration admits only its own variants; the other types admit integer and hex literals, integer constants, and — for u8 — character literals in 0 .. 255. all labels must be distinct.
the first case with a matching label runs; control resumes after the closing }. there is no fallthrough; a case body cannot be empty — labels sharing a body are written in one comma-separated case. the else case, last if present, runs when no label matches.
for an enumeration scrutinee the statement must be exhaustive: every variant labelled, or an else present — a non-exhaustive when is rejected, in the same spirit as the verified raises clauses (9.9). for the other scrutinee types an unmatched value without else does nothing.
a when statement may also dispatch on a tagged union. the scrutinee must then be a plain variable — local, parameter, or module variable — since the statement retypes its binding. each case names exactly one variant subtype u.v; within that case the variable is narrowed to the subtype as by guard (9.5), and the narrowing ends with the case. exhaustiveness applies as for enumerations; in an else case the variable keeps its union type.
note that every case carries a leading |, the first included — a prefix, unlike the separator bar of enum and union declarations. like if and for, when ends at its closing } with no semicolon.
fn brightness s64 (c color) {
when c {
| color.red: return 30;
| color.green: return 59;
| color.blue: return 11;
}
return 0;
}
fn area s64 (s shape) {
when s {
| shape.circle: return s.r * s.r;
| shape.rect: return s.w * s.h;
}
return 0;
}
guard = "guard" ident [ typename ] "|" escape ";" .
escape = ( "return" arg ) | ( "raise" qualident "." ident )
| "break" | "continue" .
a guard statement narrows a variable for the rest of the enclosing block. the discriminant is tested at run time; on success the binding is retyped (storage unchanged, only the compile-time type), on failure the escape is taken: return arg returns from the function; raise failure.kind raises that kind, whose failure must appear in the raises clause; break and continue act on the enclosing loop, and are valid in a loop only.
with a typename, ident names a tagged-union variable (or subtype) and typename a subtype u.v of it (see 6.5); a matching tag retypes the binding to u.v, exposing the payload as ident.field.
fn area s64 (s shape) {
guard s shape.circle | return 0;
return s.r;
}
without a typename, ident names an optional pointer ^t|none, tested against none; the non-null path retypes it to ^t for dereference. the optional must be live: guarding a moved-dead optional (6.4, 7) is a use-after-move and is rejected.
the narrowing ends with its block, or earlier when the variable is assigned a value of its declared (optional) type — which may make it none again (so `p = p^.next` on a guard-narrowed owning `p` ends the narrowing). to walk a borrowed list, use the for with borrow-loop (9.6): a bare list parameter is a borrow, so its head cannot be copied into a manual cursor (the alias rule, 6.4) — for with reborrows each step instead, and never disturbs the list.
type node record { next ^node|none, value s64 }
fn sum s64 (head ^node|none) var total s64 {
total = 0;
for with p = head; p.next {
total += p.value;
}
return total;
}
the for statement is the only looping construct; condition and step are both optional.
forstmt = "for" [ expr [ ";" expr ] ] compound . forwith = "for" "with" ident "=" expr ";" expr compound .
without a condition the loop is infinite. a step, separated from the condition by a semicolon, is evaluated after each iteration of the body and before the next condition check; it is the target of continue. break and continue exit the enclosing loop or proceed to its next iteration, and are not permitted outside a loop.
for i != 10; i += 1 {
tty.say i;
}
a for with is the walking-alias form: it traverses a chain of pointers, binding ident to each pointee in turn. the seed (before the semicolon) and the advance (after it) are both pointer expressions — ^t or ^t|none. the loop stops when the cursor reaches none. inside the body ident is a body-scoped, mutable alias of the current pointee: it reads and writes the node's fields directly (n.value, n.next), and cannot escape the loop (it is not addressable, and its name is gone after the body). the seed is evaluated once; the advance runs after each iteration and is the target of continue. crucially, both the seed and the advance are non-consuming reborrows — they read the pointer value without moving or taking it (6.4) — so the walked list stays singly-owned and structurally intact. walking a moved-dead list is rejected (7).
type node record { next ^node|none, value s64 }
fn find bool (head ^node|none, v s64) {
for with n = head; n.next {
if n.value == v { return true; }
}
return false;
}
because a for with borrows rather than moves, head is still usable after the loop — the walk leaves the list exactly as it found it. this is the preferred way to traverse an owned or borrowed list; assignment-based traversal (p = p^.next) instead takes each link (6.4, 9.5).
return = "return" expr ";" .
the expression must be compatible with the declared return type. all registered defer actions run before the return completes (see 9.8).
a function with no return type still requires an expression after return; the literal none, compatible with any no-result function, serves this purpose:
return none;
defer = "defer" expr ";" .
a defer statement registers an expression to be evaluated at function return or at failure propagation, in last-in first-out order. at most eight defers are permitted per function.
a deferred expression may not raise. it runs on the way out of the function — on a return, or with a failure already being carried — so a second failure raised there would have nowhere to go; a deferred function that can fail must discharge it with a handler of its own. the failure being carried is not visible to the deferred expression: it runs as ordinary code, and it is restored intact once the defers are done, so a handler downstream sees the failure the body raised.
fn open_and_read(path string) raises storage.fault
var f file.id {
f = file.open path;
defer file.close f;
...
}
failuredecl = "failure" ident
( "as" qualident | "{" ident { "," ident } "}" ) .
raise = "raise" qualident "." ident ";" .
handler = "on" failureref { "|" failureref } ":" stmts .
failureref = qualident [ "." ident ] .
a failure declaration takes one of two forms, exactly as a type declaration does (6): the kind enumeration `{ ... }`, or an alias to another failure written with as. failure io as storage.fault binds io as a second name for storage.fault — the same failure under two names, not a copy. the two are fully interchangeable: either name may appear in a raise, in a raises clause, and in an on handler, and a kind raised under one name is discharged by a handler on the other. the alias borrows the target's kinds verbatim (io.not_found is storage.fault.not_found) and takes no kind list of its own. the qualident that names the target is resolved as for any failure reference: a local failure unqualified, an imported one as m.fault. as elsewhere, the as is confined to the declaration.
a failure is a closed enumeration of kinds. signatures name failures — a stable, coarse contract; raise statements name kinds — the precise cause; handlers name either. a signature thus records what can go wrong, not how: changing which kinds a body raises leaves callers untouched while its failure set holds.
a failure declaration defines a failure by enumerating its kinds; the enumeration is mandatory and closed. a failure is module-local, exactly like a type: declared in some module, exported when it precedes the ... separator (11.1), and named from elsewhere as module.failure. within its declaring module it is named unqualified. the qualident that names a failure is resolved as for a type name (6): the first identifier, if it names an imported module, selects that module's failure; otherwise the failure is local. a trailing ident beyond the failure is its kind. the standard library groups its failures in small owner modules (appendix b.2) — core/storage, core/logic, core/bytes, core/resource, core/machine/aldan — each declaring one failure named fault, so a path error is storage.fault and a parse error bytes.fault. there are no predeclared, globally-visible failures; a module that raises or handles one imports its owner.
a raise statement names a concrete kind by qualified name — the failure (optionally module-qualified) and then the kind. a bare failure is a compile-time error. it abandons the current function and marks that failure active; registered defers are replayed before control transfers.
the raises clause names failures only, never kinds. the compiler computes the failure set from the body — a failure appears in raises if and only if at least one of its kinds can escape: each raise contributes its kind's failure, each call to a raising function contributes the callee's declared failures, each on handler removes what it discharges (a handler body may itself contribute new ones). it rejects any discrepancy at failure granularity: a failure propagated but not declared, or declared but never propagated, is an error. a function propagating no failure carries no raises clause; a bare raises is a syntax error.
handlers stand at the end of a function body, after the statements and before the closing }. each handler is on, one or more `|`-separated references — each a whole failure or a concrete kind — a colon, and a statement sequence running to the next on or to the body's }; handlers share the body's closing brace. a kind handler discharges that kind; a failure handler discharges all its kinds. a specific kind handler and a general failure handler for the same failure may coexist: the kind handler is written first and wins for its kind, the failure handler catches the rest. handling every kind of a failure individually discharges the failure. a handler runs in the caller's frame; its completion converts the failure into a regular return. a failure raised in a handler body propagates, uncaught by a sibling handler. unhandled failures propagate to the caller.
a failure value is a 32-bit word: the upper 16 bits hold the failure number, the lower 16 the kind number. the all-zero word means no failure active, so failure numbers start at 1; kind number 0 is reserved and never raised. failures are numbered in first-declaration order across the whole program, not stable across builds — a failure value does not outlive the process, and no failure number is an abi (appendix b.2). so a kind test is a full-word compare, a failure test a 16-bit shift and compare, and the no-failure test after a call a compare with zero.
fn parse_byte u8(s string) {
return byte.parse s;
on bytes.fault:
return 0;
}
here byte.parse may raise bytes.fault.invalid_format; the handler names the whole failure and discharges it, so the function propagates nothing and needs no raises clause. on bytes.fault.invalid_format would discharge only that kind.
a with statement binds a fresh name to the place a designator denotes, evaluating that designator's address exactly once at the statement.
with = "with" ident "=" designator ";" .
designator = atom { index | field | deref } .
it is a flat simple statement, terminated by its own semicolon — not a block form: there is no with-body. like a guard clause, the binding it introduces extends through the rest of the surrounding block. the right-hand side must be a designator (an lvalue): a variable, or a variable followed by index, field-selection, and dereference selectors. a literal, a call result, or an arithmetic or relational expression is rejected.
the binding names the same storage as the designator; it is not a copy. a write through it is visible through the original path and vice versa. its static type is the designator's type, and its address is computed once, at the with: later uses of the name are plain accesses off that cached address, so index arithmetic is not repeated and the name keeps referring to the same element even if an index variable changes afterward.
the name is in scope from the with statement to the end of the enclosing compound — exactly the scope of a local declared at that point. a with inside a nested compound is therefore out of scope after that compound closes; a with in the function body lasts to the end of the body. an inner with may shadow an outer name; two bindings of one name in the same compound are an error.
the binding inherits the mutability of its base: it is an lvalue exactly when the designator is. a base that is a mutable variable, a variable (var) or out parameter, a writable dereference, or a component of one yields a writable name; an immutable parameter or any sub-location of one yields a read-only name, and a write through it is the same compile-time error, with the same message, as a write through the base path directly. there is no read-only modifier — the read-only case arises only from a read-only base.
because the binding is a plain name of the designator's type, guard (9.5) and when (9.4) narrow it as they narrow any binding of union type. this is the way to narrow a union element: when and guard require a name, not a designator, and a union has no whole-value copy, so
with v = vs[i];
when v {
| u.a: ...
| u.b: ...
}
makes the element vs[i] a narrowable name without copying it.
a designator ending in a slice (a[lo:hi]) yields a computed slice value, not a place, and so is not a valid with right-hand side.
a function declaration associates a name with a signature, optional locals, and a body.
fndef = [ "inline" ] "fn" ident signature
( "as" qualident | "..." | { "var" vardefs } ( body | asmbody ) ) .
body = "{" [ stmts ] { handler } "}" .
asmbody = "asm" [ "naked" ] compound .
locals, when present, follow the signature introduced by the keyword var — a comma-separated list of name-and-type pairs. the same var introduces module-level variables (7); here it introduces the activation frame's. the locals may be written as a comma-separated list after one var, as several var clauses, or any mix — the same form either scope accepts. a trailing ... in place of a body marks a deferred definition (10.3), whose body appears later in the same section; ... admits neither locals nor the inline marker. locals are allocated on the activation frame with natural alignment and are zero-initialised by the prologue. a local may be a bare ^t or address, or a record with non-null members, each born by assignment (the field-level birth rule, 7); it may not, however, be an untrackable aggregate — one with a non-null under an array element or a union payload, where per-member birth cannot be verified (as in bss, see 7).
a function may not return an aggregate (array, record, slice, or union) by value; a variable (var) or out parameter is used instead.
a parameter is a binding, written ident type, optionally led by one marker keyword — var ident type or into ident type. scalars — bool, integers, f64, pointers, address — are passed by value; aggregates — arrays, records, slices, unions — by reference, regardless of the marker. a union subtype value may be supplied for a parameter of the parent union type (see 6.5). a parameter has one of three kinds:
the call-site marker must match the parameter kind: var feeds a variable parameter, into constructs an out parameter, from consumes a from parameter, and a bare argument an immutable one. no other pairing is admitted: a var, into, or from argument to a parameter of another kind is a mismatch, and var, into, and from each require a writable lvalue, so a read-only argument (a literal or an immutable binding) under any is rejected. a pointer parameter is the binding, not its pointee: the pointer may not be rebound, but writing through it (p^ = ..., p^.f = ..., or passing var p^ to a variable parameter) reaches the shared mutable storage and is permitted (6.4). to share a writable buffer, then, pass a plain ^t — var is needed only to rebind the caller's pointer itself.
the last parameter may carry a ... marker before its type, written ...t. the marker is a property of the declaration, not of the function type — exactly as inline is (10.2): the parameter's type is the slice []t, and in the body it is an ordinary, immutable []t (it supports length, indexing, and for iteration like any slice). only one parameter may be variadic and it must be last; ... and the var/into/from markers are mutually exclusive; and the element type t must support assignment — scalars (bool, the integers, f64, address, b64, and pointer types) and slice types (including string) are admitted, as is a liftable union (6.5), while record, array, enumeration, and non-liftable union element types are rejected. a slice element is admissible because assigning a slice copies its (address, count) pair, not the viewed elements; a liftable-union element is admissible because each pack slot is filled by lifting its argument (8.2.5) rather than by a whole-union copy.
the marker changes only how a call that names the function is checked. after the fixed parameters are bound positionally, the trailing arguments form the variadic parameter in one of two ways. in the direct form a single trailing argument whose type is the slice []t (or an array of t, which converts to it as usual, 6.2) is passed as the slice itself, with no wrapping; this forwards an existing slice. otherwise the pack form applies: zero or more trailing arguments, each assignment-compatible with t under the usual argument conversions, are evaluated left to right and stored into an array built on the caller's activation frame, and a slice over that array — its address and the element count — is passed. the two forms cannot collide, since an argument would have to be both a t and a []t, which requires t = []t, an unconstructible type. zero trailing arguments are legal and yield the empty pack, a slice of count 0 and data address none. when t is a liftable union, each pack slot is union-sized and is filled by lifting its argument — a tag and the (possibly converted) payload (8.2.5) — so a call may mix argument types freely and the callee sees an ordinary []u to narrow and read; the direct form still forwards an existing []u slice.
the array backing a pack lives in the caller's frame, so recursion and nested calls are safe and a call site reused in a loop reuses the same storage; like any by-reference argument, the callee must not retain the slice past the call. a missing final slice argument to a function without the marker remains an arity error. a call through a function value (a variable or parameter of function type) is unaffected by the marker, which the value's type does not carry, and always takes the slice form.
a function declared with a leading inline — `inline fn name ...` — is expanded at the call site. inline is a property of the declaration, not of the function type (a value cannot be typed inline, and an inline function cannot be aliased or pointed to), so it precedes `fn` rather than sitting in the signature beside raises. an inline function may not declare locals and may not contain return, defer, raise, for, break, or continue, nor an on handler: a handler unwinds through a frame of its own, which the expanded copy does not have. a call to a raising function, however, is permitted, under a raises clause naming the callees' faults exactly as any body's calls oblige (9.9): the fault skips the rest of the copy and carries into the expansion site, where the surrounding function handles or propagates it under the ordinary rules — calling an inline function obliges the caller exactly as if the body's raising calls stood in its place. a thin typed adapter over a raising worker so costs nothing: expanded, it is the worker's call. an inline asm body (10.4) may likewise carry a raises clause, trusted as for any asm body.
a signature without a body binds the name to a function value with no code address. a later definition with an identical signature patches the binding with its address, frame size, and inline flag; a definition with a different signature replaces the binding entirely. such a body-less declaration takes one of two forms, by section. in the interface section (before the ... separator, 11.1) a function declaration is always signature-only — a prototype — and carries no marker: the next declaration delimits it as for any other declaration. in the body section a definition carries a body, so a forward declaration — a deferred definition, its body given later in the section — is written with a trailing ... in place of the body. this lets a group be defined in calling order: declare them with ..., then define them from top to bottom. a body-less signature without ... in the body section is incomplete, not a declaration — the same condition the interactive prompt reads as "continue on the next line".
a signature followed by as target is neither — it is a function alias, the function analogue of a type alias (type x as y, 6) or a failure alias (failure b as a, 9.9). it binds the name to an existing function: a local function or, qualified, an imported one (m.f). no body follows; the target supplies the code. the target must already be defined — a function alias copies the target's function value, so a not-yet-defined prototype, having no code, cannot be aliased. the written signature must equal the target's exactly (return type, parameters, their var and into markers, and the raises set), and the variadic marker must agree. inline and aliasing do not combine in either direction: an inline function cannot be a target (it has no out-of- line entry to forward to), and an alias may not itself be marked inline (the marker has no meaning on a forwarding binding — it is rejected, not ignored). because the alias is a complete binding, no definition for it appears in the body section. an interface-section prototype with as names an imported function and so re-exports it (11.1); a local target, undefined while the interface is read, is aliased in the body section or at the prompt.
a body may be replaced by an asm block, passed verbatim to the rv64 assembler. the assembly syntax is outside the scope of this report.
by default the prologue and epilogue are emitted around the block. when the block needs no frame — it refers to neither fp nor ra, and the function has no locals, no with, and no by-reference parameter copies — they are dropped: the function is a frameless leaf, arguments stay in the argument registers, and a single ret is appended.
the optional naked marker — a contextual word, recognised only directly after asm — forces the frameless form even when the block does use fp or ra. a naked body owns its frame end to end: it saves and restores ra and sp itself and supplies its own ret (or a tail jump to another context), so the compiler emits no prologue or epilogue that would corrupt it. this is the form a context switch needs. a naked body still admits no locals or with.
fn nop () asm { nop }
fn task_switch (var from context, var to context) asm naked { ... }
four names are predefined in every module. b stands for a bool, x for an array or slice. they are the whole of the built-in vocabulary beyond the primitive types — everything once provided as a compiler intrinsic (printing, conversion, allocation, the math and i/o procedures) is now an ordinary standard-library function reached through a module (appendix b), and console output in particular is tty.say / tty.sayin / tty.ask from the tty module rather than a builtin.
name arguments result function length (x) array or slice s64 element count not (b) bool bool negation and (b0, b1, ...) bool bool short-circuit conjunction or (b0, b1, ...) bool bool short-circuit disjunction
these names are forms known to the compiler, not functions: their signatures — variadic, or generic over the argument type — are not expressible as almac function types, and the names do not denote function values. the sole exception is not, an ordinary function. and and or additionally evaluate lazily (see 8.2.1).
a module is the unit of compilation and of namespacing.
module = [ importlist ]
{ declaration }
[ "..." { definition } ] .
the two sections are separated by a single ... at module scope. declarations preceding the ... are public and exported under their names; declarations and definitions after it are private to the module. an interface prototype written fn name signature as m.f re-exports the imported function m.f under this module's name (10.3): the name joins this module's public interface, callable by importers as though defined here, with no forwarding code.
the first top-level ... separates the interface from the body: a bare ... at module scope can be nothing else, since a deferred-definition ... trails a signature (10.3) and a variadic ... stands inside a parameter list (10.1). the interface holds prototypes and other declarations only — a deferred definition is rejected there — so no ... before the separator can belong to a declaration. by convention the separator is written alone on its own line, set off by blank lines, so it reads as a rule dividing the module in two.
an import list names the modules to load before compilation begins.
importlist = "import" string { "," string } .
each string is a path to a source file without the .alm extension; a path beginning with / is absolute, otherwise it is resolved relative to the importing module's work directory. the local name of the module is the basename of the path. an exported member t of module m is accessed as m.t. resolution is uniform: every import, the standard library included, names a source file on the path; there are no host-registered modules bound by basename. recursive imports are rejected.
the standard modules (appendix b) are written in almac and live under core/ in the source tree; a module gains one by importing it — import 'core/file binds the local name file, whose members are file.open, file.read, and so on. they are ordinary modules, distinguished only by shipping with the system: the same import, naming, and qualification rules apply, and nothing in them is privileged over user code. only the primitive types and the four forms of 10.5 are built into the compiler without an import; even the common failures are ordinary module-local declarations in small owner modules (core/storage, core/logic, core/bytes, core/resource, core/machine/aldan; appendix b.2), imported like any other. most of the library is layered on two interface modules — core/machine/aldan, the rom firmware call table (video, keyboard, sound, the byte-copy primitives), and core/platform/aldos, the kernel os-call table (memory, files, directories, tasks) — so the host boundary is two thin modules, not the whole library.
at the interactive prompt only, importing a module also re-imports a sibling module whose name is the subject's basename plus _spec (core/bit gives core/bit_spec) when that file exists, so a module and its checks recompile together. the spec is loaded, not run: it exports an entry point — by convention verify — that you call yourself (bit_spec.verify), and a failed spec.expect raises spec.fault.unexpected, reported at the prompt like any other fault. this applies to repl imports only; a program's import list is unaffected, and a _spec module is not itself given a spec. core/spec (appendix b) holds the expect helper.
a module may define two functions the system calls on its behalf: setup, once the module has been loaded, and teardown, before it is unloaded. neither is declared by the language and neither is a keyword; they are ordinary definitions, recognised by name, and only at module scope. the same names at the prompt, or in the startup script, are ordinary names.
each takes no parameters, returns nothing, and raises nothing; it is neither inline nor an alias. a definition of either name that departs from this is rejected. the restriction is the point: a hook runs where no caller is waiting on it, so it has nowhere to report a failure, and one that could raise would raise into nothing.
setup runs as the last step of loading, after the module's own imports have been loaded and set up, and before the importing module continues. a hook may therefore call anything the module imports, and a module is never used before its setup has run. a module already loaded is not loaded again, so setup runs once.
teardown runs when the image gives a module up — at reset, and when a compile unit that imported it fails and is rolled back. the modules are taken in reverse order of loading, so a module is torn down before anything it imports: an importer's teardown may still use them. it runs while the module's code is still in place, which is what makes it the place to give back anything held outside the image.
the pair is for exactly that: state the module installs somewhere the image cannot see, and that would outlive it. aldos/taskmgr is the case to read — its setup writes the scheduler into the firmware call table and its teardown puts the firmware's own routines back, so a machine still dispatching through that table never reaches code that a reset has reclaimed. a module whose state is its own variables needs neither: the image reclaims those wholesale.
both run on the fiber doing the loading, and must return to it: a hook neither switches fibers nor blocks.
module = [ importlist ] { declaration }
[ "..." { definition } ] .
importlist = "import" string { "," string } .
declaration = functionproto | vardecl | constdecl | typedecl
| failuredecl .
definition = fndef | declaration .
functionproto = "fn" ident signature [ "as" qualident ] .
vardecl = "var" vardefs .
constdecl = "const" ident "=" constexpr .
constexpr = "true" | "false" | "none"
| integer | hex | fp | character | string
| qualident .
typedecl = "type" ident ( "as" typename | "from" typename | structtype ) .
failuredecl = "failure" ident
( "as" qualident | "{" ident { "," ident } "}" ) .
vardef = ident type .
fndef = [ "inline" ] "fn" ident signature
( "as" qualident | "..." | { "var" vardefs } ( body | asmbody ) ) .
asmbody = "asm" [ "naked" ] compound .
signature = [ type ] "(" [ param { "," param } ] ")" [ raises ] .
vardefs = vardef { "," vardef } .
param = ( "into" | "var" | "from" ) ident type | ident [ "..." ] type .
raises = "raises" qualident { "|" qualident } .
type = typename | structtype .
structtype = "^" type [ "|" "none" ]
| "[" [ constexpr ] "]" type
| "record" recdef
| "enum" ident { "|" ident }
| "union" unionvariant { "|" unionvariant }
| "fn" signature .
typename = qualident [ "." ident ] .
recdef = "{" [ vardefs ] "}" .
unionvariant = ident [ recdef ] .
qualident = ident [ "." ident ] .
body = "{" [ stmts ] { handler } "}" .
handler = "on" failureref { "|" failureref } ":" stmts .
failureref = qualident [ "." ident ] .
compound = "{" [ stmts ] "}" .
stmts = stmt { stmt } .
stmt = return | defer | raise | for | forwith | break | continue
| if | guard | when | with | ( expr ";" ) .
return = "return" expr ";" .
defer = "defer" expr ";" .
raise = "raise" qualident "." ident ";" .
for = "for" [ expr [ ";" expr ] ] compound .
forwith = "for" "with" ident "=" expr ";" expr compound .
if = "if" expr compound [ "else" ( if | compound ) ] .
guard = "guard" ident [ typename ] "|" escape ";" .
escape = ( "return" arg ) | ( "raise" qualident "." ident )
| "break" | "continue" .
when = "when" expr "{" { case } [ elsecase ] "}" .
case = "|" caselabel { "," caselabel } ":" stmts .
caselabel = integer | hex | character | qualident .
elsecase = "|" "else" ":" stmts .
with = "with" ident "=" designator ";" .
designator = atom { index | field | deref } .
break = "break" ";" .
continue = "continue" ";" .
expr = assign .
assign = compare [ ( "=" | "+=" | "-=" ) compare ] .
compare = addition [ relation addition ] .
relation = "==" | "!=" | "<" | "<=" | ">" | ">=" .
addition = multiply { ( "+" | "-" ) multiply } .
multiply = cast { ( "*" | "/" | "mod" ) cast } .
cast = unary [ "as" type ] .
unary = [ "-" ] postfix .
postfix = primary { index | field | deref } [ args ] .
args = arg { "," arg } .
arg = ( "into" | "var" | "from" ) atom { index | field | deref }
| atom { index | field | deref } .
index = "[" expr [ ":" expr ] "]" .
field = "." ident .
deref = "^" .
primary = atom | ifexpr .
atom = "none" | "true" | "false"
| integer | hex | fp | character | string
| qualident
| "(" expr ")" .
ifexpr = "if" expr compound [ "else" ( ifexpr | compound ) ] .
ident = letter { letter | digit } .
letter = "a" ... "z" | "_" .
digit = "0" ... "9" .
hexdigit = digit | "a" ... "f" .
integer = digit { digit } .
hex = "0" "x" hexdigit { hexdigit } .
fp = digit { digit } "." digit { digit }
[ "e" [ "+" | "-" ] digit { digit } ] .
character = "'" any-char-except-quote "'" .
string = '"' { any-char-except-quote } '"' | shortstring .
shortstring = "'" { any-char-except-whitespace-or-closer } .
comment = "--" { any-char-except-newline } newline .
there are no predeclared, globally-visible failures: every failure is an ordinary module-local declaration (9.9), named fault in its owner module and reached qualified as module.fault. the standard library groups the common faults in small owner modules, each declaring a single failure named fault and grouped by where the blame lies — the environment, the code, the data, or the machine resource. a module that raises or handles one imports its owner; the file and directory modules, for instance, import core/storage.
-- core/storage
failure fault { not_found, access_failed, already_exists, no_space }
-- core/logic
failure fault { invalid_argument, contract_failed, out_of_range }
-- core/bytes
failure fault { invalid_format, buffer_overflow }
-- core/resource
failure fault { out_of_memory }
-- core/machine/aldan
failure fault { illegal, access, misaligned, unknown }
-- core/spec
failure fault { unexpected }
storage.fault names environment faults of the file system; logic.fault a violated contract; bytes.fault a malformed value or an overflowed buffer (raised by the byte, bytes, boolean, float, signed, text, and time codecs); resource.fault exhaustion of memory. aldan.fault is the machine's own failure: its kinds name a recovered cpu trap (illegal instruction, bad access, misalignment). it is never raised from ordinary source — a trap can strike any frame, so no signature declares it; the recovery boundary alone calls aldan.from_trap, which maps the trapped mcause to a kind and raises it where aldan.fault is observed. spec.fault.unexpected is raised by spec.expect when an assertion in a *_spec module does not hold (11.4). no failure number is fixed — the host maps its error codes onto failures by name, not by number (9.9).
the kinds that need a gloss:
the file and directory modules raise storage.fault for every path operation; the run-time kind reports the real cause. open, size, remove and directory.remove can only miss (not_found); create, append, replace, copy and touch can also run out of space (no_space); move and directory.make can additionally hit an existing target (already_exists).
these modules ship with the system under core/; a module imports the ones it needs (import 'core/byte) and qualifies their members by the basename (byte.peek). they are written in almac, not host intrinsics: the firmware and kernel boundaries are reached only through the two interface modules core/machine/aldan and core/platform/aldos, on which the rest is layered. the signatures below describe the public interface; failures flow through them by the rules of 9.9.
byte — access and conversion for the single byte u8. format and parse render and read the character-literal form 'x' (one byte between apostrophes); peek and poke are a bare load and store through an address.
fn peek u8 (addr address) fn poke (addr address, v u8) fn format string (var b string, v u8) raises bytes.fault fn parse u8 (src string) raises bytes.fault
bytes — operations on byte buffers viewed as slices of u8. copy is a forward memcpy, unsafe on overlapping ranges where the destination follows the source; move is a memmove, safe for any overlap; both return the unwritten tail of the destination. fill writes one u8 value to every byte; clear is fill with 0. format and parse render and read the quoted "..." form. view is the trusted forge that gives a claimed block its slice: a slice is a data address and a count, which is what its two arguments already are, so it is an empty inline asm body costing nothing. the program asserts the storage is there and outlives the view — the same assertion address-to-pointer makes (8.2.5), but with the length carried honestly rather than borrowed from an oversized array type.
fn clear (var buffer string) fn fill (var buffer string, value u8) fn copy string (src string, var dst string) fn move string (src string, var dst string) fn equal bool (a string, b string) fn compare s64 (a string, b string) fn format string (var b string, v string) raises bytes.fault fn parse string (var b string, src string) raises bytes.fault fn view string (base address, count s64)
collections/array — a growable heap-backed byte array. it stores bytes and nothing else: an element type is supplied by the caller through the byte-slice conversions of 8.2.5, which is what stands here in place of generics. a value goes in as its byte view and comes back out of view as a typed slice or as one scalar, so one module serves every element type and the two casts are the only places the program asserts what the bytes hold. storage grows by doubling; because growing moves the block, a view does not survive the append that outgrows it. a zeroed record is a valid empty array, so one may stand as a module variable (7); dispose is idempotent and leaves an array a later create can fill again. grow takes n more bytes and returns them uninitialised for the caller to write; append copies data onto the end.
type ty record { base s64, count s64, room s64 }
fn create (var self ty, room s64) raises resource.fault
fn dispose (var self ty)
fn reset (var self ty)
fn size s64 (self ty)
fn capacity s64 (self ty)
fn view string (self ty)
fn grow string (var self ty, n s64) raises resource.fault
fn append (var self ty, data string) raises resource.fault
collections/bitset — a heap-backed set of bits, n of them from create, addressed by index. create builds the set into an out parameter and dispose consumes it (from), so a double dispose or a use after one is a compile-time error; reset clears every bit.
type ty record { base address, words bit.slice }
fn create (into self ty, n s64) raises resource.fault
fn dispose (from self ty)
fn reset (var self ty)
fn set (var self ty, i s64)
fn clear (var self ty, i s64)
fn test bool (self ty, i s64)
collections/table — a growable heap-backed hash table, mapping a key of key_size bytes to a value of value_size bytes. it knows nothing else about either: both cross the boundary through the byte-slice conversions of 8.2.5, exactly as the array's elements do, so one module serves every pair of types. the two sizes are fixed at create — or, for a table that never created, by its first put — and every later key and value must match them; put says so with logic.fault.invalid_argument, while get and remove stay quiet, a key of the wrong length being one the table provably does not hold. storage is one block, a control byte a slot and then the slots: a byte is 0 when the slot is empty, 1 when a removal emptied it, and otherwise the top seven bits of the key's hash under the high bit, so a linear probe compares key bytes only where those seven agree. the slot count is a power of two and a claimed block arrives zeroed, which makes a fresh table all-empty for free. three slots in four may be spoken for; filling past that rehashes into a block twice the size, or one of the same size when it was removals rather than entries that filled it. because growing moves the block, a view does not survive the put that outgrows it. a zeroed record is a valid empty table, so one may stand as a module variable (7); dispose is idempotent. next, key_at and value_at walk the slots: next hands back the first taken slot at or after i, and -1 past the end.
type ty record { base s64, count s64, used s64, room s64,
key_size s64, value_size s64 }
fn create (var self ty, expect s64, key_size s64, value_size s64)
raises resource.fault
fn dispose (var self ty)
fn reset (var self ty)
fn size s64 (self ty)
fn capacity s64 (self ty)
fn put (var self ty, key string, value string)
raises resource.fault | logic.fault
fn get string (self ty, key string)
fn remove bool (var self ty, key string)
fn next s64 (self ty, i s64)
fn key_at string (self ty, i s64)
fn value_at string (self ty, i s64)
bit — bit-twiddling on b64. shil/shir shift; xor is bitwise exclusive-or. incl/excl set and clear a single bit; inclr/exclr a half-open range [start, end). each of the four mutators takes the bitfield by reference, updates it in place, and also returns the new value.
fn shil b64 (v b64, n s64) fn shir b64 (v b64, n s64) fn xor b64 (a b64, b b64) fn incl b64 (var b b64, pos s64) fn excl b64 (var b b64, pos s64) fn inclr b64 (var b b64, start s64, end s64) fn exclr b64 (var b b64, start s64, end s64)
signed — s64 arithmetic, conversion, and limits. lowest and highest are the s64 range bounds, written as the b64 bit patterns.
fn abs s64 (x s64) fn round s64 (x f64) fn min s64 (a s64, b s64) fn max s64 (a s64, b s64) fn format string (var b string, v s64) raises bytes.fault fn parse s64 (src string) raises logic.fault | bytes.fault const lowest = 0x8000000000000000 const highest = 0x7fffffffffffffff
float — ieee-754 arithmetic, conversion, and constants. pi, e, and epsilon are consts; the special values nan, infinity, lowest (the most-negative finite f64), and highest (the most-positive) are returned by functions, since their bit patterns are not writable as literals.
fn abs f64 (x f64) fn sqrt f64 (x f64) fn min f64 (a f64, b f64) fn max f64 (a f64, b f64) fn round f64 (x f64) fn cbrt f64 (x f64) fn trunc f64 (x f64) fn frac f64 (x f64) fn ln f64 (x f64) fn exp f64 (x f64) fn sin f64 (x f64) fn cos f64 (x f64) fn tan f64 (x f64) fn arcsin f64 (x f64) fn arccos f64 (x f64) fn arctan f64 (x f64) fn arctan2 f64 (y f64, x f64) fn is_infinity bool (x f64) fn is_nan bool (x f64) fn is_finite bool (x f64) fn decompose (x f64, var mantissa f64, var exponent f64) fn compose f64 (mantissa f64, ex f64) fn format string (var b string, v f64) raises bytes.fault fn parse f64 (src string) raises bytes.fault fn nan f64 () fn infinity f64 () fn lowest f64 () fn highest f64 () const pi = 3.14159265358979323846 const e = 2.71828182845904523536 const epsilon = 2.2204460492503131e-16
boolean — conversion for bool, plus elementwise ops on []bool buffers. format and parse render and read "true"/"false"; clear/fill/copy set or copy bool elements (copy returns the unwritten tail).
fn format string (var b string, v bool) raises bytes.fault fn parse bool (src string) raises bytes.fault fn clear (var buffer []bool) fn fill (var buffer []bool, value bool) fn copy []bool (src []bool, var dst []bool)
heap — untyped heap allocation over the kernel (core/platform/aldos). claim asks for n bytes — a type name in argument position passes its size, so heap.claim node asks for room for a node. it raises resource.fault.out_of_memory instead of returning none, so its address result converts implicitly to the pointer it initialises (the trusted boundary, 8.2.5) — assigning it to a non-null ^t local births that local (the birth rule, 7), with no ^t|none and guard needed. when the claimed type is a record with non-null members, the block comes back zeroed and its fields are deferred obligations: born one by one before the record is used as a whole (the field-level birth rule, 7). release takes its pointer `from` — freeing consumes it (6.4, 7), so the caller's binding dies at the call and a use-after-free or double-release is a compile-time error. available reports the bytes still claimable; total the bytes the pool manages.
fn claim address (size s64) raises resource.fault room for size bytes fn release (from p address) free a claimed block fn available s64 () bytes still claimable fn total s64 () bytes the pool manages
random — pseudo-random number generation; the state record holds a single 64-bit splitmix64 seed.
type state record { seed s64 }
fn seed (var s state, n s64)
fn float f64 (var s state)
fn boolean bool (var s state)
fn range s64 (var s state, lo s64, hi s64) raises logic.fault
clock — monotonic timing.
type tick from s64 fn now tick () fn elapsed_ns s64 (t tick) fn elapsed_us s64 (t tick) fn elapsed_ms s64 (t tick)
time — wall-clock time and calendar decomposition; usecs counts microseconds since the unix epoch.
type usecs from s64
type calendar record {
year s64,
month s64,
day s64,
hour s64,
minute s64,
second s64,
weekday s64,
yearday s64,
}
fn now usecs ()
fn decompose (t usecs, var cal calendar)
fn compose usecs (cal calendar)
fn format string (var b string, t usecs) raises bytes.fault
file — file input and output. a file id is an opaque handle returned by open, create, append, or replace and consumed by the other operations.
type id from s64 fn open id (path string) raises storage.fault fn create id (path string) raises storage.fault fn append id (path string) raises storage.fault fn replace id (path string) raises storage.fault fn seek (f id, offset s64) raises storage.fault fn read string (f id, var buffer string) raises storage.fault fn write string (f id, data string) raises storage.fault fn close (f id) fn is_exist bool (path string) fn size s64 (path string) raises storage.fault fn remove (path string) raises storage.fault fn copy (src string, dst string) raises storage.fault fn move (src string, dst string) raises storage.fault fn touch (path string) raises storage.fault
directory — directory operations, iteration, and path inspection. open returns a handle; next advances one entry, filling a caller-owned entry record and returning false at the end; close releases the handle. the is_* predicates never raise. change sets the current working directory and returns the normalized path (the repl's cd wraps it to print that path).
type id from s64 -- open directory handle
type entry record { -- one directory entry
name [64]u8, -- name bytes, length of them valid
length s64,
is_dir bool
}
fn make (path string) raises storage.fault
fn remove (path string) raises storage.fault
fn open id (path string) raises storage.fault
fn next bool (handle id, var e entry)
fn close (handle id)
fn change string (path string) raises bytes.fault
fn is_exist bool (path string)
fn is_file bool (path string)
fn is_dir bool (path string)
a typical listing walks the handle and slices each name to its valid length:
fn ls () raises storage.fault
var h directory.id, e directory.entry, more bool {
h = directory.open ".";
defer directory.close h;
more = directory.next h, var e;
for more {
tty.say e.name[0:e.length];
more = directory.next h, var e;
}
}
the shell-style file commands (cd, ls, mkdir, rmdir, rm, cat, touch, cp, mv) are not language builtins: the repl defines them in its rc.alm startup file over the file and directory modules.
screen — the off-screen text display and keyboard. a program draws into a surface, a width x height byte canvas it owns, with the buffer edits clear/print/top/bottom/frame, then blit copies the surface to the live screen at one of four intensities (zero/dim/normal/bright). snapshot saves the live screen and restore puts it back, so a full-screen program can leave the console as it found it. poll_key is a non-blocking read that fills a decoded key record and returns false when no key is waiting. the decoded code is flat: a text key is its ascii byte (below key_control = 0x80), a control key sits at 0x81..0x9b, so a single when on code dispatches both. the live cursor and console text output live in tty, not here.
type intensity from u8 -- a 2-bit cell intensity (0..3)
type surface [4800]u8 -- a width x height canvas
type key record { -- a decoded keyboard event
code u8,
is_key_down bool,
is_shift_pressed bool,
is_ctrl_pressed bool,
is_alt_pressed bool,
is_key_up bool,
}
fn snapshot bool ()
fn restore ()
fn blit (source []u8, c intensity)
fn clear (var b surface)
fn print (var b surface, x s64, y s64, s string)
fn top (var b surface, s string)
fn bottom (var b surface, s string)
fn frame (var b surface, x s64, y s64, w s64, h s64) raises logic.fault
fn poll_key bool (var k key)
const width = 120 -- text columns, s64
const height = 40 -- text rows, s64
const size = 4800 -- width * height, the surface length
const zero = 0 -- intensity values
const dim = 1
const normal = 2
const bright = 3
const key_control = 128 -- code >= this (0x80) is a control key
const key_arrow_up = 129 -- control-key code (bit 7 set), u8
const key_arrow_down = 130
const key_arrow_left = 131
const key_arrow_right = 132
const key_home = 133
const key_end = 134
const key_page_up = 135
const key_backspace = 136
const key_tab = 137
const key_enter = 138
const key_page_down = 139
const key_insert = 140
const key_del = 141
const key_f1 = 142
const key_f2 = 143
const key_f3 = 144
const key_f4 = 145
const key_f5 = 146
const key_f6 = 147
const key_f7 = 148
const key_f8 = 149
const key_f9 = 150
const key_f10 = 151
const key_f11 = 152
const key_f12 = 153
const key_esc = 155
tty — the operator console: formatted text output and line input. say and sayin write a freely-mixed sequence of values (each lifted into text's formattable union, appendix below), say ending the line and sayin not; neither raises, a formatter overflow being absorbed internally. ask writes a prompt and reads one edited line into the caller's buffer, returning the bytes read. at, show_cursor, and hide_cursor drive the live console cursor that say/sayin/ask advance.
fn say (xs ...text.formattable) fn sayin (xs ...text.formattable) fn ask string (prompt string, var buffer string) fn at (col s64, row s64) fn show_cursor () fn hide_cursor ()
text — rendering values into a byte buffer. formattable is the shared vocabulary for textual output: each primitive type lifts into it (8.2.5), so a variadic ...formattable accepts a mixed argument list (tty's say/sayin take the same union). format_any renders one value into the buffer; format renders a whole pack, writing each element in turn; both return the written slice and raise bytes.fault on overflow.
type formattable union
number { val s64 } | real { val f64 }
| truth { val bool } | str { val string }
fn format_any string (var buffer string, v formattable) raises bytes.fault
fn format string (var buffer string, xs ...formattable) raises bytes.fault
debug — diagnostic tracing. trace formats a mixed sequence of values into one line and submits it to the firmware tracer device, a debug channel separate from the console; an overflow truncates rather than raising, so trace never fails.
fn trace (xs ...text.formattable)
sound — the program-facing audio api, a thin frontend over the aldos sound stack (aldos/mixer owns the ring device and mixes two channels; aldos/pcm streams sample windows; aldos/apu is the beeper synth). play streams an in-memory 24khz mono s16le buffer end to end. begin/ready/fill/finish are that same stream taken apart so a streaming caller can poll and redraw between fills; stop aborts. tone sounds a square wave that mixes over any pcm playback. (the .echo file player lives in programs/sound/phono; the mixer lets pcm and beeper play at once.)
fn play (samples string) fn begin (w []u8, n s64) fn ready bool () fn fill (w []u8, n s64) fn finish () fn stop () fn tone (frequency s64, delay s64)
spec — assertion helper for the *_spec auto-tests (11.4). expect raises spec.fault.unexpected when its argument is false and does nothing when it is true. a spec module beside core/m, named core/m_spec, is re-imported automatically when the prompt (re)imports core/m; gather its checks under an entry point (by convention verify) and run it as m_spec.verify.
failure fault { unexpected }
fn expect (assertion bool) raises fault
task — cooperative multitasking. yield hands control to the scheduler so other ready tasks can run; on hosts without a scheduler it is a no-op. the module also runs resident own-stack agents: every runs work on its own coroutine stack every period_ms (cooperatively, only when the system yields to it), returning a handle; cancel stops the agent and frees its stack. each agent carries its own resource-leak counter (core/scope), and cancel reports an agent that ended with resources still outstanding.
type handle as address fn yield () fn every handle (period_ms s64, stack_size s64, work fn ()) raises resource.fault fn cancel (h handle)
the two interface modules are part of the standard library but sit below it, wrapping the host boundary as thin asm trampolines: core/machine/aldan exposes the rom firmware call table (video, keyboard, sound, the byte-copy primitives behind bytes), and core/platform/aldos the kernel os-call table (memory behind heap, files and directories, yield). ordinary programs use the modules above rather than these directly.