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 29 words are reserved and may not be used as identifiers:
as asm break const continue defer else enum failure false fn for guard if import inline 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), & (out-parameter), | (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 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 | structtype ) .
type = typename | structtype .
structtype = "^" type [ "|" "none" ]
| "[" [ constexpr ] "]" type
| recordtype
| enumtype
| uniontype
| functiontype .
typename = qualident [ "." ident ] .
a type declaration names another type only through as — type byte_count as s64 is an alias for s64 (the bare type byte_count s64 is rejected). a structural right-hand side — a pointer, array, slice, record, enum, union, or function type — carries no as. the as 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).
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 a non-null pointer: per-element initialisation is unverifiable. arrays of pointers are declared optional (^t|none) and narrowed at use (see 6.4, 9.5).
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.
type point record { x s64, y s64 }
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 — a non-out 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).
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.
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 = 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. 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 pointer is unsound, so such a variable's type may not contain one — not as a pointer, a record field, or an array element; use an optional pointer. 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 pointer type ^t, which is born by assignment and only by assignment. it may be used — read, dereferenced, or passed with or without & — only where a direct assignment to it 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 ^t value assigns to a ^t variable. 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; };
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 a non-null pointer or contain one, and a record field or an array or slice element may never be non-null (6.2, 6.3). all other scalars keep plain zeroing with no birth obligation, so out-parameter idioms like float.decompose x, m&, 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).
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
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, an array converts to a slice where one is required, none converts to an optional pointer type, and a non-null pointer widens to the matching optional pointer.
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 nor an atom-starter (literal, qualident, or `(` ).
args = arg { "," arg } .
arg = 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 parameter declared with & must be supplied with a writable lvalue, itself followed by & at the call site (see 10.1). the converse marker is permitted but not required: an argument to an immutable parameter may also carry &, asserting a writable lvalue that the callee receives read-only — a mutable argument is acceptable where an immutable one is wanted, never the reverse. 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 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 ends the narrowing, and a list traversal guards afresh each iteration:
type node record { next ^node|none, value s64 }
fn sum s64 (head ^node|none)
var total s64, p ^node|none {
total = 0;
p = head;
for p != none {
guard p | break;
total += p^.value;
p = p^.next;
}
return total;
}
the for statement is the only looping construct; condition and step are both optional.
forstmt = "for" [ 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;
}
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.
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 comma-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, an out-parameter, a writable dereference, or a component of one yields a writable name; a non-out 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. as in bss (see 7), a local's type may not contain a non-null pointer in a record field or an array element; but a local may itself be a non-null pointer ^t, born by assignment (the birth rule, 7).
a function may not return an aggregate (array, record, slice, or union) by value; an out-parameter is used instead.
a parameter is a binding ident [ & ] type. scalars — bool, integers, f64, pointers, address — are passed by value; aggregates — arrays, records, slices, unions — by reference, regardless of &. a union subtype value may be supplied for a parameter of the parent union type (see 6.5).
the & annotation marks an out-parameter: the call site must supply a writable lvalue followed by &, and the callee may modify the backing storage. a parameter without & is immutable in the callee: its binding may not be reassigned, and an aggregate passed by reference may not have its fields or elements written. an argument to such an immutable parameter may nonetheless be written with a trailing & at the call site: the marker only asserts that the argument is a writable lvalue, which the callee still receives read-only. so a writable lvalue passes to either kind of parameter, while a read-only argument is rejected wherever & appears. a pointer parameter is the binding, not its pointee: the pointer may not be rebound, but writing through it (p^ = ..., p^.f = ..., or passing p^& to an out-parameter) reaches the shared mutable storage and is permitted (6.4). to share a writable buffer, then, pass a plain ^t — & 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 & 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.
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, the out 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 (from& context, 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
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.
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 | 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 = 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 | break | continue
| if | guard | when | with | ( expr ";" ) .
return = "return" expr ";" .
defer = "defer" expr ";" .
raise = "raise" qualident "." ident ";" .
for = "for" [ 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 = 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 (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.
fn clear (buffer& string) fn fill (buffer& string, value u8) fn copy string (src string, dst& string) fn move string (src string, dst& string) fn equal bool (a string, b string) fn compare s64 (a string, b string) fn format string (b& string, v string) raises bytes.fault fn parse string (b& string, src string) raises bytes.fault
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 (b& b64, pos s64) fn excl b64 (b& b64, pos s64) fn inclr b64 (b& b64, start s64, end s64) fn exclr b64 (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 (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, mantissa& f64, exponent& f64) fn compose f64 (mantissa f64, ex f64) fn format string (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 (b& string, v bool) raises bytes.fault fn parse bool (src string) raises bytes.fault fn clear (buffer& []bool) fn fill (buffer& []bool, value bool) fn copy []bool (src []bool, 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. release takes a pointer by value. 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 (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 (s& state, n s64)
fn float f64 (s& state)
fn boolean bool (s& state)
fn range s64 (s& state, lo s64, hi s64) raises logic.fault
clock — monotonic timing.
type tick as 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 as 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, cal& calendar)
fn compose usecs (cal calendar)
fn format string (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 as 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, 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 as 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, 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, e&;
for more {
tty.say e.name[0:e.length];
more = directory.next h, 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 (the raw bit-packing is hidden) and returns false when no key is waiting. the live cursor and console text output live in tty, not here.
type intensity as 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_control_key bool,
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 (b& surface)
fn print (b& surface, x s64, y s64, s string)
fn top (b& surface, s string)
fn bottom (b& surface, s string)
fn frame (b& surface, x s64, y s64, w s64, h s64) raises logic.fault
fn poll_key bool (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_arrow_up = 1 -- control-key code, u8
const key_arrow_down = 2
const key_arrow_left = 3
const key_arrow_right = 4
const key_home = 5
const key_end = 6
const key_page_up = 7
const key_backspace = 8
const key_tab = 9
const key_enter = 10
const key_page_down = 11
const key_esc = 27
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, 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 (buffer& string, v formattable) raises bytes.fault
fn format string (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 — audio playback of 24khz mono pcm16. play streams a sample buffer; play_file streams a .pcma file.
fn play (samples string) raises storage.fault, logic.fault fn play_file (path string) raises storage.fault, bytes.fault
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.