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 32 words are reserved and may not be used as identifiers:
as asm break const continue defer else enum failure false final 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". the two integer notations differ only in how they read: both have type integer (6.1) and adopt the type they meet, so a hexadecimal may initialise an s64 as readily as a b64. the notation is a note to the reader about what the number is — a quantity in decimal, a bit pattern in hexadecimal — and the type says the rest.
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 b8. a string literal is a byte sequence in quotation marks; it cannot contain the delimiting quotation mark. its bytes are shared by every run of the code that names it, so a literal of at least one byte has type const string, a view (6.2); the empty literal "" has none to share and has type string, which is what lets it birth a writable slice (7). 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, guard escape mark), * (multiplication, when-case bullet), and ... (variadic parameter marker inside a parameter list, the unnamed failure inside a raises clause and as a handler's target, deferred-definition marker after a signature, and the module interface/body separator) are defined with the constructs that use them (6.4, 6.5, 8.2.2, 9.4, 9.5, 9.9, 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. a type may stand between the name and the =, written exactly as a variable's is; without one the literal's own kind decides.
constdecl = "const" ( ident [ type ] | ident "." ident ) "=" constexpr .
constexpr = "true" | "false" | "none"
| integer | hex | fp | character | string
| qualident [ "." ident ]
| "fn" qualident
| arrayconst
| recordconst .
arrayconst = "[" constexpr { "," constexpr } [ "," ] "]" .
recordconst = "{" constexpr { "," constexpr } [ "," ] "}" .
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 an enumeration's variant, color.red, the constant is that value, of the enumeration's type; if it is a scoped constant of a distinct type, opcode.add (5.3), the same. if it is another constant, the new constant receives its value and type. a written type must admit the value — an untyped integer literal, decimal or hex, fits any integer type — and a value that would not survive the width it is given, such as 300 in a b8, is rejected rather than truncated.
const limit = 100 const origin = none const node_size = node -- size of type node in bytes const mask b64 = 0xff const first = color.red -- of type color
a constant names no expression: there is no arithmetic on the right of the =. a value that needs one is written as a function.
two forms have no type of their own and so appear only where one is written — as a field of a record constant (5.2) or an element of an array constant whose slice type is declared: a record written in place, and fn d, the address of the function d (the function value of 9.2). d must already have its final address: a function of an imported module, or of an earlier unit at the prompt. a function of the module being compiled does not yet have one, and nothing patches a constant afterwards, so it is refused; so is an inline function (10.2), which has no entry to point at. fn d alone is not a constant — a function's second name is written as an alias (10.3).
an array constant is a bracketed list of constants, laid out contiguously in the image. the name is bound to the slice over those elements, never to an array, so a constant table indexes, sub-slices, aliases with with, and passes wherever a []t is wanted. a string constant is this form's []b8 case, written with quotes instead of brackets.
const inc_tab = [22861, 21578, 20367] -- const []s64 const sbox = [0x0f, 0x1e, 0x2d] -- const []s64: a hex literal is untyped const keys []b8 = [1, 2, 3] -- const []b8 const gains []f64 = [0.5, 0.25] -- const []f64 const name = "aldan" -- const string, the string case const words []string = ["one", "two"] -- const []string
the element type is the element type of the declared slice type when one is written, and otherwise the type of the first element, with an untyped integer settling on s64 as it does everywhere else (8.2.5). every element must then fit that type. a declared array type [n]t is not a slice type and is rejected: an array constant is always a slice.
an element is a scalar constant — a boolean, an integer, a floating point number, a character, an enumeration's variant, or a name for one — a slice constant — a string written in place, or a name for a string or an array constant — or a record constant (5.2), written in place or named. where the slice type is declared, an element may also be a function value, fn d (5). a table of strings is the form a list of names takes, and it is written where the list belongs rather than parsed out of one long string at runtime. what such a table holds is the slices themselves; the bytes each of them reaches live in the image beside it.
a nested list and none are not elements, and the list may not be empty. none has no element type; a nested list is refused because an element that is a slice is a constant that already exists rather than one written in place, so a table of tables is spelled by naming them (const t = [a, b]). a record element is different: the table holds the record's bytes, not a reference to them, so a row is laid down where it stands and may be written in place. an array is never an element. there are still no aggregate values, only aggregate places (9.1): a table of records is a place whose rows are places, selected from and never copied whole.
the elements live in the image, so an array constant is a read-only place, and its type is the view of the slice type, const []t, whatever slice type is written (6.2). it may be indexed and sub-sliced, but never written through, and it may not be passed to a variable (var) or out (into) parameter; nor may it be assigned to a []t, only to a const []t.
const t = [1, 2, 3] t[0] -- 1 t[1:3] -- a const []s64 of two elements t[0] = 9 -- rejected: read-only const a = [1, 2] const b = [3] const nest = [a, b] -- []([]s64), by naming the inner tables nest[0][1] -- 2 const bad = [[1]] -- rejected: a nested list is not an element
a record constant is a record laid down in the image at compile time. its type is written on the declaration, and the values follow in braces, one for each field in the order the fields are declared; every field is given, and a trailing comma is permitted. a field's value is any constant its type admits: a scalar, an enumeration's variant, none for an optional pointer, a string or a slice constant for a slice field, fn d for a function field (5), and for a record field a record written in place or a named record constant. a field no constant can fill — a non-null pointer, an address, an array, a union — makes a type that has no constants.
type point record { x s64, y s64 }
type row record { name string, code b8, run fn s64 () }
const origin point = {0, 0}
const corner point = origin -- the same record, not a copy
const table []row = [
{"add", 1, fn ops.add},
{"sub", 2, fn ops.sub},
]
like an array constant, a record constant is a read-only place rather than a value, and a slice read out of it is a view (6.2) whatever its field's type. its fields are selected and read, it may be aliased with with, and it is passed by reference wherever a bare record parameter is wanted; it may not be written through, passed to a var or out parameter, or copied whole (9.1). what a record constant replaces is the run of field assignments that would otherwise build the same record in setup, and the parallel tables, read in step by one index, that stand in for a table of records.
origin.x -- 0 table[1].code -- 2 origin.x = 1 -- rejected: read-only var p point p = origin -- rejected: a record is not assignable whole
a distinct type (6) has a namespace of its own, as an enumeration does: const t.name = v declares a constant in it. the qualifier is the type, so no type is written after the name; the value must be admitted by t as for any typed constant. the constant is read as t.name, or m.t.name from an importer, wherever an enumeration's variant is — an operand, a constant, a when label (9.4) — and it has type t, not t's base.
type opcode from b8 const opcode.add = 0x33 const opcode.sub = 0x3b type perm from b64 const perm.read = 0x1 const perm.write = 0x2 or perm.read, perm.write -- a perm m.opcode.add -- from an importer, no alias needed
only a distinct type takes scoped constants: an enumeration's and a union's namespaces are their variants', and a record, a transparent alias's target, a built-in or a structural type has none. the declaration must stand in the type's own module and name the type by its own name, so neither an importer nor an alias declares into it. reading goes through any name of the type: an alias, type op as m.t, reaches m.t's constants as op.name. a constant declared in the interface section is public; one in the body is the module's own.
the namespace is the type's alone. type b from a takes none of a's constants, being a type of its own, and a name in t's namespace never falls back to t's base or to a constant of the module.
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 = [ "const" ] ( 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, and each has its own namespace of scoped constants (5.3). 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).
there are two integer families, and the letter says which. s64 is a number: it is signed, and it is the type a count, an index, or an offset should have. bn is a field of n bits: it is unsigned, so its ordering, its division and its mod read the whole width as a magnitude, and it indexes its own bits (8.2.3). b8 is also the byte, so string is [ ] b8.
the language has no un-prefixed unsigned type. what c would spell unsigned long is b64, and the naming makes the choice at each declaration an explicit one: a value is a number or it is bits.
bool and b8 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.
a slice may be a view. const []t — const string for const []b8 — is the slice type that may not be written through: an element of it is a read-only place, a subrange of it is a view, and a slice read out of it is a view in turn, so const []string is const []const string. a const prefix applies to a slice type and to nothing else. a []t converts implicitly to const []t wherever a slice is required; no conversion, implicit or written, goes the other way, and an as cast of a view to a slice type yields the view of that type (8.2.5).
what is a view is what may not be written: a constant (5) and a nonempty string literal (3); a plain parameter, inside its function (10.1); and anything read out of a read-only place — a constant, a plain parameter, a with alias of one (9.10), an element of a view. a slice made over a read-only place — a subrange of a read-only array, the array->slice conversion of one, a byte view — is a view too. so a view's bytes are written by no one: it cannot be stored in a []t binding, field or module variable, returned as a []t, or passed to a var or out parameter of type []t. a binding, field, result or var parameter that holds a view is declared const []t.
fn base_name const string (p string) { return p[1:(length p)]; }
var b [8]b8, s const []b8, w []b8
s = "ab"; -- a literal is a view
s = b[0:2]; -- a []b8 converts to its view
w = s; -- rejected: a view is not writable
s[0] = 'x'; -- rejected: read-only
a constant table is written as an array constant (5.1). it is a read-only slice over elements laid out in the image, not an array, and it is the only way to give a sequence its contents at compile time, as a record constant (5.2) is for a record: a variable, module-scope or local, is always zero-initialised (7).
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 slice is not an owner and carries no alias taint: it is an address and a count, and it copies freely. one rule governs it — the frame rule. a slice taken over a local array views the activation frame, which is gone once the function returns, so such a slice may not escape: it may not be returned, nor stored in a module variable, in a var or out parameter, or in any field reached through a pointer. it may be read, indexed, passed as an argument (a callee cannot outlive the call), and stored in another place that dies with the same frame — another local, or a field of a local record. the taint follows the view: a subrange of a frame slice, an as []b8 or as string byte view of one (8.2.5), and a slice local assigned one are all frame slices in turn. a with alias (9.10) carries the residence of the place it caches, so a slice taken through an alias of a local array is a frame slice too.
what the rule measures is where the BYTES live, not where the header does. a slice parameter's 16-byte header is copied into the callee's frame while the elements it views belong to the caller, so returning a parameter, or a subrange of one, is sound and is the idiom by which a function hands back a view: the caller passes the storage in (a var buffer) and reads the written extent out. a slice over module-scope storage, or over a heap block, likewise outlives any call.
fn line []b8 () var b [16]b8 { return b[0:4]; } -- rejected
fn line []b8 (var b [16]b8) { return b[0:4]; } -- the idiom
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. a run of names may share one type, written once after the last of them (see 7); the fields are laid out as though each had been written separately.
recordtype = "record" recdef .
recdef = "{" [ vardefs ] "}" .
vardefs = vardef { "," vardef } .
vardef = ident { "," 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.
the third half is exclusivity: at any point a place has at most one mutable path. because a borrow can be neither stored nor returned, it lives exactly as long as one call or one block, so this needs no lifetimes — only two local rules over places. a place is a variable followed by its selectors: fields, indexes, dereferences. two places overlap when they start at the same variable and neither path turns off the other: a place overlaps everything inside it, a.x and a.y are disjoint, a[0] and a[1] are disjoint, but an index that is not a literal may be any element, so a[i] overlaps every a[...]. the rules are the argument rule (10.1), that a call may not hand over a place twice when one of the two may be written, and the alias rule of with (9.10), that while a with or for with binding is live its place is reached through the binding alone. two gaps are known and not yet closed: a function reaching a module variable directly while that variable is also passed to it, and two slice values viewing the same bytes — a slice copies freely, so the rules see only the variable it was taken from.
an enumeration lists a fixed set of named variants separated by the bar operator; at least one variant is required. its representation is b8, 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 clause may close with the unnamed failure, written ... — alone (`raises ...`) or after names (`raises file.fault | ...`). it stands for the failures the function may propagate without naming them: typically those of a function value it was handed, which are not known where the signature is written. it closes the clause because it already covers whatever a further name would say; nothing may follow it. a function whose signature carries it is called open, one whose clause is a plain list closed. what the named part of an open clause promises is undiminished — those failures are still exactly the ones the body can propagate under a name — so `raises file.fault | ...` tells a caller which failure to expect and handle by name, and that others may arrive besides. the unnamed failure is part of the type, unlike the two markers below, and 9.9 gives the rules that verify and discharge it.
a function value converts to an open function type when the two agree on everything else — the parameters, their markers, and the result. the source's own failures are not compared: a function raising fewer, or none at all, keeps the promise the open type makes. the conversion never runs the other way, since a caller of an open value could not declare what it may raise. a closed function type accepts only its own type, which is what keeps a var or into parameter, an alias, and a definition matched against its prototype (10.3) exact.
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 of a variadic parameter (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. neither is to be confused with the unnamed failure above, which stands after raises rather than inside the parameter list, and which the type does carry.
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.
a run of names separated by commas may share one type, written once after the last of them: var first, last s64 declares two locations of type s64, exactly as var first s64, last s64 does. the type is what ends a run, so a comma after it opens the next — var x, y s64, r f64 groups x and y and leaves r alone — and grouped and singly-typed names mix in one list in any order. the grouping is a spelling and nothing more: each name is a location of its own, laid out and initialised as though it had been written separately, and a distinct type stays distinct for every name of the run.
vardecl = ( "var" | "final" ) vardefs .
a declaration headed final instead of var declares final variables (7.1); everything said here of a var holds for them too.
module-scope variables reside in the module's bss segment and are zero-initialised. a zeroed slice, non-null pointer or address is not a valid one, so a variable that is or holds one — directly or as a record field — starts unborn and is born in the module's setup (7, 11.5). one holding a non-null pointer or address under an array element or a union payload cannot be tracked and is rejected; so is a module ^t whose pointee itself carries such members (it is not tracked through the pointer). use an optional pointer, or s64 for raw address bits, there. 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 var head, tail s64 -- one type, two locations var x, y s64, scale f64 -- a run, then a name of its own
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 — or of slice type, 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)
a slice is held to the same rule because a zeroed slice views address zero: an index through it reads or writes there, and nothing in the slice says so. an empty slice is written as a value — "" for a string, a[0:0] over an array — and assigned like any other. a slice owns nothing, so it has no death, and a from parameter (10.1) may not take one.
s = buffer[0:n]; -- born here s = ""; -- the empty slice, born the same way split path, into s; -- a callee that fills s takes it into
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 or slice 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 leaf: a non-null or slice 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 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. 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). a slice under an array element or a union payload is not tracked: zero stays the empty slice there.
a module-scope variable of such a type — a slice, a ^t, an address, or a record with such leaves — obeys the same rule, with the module's setup (11.5) as its statement sequence. it starts unborn, and is born for good by setup's own outermost sequence — an assignment, a leaf store, or an into argument there — and by nothing else: a birth anywhere else lasts for its block, like a local's. since a name binds where it is defined and nothing compiles twice, the rule is positional and needs no flow analysis. every function that uses such a variable stands below setup; a variable declared after setup, or one setup leaves unborn, is an error. within setup the births come first: setup may not return early while one is pending, and while one is pending it may not call or take the address of a function that may run code defined further down — a prototype still without a body, or a function whose body reached one. a birth may follow a call that can raise only when the fault leaves setup, which fails the import (11.5) so that nothing ever sees the variable unborn; once a birth follows such a call, setup may have no on handler, since a handler could end it normally past the birth. at the prompt, which has no setup, the unit's top-level statements play its part: `var s string` alone leaves s unborn, and a later top-level `s = ""` births it for good. a prompt unit is committed before it runs, so there a birth may not follow a call that can raise at all.
a module ^t owns its pointee for as long as the module lives. it may be dereferenced in place or borrowed as a plain argument, but not copied into a binding, returned, or passed from (6.4): any of those would alias it. a pointer from heap.claim is born in a setup that raises the claim's fault on:
var p ^cell
fn setup () raises heap.fault {
p = heap.claim cell;
}
born says a slice was assigned a real value, not that the value is the one a later use wants: the language has no bounds checks, so an empty slice indexed past its length reads whatever lies there, exactly as a local's would. 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; a slice handed to a var parameter must be born first, and one the callee only fills is an into parameter.
a final variable is written once. it is declared exactly as a variable is, with final in place of var — at module scope, or in a function's locals (10), where final and var clauses mix freely — and the two differ only in what may be done with the name once it holds a value.
final cache []b8
fn setup () {
cache = view_at (cache_base as address), (capacity * page_bytes);
}
fn lba_of s64 (vol s64, page s64) final sh b64 {
sh = kore.shil (page as b64), vols[vol].shift;
return vols[vol].part_lba + (sh as s64);
}
a final of any type starts unborn and obeys the birth rule above, which for it is the whole story: it may not be read, passed or aliased before its birth, and its birth is its one write. it is born by an assignment to the bare name or by being passed as an into argument (10.1) — the only way a record or an array, which have no values to assign, are born — and by nothing else. a write through a field or an element is not a birth. the birth is positional as every birth is: it lasts to the end of the statement sequence that holds it, so a birth inside a branch does not survive the branch, a conditional birth is written with the if expression, and a birth inside a loop body is made afresh on every iteration. a module final is born for good in setup's outermost sequence (11.5), and a final declared after setup is never born.
once born, the name is fixed: an assignment, += or -=, a bit store, a var argument or a second into argument naming it is rejected, and so is a write through a with alias of it (9.10). a birth may not stand where the positional rule cannot see it run once and first: in a for step (9.6), which runs after the body and perhaps never; in a defer (9.8), which replays at every exit; or in an on handler (9.9), where the fault may have come after the birth. nor may a local final be used in a handler at all, since the fault may have come before it.
final fixes the binding, not what it reaches. a final scalar, record, array or union is a read-only place, written by no one after its birth. a final slice cannot be rebound, but its elements are as writable as its type says — cache above is written through every time the cache fills — and the view, when one is wanted, is written as the type, final s const string. a final pointer cannot be rebound; its pointee is written through it as through any pointer. an owned final pointer may still be moved out or passed to a from parameter (6.4, 10.1), which is its death; being final, it is never born again. a module final owns its pointee for good: it is neither moved nor taken with null.
an imported final is as fixed to its importers as it is to its own module.
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
| "fn" atom { index | field | deref }
| "(" 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).
an atom marked fn denotes a function value: the designator after the marker must be of function type — a function, or a variable, field, or element holding one — and it is taken, never called. it is the only way to write a function value; a function designator without the marker is a call (9.2). `fp = fn clock.now` assigns the function, `pick fn x.f` passes the one held in a field.
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. each of the three is polymorphic in the same way: over bool it is the logical operation, over an integer type the bitwise one. there is no separate spelling for the bitwise forms — these three are the whole of and, or, and not in the language.
and ( x0, x1, ... ) conjunction, logical or bitwise or ( x0, x1, ... ) disjunction, logical or bitwise not ( x ) negation, logical or bitwise
and and or are forms, not functions: they cannot be taken as values with fn, and they take at least one argument. not is an ordinary function. since an argument is an atom (9.2), composite arguments are parenthesised:
and (0 <= i), (i < limit)
the first argument decides which operation the form denotes, and the remaining arguments must agree with it.
over bool, and and or short-circuit: they evaluate left to right and stop at the first false or true argument respectively — an exception to the call rule of 9.2 — and yield bool. not b is the logical negation.
over an integer type, and and or combine every argument with a bitwise and or a bitwise or. there is no short-circuit here: all arguments are evaluated, in order. the arguments must share one type, except that an untyped literal adopts the others', and the result has that shared type. not x is the bitwise complement, within the operand's own width — on b8 it complements the eight bits, so not 0xf0 is 0x0f, not a value outside the type.
and mask, 0xff -- bitwise, when mask is b8 / b64 / s64 or flags, 0x08 not mask -- complement
mixing the families is an error: every argument of one form is bool, or every one is an integer of a single type. the bit module (8.2.3, appendix b) supplies the shifts, xor, and the bit counts.
the binary +, -, *, / apply to integer and f64 operands; mod applies to integer operands only. unary - applies to every integer type and to f64.
integer arithmetic wraps around: a result is reduced modulo 2^64 for s64 and b64, and modulo 2^8 for b8; overflow never raises a failure. on the bit types the wrap is the whole story — there is no sign to lose — and unary - is the two's-complement negation, 0 - x.
/ and mod read their operands the way the type does: signed on s64, unsigned on b8 and b64. so a b64 whose high bit is set divides as a magnitude near 2^64, not as a negative. signed division truncates toward zero and x mod y takes the sign of the dividend x, so that x = (x / y) * y + (x mod y) holds; on the bit types both operands are non-negative and the identity holds trivially. division by zero follows the rv64 processor and raises no failure: x / 0 yields -1 (all bits set on a bit type), 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 a bit type — b8 or b64 — and integer expressions i, j:
bit 0 is the least significant. an index at or past the width, and the part of a range that lies past it, read as zero rather than raising.
both forms are places: either may stand on the left of an assignment, and the store leaves every bit outside the named field unchanged.
b [ i ] = truth -- truth is a bool b [ i : j ] = v -- v is of b's own bit type
the operand must itself be a place — a variable, a field, an element — under the same rules a whole-variable assignment answers to (9.1). the bounds are ordinary expressions, evaluated once, and a value wider than the field is truncated to it rather than overwriting its neighbours.
bitwise and and or are the integer forms of the and and or operators (8.2.1); the bit module exports the shifts, xor, and the bit counts.
the six relational operators compare two values of the same type and yield bool. f64 comparison follows ieee-754 ordering. an integer comparison follows its family: s64 and integer order as signed, the bit types b8 and b64 as unsigned magnitudes.
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 conversion never makes a view writable (6.2): a slice made out of a view, or over a read-only place — the byte view of a constant or of a plain parameter, a typed slice over one, the array->slice conversion of a read-only array — is the view of the slice type named, so k as []b8 over a constant k is a const []b8. reading the leading bytes as one scalar is a read like any other.
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 b8 target keeps the low 8 bits; a wider integer target extends the way the source's family reads, sign-extending from s64 and zero-extending from b8. a conversion between an integer type and f64 reads the integer side by its family as well: s64 crosses as a signed value, a bit type as an unsigned one, so a b64 in the high half becomes a double near 2^64 rather than a negative. 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).
a cast to the type its operand already has is a compile-time error: it converts nothing, and it is either noise or a sign the writer believes the operand is something else. an alias (type x as y, 6) names the same type, so a y cast to x is one such. an untyped literal has no type of its own until it meets one, so 0 as b8 stays a conversion.
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]b8 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 final variable is assigned once, by the assignment that is its birth (7.1).
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, fn, 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 is the bare designator: a function designator used as a value, with no arguments following, is called with none, whatever the context expects. `last = clock.now` calls clock.now; so does `fp = clock.now`, which therefore needs clock.now to return a function. the function value itself is written with the fn marker (8.1): `fp = fn clock.now`. whether a designator is a call is thus read off the designator alone, never off its surroundings. the one place a function designator is not a value is where it names storage — the left side of an assignment and a var or into argument — so `fp = fn g` stores into fp rather than calling it. only a named function or a function-typed variable or parameter can be called; a function held in a field or element (`r.f`, `a[i]`) must be taken with fn into a variable first, and written bare it is an error.
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, b8, 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, written out or named by a constant of the enumeration's type (5); the other types admit integer and hex literals, integer constants, and — for b8 — character literals in 0 .. 255. a constant of a distinct type, such as a scoped constant (5.3), matches only a scrutinee of that same type. 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 bullet prefixing each case, not a separator between them, and distinct from the bar of enum and union declarations (6.5) and of guard (9.5). 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 file.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" ( "..." [ "with" ident ] | 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 file.fault binds io as a second name for file.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 file.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 declares each failure in the module whose operations raise it (appendix b.2) — core/file, core/heap, core/bytes, core/socket, core/boolean, aldos/machine — each declaring one failure named fault, so a path error is file.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.
the unnamed failure (6.6) takes part in that computation as an ordinary member of the set, and is subject to the same exactness. a call to an open function contributes it, along with the failures that call names; it appears in the caller's clause if and only if it survives to the end of the body. so a body that calls an open function is itself open unless it discharges the unnamed failure, and a clause that carries ... over a body with nothing to relay is rejected as readily as a name that is never propagated. no raise statement produces it: a raise names a kind, and a kind belongs to a failure that has a name. it enters a body only by being relayed — through a call whose type carries it, or from an asm body, whose clause is trusted like any other.
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 handler written on ... is the unnamed handler, and it discharges every failure the body can propagate — the named ones and the unnamed one alike. it names no target: `on a | ...` and `on ... | a` are both rejected, since the name beside it could never be reached. it stands last, for the same reason: no handler after it could run. it tests nothing, so it is the one handler that costs no comparison; control falls into it from whatever the handlers above did not match.
the unnamed handler may bind the failure it caught: `on ... with f` introduces f as a fresh name for that failure, of type mirror.fault_kind (11.3, appendix b.3). the name is bound as a with statement binds one (9.10) — it is in scope to the end of the handler, may shadow an outer name of the same name, and is read-only — and it is the only way to learn which failure ran when the head named none. only the unnamed handler binds: a named handler's head has already said which failure it caught, so there is nothing left to ask. the value is opaque; its operations live in mirror, and reading it as a number takes an explicit `as b64`.
fn describe string() {
attempt;
on ... with f:
return mirror.fault_name_of f;
}
a named handler discharges its own failure only, and never the unnamed one — the unnamed failure is by definition what no name accounts for. named handlers may therefore stand in an open body freely, in front of an unnamed handler or without one: they run first, taking the failures they name, and what is left keeps the clause open. this is the useful arrangement, since the whole point of naming a failure in an open signature is that a caller can handle that one by name:
fn eval(f fn () raises ...) raises file.fault | ... {
file.touch stamp;
f;
}
fn run() {
eval work;
on file.fault.not_found:
say "no stamp";
on ...:
say "the work failed";
}
eval names the failure it raises itself and relays whatever work raises; run handles the one by kind and the rest with an unnamed handler, so run propagates nothing and carries no raises clause of its own.
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 b8(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. while it is in scope it is the only way to that storage (6.4): any designator that overlaps the place — the same element, a part of it, the whole it is part of, or an element of the same array whose index is not a literal — is rejected, whether it reads, writes or passes it, and so is a second with over it. disjoint places stay reachable: with p = a[0] leaves a[1] and every other field usable. once the binding's block closes, a write made through it is visible through the original path. 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" | "final" ) 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, in which a run of names may share one type (7). 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 parameter list is not this list: its markers and variadic form are its own, so every parameter carries its own type. a clause headed final instead of var declares final locals (7.1), and the two kinds of clause mix in any order. 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.
no two arguments of one call may overlap (6.4) when either of them may be written by the callee. a var, into or from argument may be written; so may a plain ^t argument, whose pointee is the callee's to write. so g var p, var p; and g var a[i], var a[j]; are rejected, as is g var r, r.f; where r.f is a record, an array, a union or a slice, which the callee would read by reference while writing r. a scalar argument is a copy and never overlaps: g var r, r.n; is admitted, and so are g var r.x, var r.y; and g var a[0], var a[1];. a nested call has finished before the outer one starts, so g var e, (h var e); is admitted. to hand a callee two elements of one array chosen at run time, pass the array and the two indexes.
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, 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) carries no raises clause, as no asm body does. an inline function may be open (6.6) like any other — it relays what it calls — but the unnamed handler is a handler, and so stays barred from an inline body along with the rest.
an inline body has no out-of-line entry: it ends in a frame pop, and the copy falls through into the expansion site rather than returning, so there is no address a call could jump to and come back from. an inline function therefore may not be referenced other than by expansion. it may not be taken as a value, and it may not be called before its definition is read — neither through a prototype (10.3) nor, since a function name binds only after its body, from within its own body. an inline function cannot be recursive: direct self-recursion does not find the name at all, and reaching it through a forward declaration is rejected where the marker meets the earlier reference. the forms this bars are exactly the ones that would need a return the body does not have; a prototype whose definition is inline stays legal so long as nothing referenced it in between, which is how a module exports one (11.1).
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, and an inline marker written there is rejected rather than dropped — the prototype has no body to expand, and the definition below the separator carries it. 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".
the deferred definition of an open function therefore ends in two of them, `fn f() raises ... ...`, and they are read left to right without ambiguity: the first follows raises (or a `|`) and closes the clause (6.6), the second follows a complete signature and defers the body.
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.
an asm body cannot raise, and a signature with an asm body carries no raises clause. the compiler computes every raises clause from the body it compiled (9.9), and it has not compiled this one; nor could the assembly set the failure register honestly, since a failure number is assigned in declaration order and is no abi. an operation that can fail returns a status from the asm and is wrapped by an ordinary function that raises on it — the shape the firmware trampolines in aldos/machine take.
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), a variadic ... stands inside a parameter list (10.1), and the unnamed failure follows raises or a `|` within a clause (6.6, 9.9) — so a prototype ending in `raises ...` and the separator below it never run together. 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 } .
one list stands at the head of a module, before its first declaration; a module has no second import list, and no import appears in the body section. each string is a path to a source file without the .alm extension.
paths are relative. resolution tries the importing module's work directory first and the library root (/a on the boot volume) second, so a module-local sibling wins over a library module of the same name. a path beginning with / is rejected: modules load from the boot volume alone, so a drive prefix would say nothing.
the local name of the module is the basename of the path, and an import does not rename what it binds. an exported member t of module m is accessed as m.t. modules are distinguished by their resolved path, so util/log and core/log are two modules and both may be loaded — but one module cannot import both, since the second binding of the name log collides with the first; where both are wanted, an intermediary module re-exports what is needed under other names (11.1). 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 the modules that raise them (core/file, core/heap, core/bytes, core/socket, core/boolean, aldos/machine; appendix b.2), imported like any other.
the machine boundary is a single thin module: aldos/machine, whose functions are the rom firmware call table (video, keyboard, sound, disk, the byte-copy primitives), each a framed asm call through a slot of the table the firmware publishes at boot. the operating system above it is no table at all — aldos/volmgr, aldos/heapmgr, aldos/taskmgr, aldos/svcmgr and aldos/net are ordinary almac modules, imported and called by name like any other, and the core modules that need them (file, directory, heap, task, service, socket) import them directly.
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 and returns nothing, and is neither inline nor an alias; a definition of either name that departs from this is rejected. teardown raises nothing: it runs while a module is given up, where there is nothing left to fail. setup may raise, and a failure that leaves it fails the import, exactly as a compile error in the module would: the unit that imported it is rolled back, the modules loaded for it are torn down, and the module's own teardown does not run, since its setup never finished. this is what lets setup claim the storage its module variables are born from (7). a setup that leaves something outside the image before it raises undoes that itself.
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 no teardown: the image reclaims those wholesale.
setup has a second job: it is where a module variable holding a slice, a ^t or an address is born (7), and where a module final of any type is born (7.1). aldos/blockmgr binds its cache view there, over the fixed address the cache lives at, once and for good:
final cache []b8
fn setup () {
cache = view_at (cache_base as address), (capacity * page_bytes);
}
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" | "final" ) vardefs .
constdecl = "const" ( ident [ type ] | ident "." ident ) "=" constexpr .
constexpr = "true" | "false" | "none"
| integer | hex | fp | character | string
| qualident [ "." ident ]
| "fn" qualident
| arrayconst
| recordconst .
arrayconst = "[" constexpr { "," constexpr } [ "," ] "]" .
recordconst = "{" constexpr { "," constexpr } [ "," ] "}" .
typedecl = "type" ident ( "as" typename | "from" typename | structtype ) .
failuredecl = "failure" ident
( "as" qualident | "{" ident { "," ident } "}" ) .
vardef = ident { "," ident } type .
fndef = [ "inline" ] "fn" ident signature
( "as" qualident | "..." | { ( "var" | "final" ) 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 = [ "const" ] ( 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" ( "..." [ "with" ident ] | 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
| "fn" atom { index | field | deref }
| "(" 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 declares each common fault in the module whose operations raise it — no module exists only to hold a failure. a module that raises or handles one imports its owner; the directory module, for instance, imports core/file for file.fault.
-- core/file
failure fault { not_found, access_failed, already_exists, no_space }
-- core/boolean
failure fault { invalid_argument, contract_failed, out_of_range }
-- core/bytes
failure fault { invalid_format, buffer_overflow }
-- core/heap
failure fault { out_of_memory }
-- core/socket
failure fault { unavailable, in_use, no_sockets, transfer_failed }
-- aldos/machine
failure fault { illegal, access, misaligned, unknown }
-- core/spec
failure fault { unexpected }
file.fault names environment faults of the file system, shared by core/directory; boolean.fault a violated contract — a truth about the arguments that failed to hold, raised by any module whose caller broke its precondition; bytes.fault a malformed value or an overflowed buffer (raised by the byte, bytes, boolean, float, signed, text, and time codecs); heap.fault exhaustion of memory, also raised by a program whose own fixed pool runs out; socket.fault a datagram exchange gone wrong. machine.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 machine.from_trap, which maps the trapped mcause to a kind and raises it where machine.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 operating-system modules under aldos/ keep their own aldos.fault, declared in aldos/aldos: the kernel borrows nothing from core, so every aldos-operation error is one of its kinds, and the core modules above translate them back to the file.fault, heap.fault and socket.fault kinds their interfaces document.
the kinds that need a gloss:
the file and directory modules raise file.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 machine boundary is reached through one interface module, aldos/machine (11.3), and the kernel modules under aldos/ are imported by name like any other. 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 b8. 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 b8 (addr address) fn poke (addr address, v b8) fn format string (var b string, v b8) raises bytes.fault fn parse b8 (src string) raises bytes.fault
bytes — operations on byte buffers viewed as slices of b8. 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 b8 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.
read_le16/read_le32 and write_le16/write_le32 are the fixed-width little-endian scalars of a disk or wire format, at a byte offset in the buffer. the byte order is the format's and is spelled out, not the machine's — a load of the whole width would bake the host's endianness into the file. neither the offset nor the width is checked against the slice: the caller asserts the bytes are there, as it does for an ordinary index. the readers are unsigned, so a caller whose field is signed extends it itself; the writers take the low bits of v and are exact for a negative one, its two's complement being what lands in the buffer. b8 and b64 need no such pair — one byte has no order, and the whole word is a cast (8.2.5).
fn clear (var buffer string) fn fill (var buffer string, value b8) 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) fn read_le16 s64 (b string, off s64) fn read_le32 s64 (b string, off s64) fn write_le16 (var b string, off s64, v s64) fn write_le32 (var b string, off s64, v 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 heap.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 heap.fault
fn append (var self ty, data string) raises heap.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 heap.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 boolean.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 heap.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 heap.fault | boolean.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, both logically (the arithmetic shifts are signed's ashil/ashir); xor is bitwise exclusive-or; clz/ctz count leading and trailing zeros, each 64 for a zero operand, and ones counts the set bits. setting and clearing single bits and ranges is written in the language (8.2.3), not called for. every operation here is on b64; a b8 reaches them through as b64.
fn shil b64 (v b64, n s64) fn shir b64 (v b64, n s64) fn xor b64 (a b64, b b64) fn clz s64 (v b64) fn ctz s64 (v b64) fn ones s64 (v b64) fn format string (var b string, v b64) raises bytes.fault
signed — s64 arithmetic, conversion, and limits. lowest and highest are the s64 range bounds, written as hexadecimal 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 boolean.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). it also owns boolean.fault, the contract-violation failure (b.2).
failure fault { invalid_argument, contract_failed, out_of_range }
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 (aldos/heapmgr). 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 heap.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.
failure fault { out_of_memory }
fn claim address (size s64) raises heap.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 coin bool (var s state)
fn range s64 (var s state, lo s64, hi s64) raises boolean.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. file.fault (b.2) is declared here and shared by directory.
failure fault { not_found, access_failed, already_exists, no_space }
type id from s64
fn open id (path string) raises file.fault
fn create id (path string) raises file.fault
fn append id (path string) raises file.fault
fn replace id (path string) raises file.fault
fn seek (f id, offset s64) raises file.fault
fn read string (f id, var buffer string) raises file.fault
fn write string (f id, data string) raises file.fault
fn close (f id)
fn is_exist bool (path string)
fn size s64 (path string) raises file.fault
fn remove (path string) raises file.fault
fn copy (src string, dst string) raises file.fault
fn move (src string, dst string) raises file.fault
fn touch (path string) raises file.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]b8, -- name bytes, length of them valid
length s64,
is_dir bool
}
fn make (path string) raises file.fault
fn remove (path string) raises file.fault
fn open id (path string) raises file.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 file.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 b8 -- a 2-bit cell intensity (0..3)
type surface [4800]b8 -- a width x height canvas
type key record { -- a decoded keyboard event
code b8,
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 []b8, 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 boolean.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), b8
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.
what say and sayin write to is a sink — a function taking the bytes — and the console is the sink they start with, installed before anything importing tty can say a word. to puts another in its place, and output follows it until console is put back; there is no state saying which destination is in force, because the destination is the state. a program holding the display has the console grid hidden under its canvas, so it installs a sink of its own and presents what it collected; a sink over a file or a socket is the same one line. the line ending say writes is a newline byte in the text rather than a separate cursor move, which is what lets a sink be one function. a sink that cannot take what it is given drops it: say and sayin stay non-raising however they are routed. a call from a service still goes to the journal instead — that is the calling fiber's own destination, and a foreground sink has no claim on it.
type sink fn (s string) 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 () fn console (s string) fn to (destination sink)
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/recording; the mixer lets pcm and beeper play at once.)
fn play (samples string) fn begin (w []b8, n s64) fn ready bool () fn fill (w []b8, 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 heap.fault fn cancel (h handle)
mirror — reflection: the one module that reads compiler state, and the one not written in almac. fault_kind is the failure word an unnamed handler binds (9.9); it is a distinct type, so it never mixes with a number on its own, and its layout is deliberately not a language matter — failure numbers are assigned in import order and are no abi (b.2), so no program may compute a name from one. the three accessors do that instead, returning the interned names the compiler already holds: owner and name give the qualified `owner.failure` a diagnostic prints, kind the kind within it. all three are total — a word they cannot place yields the empty string rather than a failure, since a caller is typically inside a handler reporting one already. mirror needs no import and, being the whole meta surface, a program that never names it does no reflection at all.
run compiles a source string into the live image as a prompt line, runs it, and reifies what happened into the value union it is given. the variants are the outcomes a prompt already has: no value at all, a scalar, a real, a slice — and the two ways a run ends without one, the source raised or it never compiled. so a run is answered by a single when, and every outcome arrives as data: a failure becomes a raised holding the same word a handler would bind, a diagnostic becomes the rejected report, worded as the prompt words it. run is therefore total — it raises nothing, and it clears the failure it caught rather than leaving one standing that the caller never made. it answers rejected, not a compile, for a source given while a compile is already in flight, which a module setup hook is the way to reach.
each carrying variant also names the type of what it carries. a type_ref is an opaque handle on a compiler type, distinct for the same reason fault_kind is: it is an address, and no arithmetic on one means anything. type_name_of spells it — the name it was declared under, or its structure when it never had one — and is total in the same way, so a handle it cannot place is the empty string. what a slice variant carries is borrowed, not owned: it views storage the run unit left behind, and a report is valid until the next run, so anything to be kept is copied.
type fault_kind
type type_ref
type value union
nothing
| number { of type_ref, item s64 }
| real { of type_ref, item f64 }
| bytes { of type_ref, item []b8 }
| raised { f fault_kind }
| rejected { report string }
fn fault_owner_of string (f fault_kind)
fn fault_name_of string (f fault_kind)
fn fault_kind_of string (f fault_kind)
fn run (source string, into v value)
fn type_name_of string (t type_ref)
below the standard library sit the aldos modules. aldos/machine wraps the host boundary as thin asm trampolines over the rom firmware call table (video, keyboard, sound, disk, the byte-copy primitives behind bytes); the rest — volmgr behind file and directory, heapmgr behind heap, taskmgr behind task, svcmgr behind service, net behind socket — are ordinary almac modules. ordinary programs use the modules above rather than these directly.