specification

Normative. Both implementations are readings of it.

These documents are the language. The compiler and the reference interpreter share no code; what they share is this specification, the conformance corpus, and the protocol they are compared through. When the two disagree about a program, the specification is what gets read, and sometimes amended.

Every document carries clause anchors in brackets, spelled like [mem.region.create.3]. A diagnostic cites one, a trap cites one, a corpus program declares which ones it conforms to, and the anchor index is checked against the prose so a citation cannot point at a clause that was renamed. Files are served as plain text.

The documents

01. Surface Grammar (81 KiB)
Lexing, the mode stack that lets strings nest inside strings, the expression grammar and its precedence tiers, statement and item forms, and the ambiguity rulings. Status: normative, grammar/1, declared at v0.1.0 and held additive-only through the 0.1.x line; at v0.2.29 it is still grammar/1 — a removal or narrowing would be a grammar/2 act, and none has been taken. Every change to it since has widened it, which is the additive direction. v0.2.11 gave the pattern grammar a range form, [gram.pat.range]: lo..hi and lo..=hi over integer and char literals are patterns wherever a pattern goes, both ends literal because an open end is how a slice is spelled, overlap legal with the first arm winning, an arm nothing can reach E0802 and a range no value can match E0815. v0.2.13 added three forms at once — the list literal, [gram.expr.list], with position settling the clash against indexing so [10, 20, 30][1] needs no lookahead; the error alias item, [gram.item.error], with error staying contextual so let error = 1 still parses; and the nullary variant pattern, [gram.pat.nullary] — and then joined the contextual table. Every program that parsed before still parses. v0.2.15 widened it once more and wrote two refusals down without moving the line: FORMAT_SPEC is a production now — fill, align, sign, width, precision and type — where the grammar had carried a colon and a pointer to the types document, while [gram.expr.variant] and [gram.type.start] give anchors, and one of them a machine-applicable fix, to two inputs that were already refused. The accepted language is the same; the diagnostics moved. v0.2.17 widened it by one production: the right-hand side of a plain = whose place is a container element may be spelled take — xs[i] = take v, m[k] = take v — and nowhere else in an assignment may a mode appear ([gram.expr.assign]). It is push(take x)'s twin and means what that spelling means: the stored value moves, where the plain store now copies. v0.2.22 closed the attribute set without a production moving: [gram.item.attr.set] lists the attributes a pass reads, and one wolf does not know, or knows and does not implement yet, is E0817 by name where it used to compile and mean nothing; [gram.item.attr.cfg] evaluates #[cfg(target = "…")] against the build's target, which was ignored, so target-gated code had compiled for every target. v0.2.23 widened the grammar by one production, extern_let_item: extern "c" let NAME: *T names a symbol the link defines, with no initializer, and the parser reads a var, a const or an initializer there only so the checker can refuse each by name (E0821). The attribute table gains #[repr(c, packed)] and #[repr(c, align(N))], and a representation that cannot be laid out as written is E0820. At v0.2.29 the document and its EBNF are v0.2.23's, byte for byte: every release since wrote its clauses into the memory, concurrency, ABI, conformance, package, types and OS documents, and the spellings v0.2.26 added — -> never, ! on an integer and copy region { … } — parse under productions the grammar already had. Anchors [gram.*].
02. Memory Model (131 KiB)
Regions, ownership tiers, moves, freezing, the shared and handle edges, provenance, and the closed list of rows that are undefined behavior with the optimization each one licenses. The interpreter implements this document as a dynamic machine; the compiler implements a static approximation of it. v0.2.11 added [mem.str.imm]: a str never changes after it is built and a slice of one is a view, so there is no place on the left of s[i..j] = … — a rule rather than a thing the backend has not got to, and E0416 says so. v0.2.14 added [mem.region.proc]: a spawned proc owns its ambient region, so bytes built inside it cannot be handed out of it — E1010 — and the fix the diagnostic prescribes is to build them in the spawner and pass them in. v0.2.15 was the first release that changed what a program you already wrote means, and this is the document that says why. A builtin container store copies its element, at [mem.tier0.move.3]'s cost: keeping the caller's handle alive inside the container was [mem.tier0.excl.1] violated with a builtin as the callee — two writable paths to one value, nothing spelled, nothing reported. push(take x) is the spelling that hands the value over instead, and a store is the one read position where a call site may name a mode at all; the same clause ([mem.tier0.mode.read]) rules that a move out of a read parameter is lent and may not outlive the call. Nothing warns, because there is nothing wrong with the new answer. v0.2.16 was the largest change this document had taken since, and the part of it a reader meets is a correction rather than an addition: move out of a place whose type is copyable had recorded no move at all, so the place stayed readable and printed the value that had been moved away, on every tier. The document is explicit now that the operator empties the place whatever the type, and E1001 answers a read of it afterwards. Two clauses arrive beside it — a region's root ([mem.region.root]) and what an immutable value returned out of a region means ([mem.region.imm.ret]). v0.2.17 carried the store's half of v0.2.15's rule to the index: xs[i] = v and m[k] = v copy a non-Copy value in and leave v live, exactly as push does, and take is the spelling that hands it over ([mem.region.edge.elem]). Beside it, [mem.model.place.rhs] wrote down the order every machine already kept: a store evaluates its right-hand side first and mints its place afterwards, so a right-hand side that grows the list it stores into cannot leave the store aimed at the old buffer. The order of the place's own operands against the right-hand side was not ruled by it, and the machines were not agreed there. v0.2.18 ruled it and answered two questions the element had left open. The place's operands come first, left to right and outermost first, then the right-hand side, then the address, then the store — so xs[idx()] = val() calls idx first on every machine, the checked one included, which had called val first. [mem.model.place.elem] says which elements of a container are distinct places: two integer-literal indices with different values, two literal Map keys, an element against its container's len; everything else is one place, and a store revives a moved element only through an index that surely denotes it, a key local of any copyable type unwritten since the move included. And [mem.tier0.mode.mut] gains the sentence two silent wrong answers were missing: a mut parameter is initialized at every return of the function, so moving out of one and returning without storing a value back is E1001. v0.2.19 carried the distinct places from moves to claims. Exclusivity, borrows and iteration read [mem.model.place.elem] per element now, so add2(mut xs[0], mut xs[1]), add2(mut g[0][1], mut g[1][0]) and bump(mut xs[0], xs.len) hold two claims that cannot meet, and every shape the clause calls one place — a run-time index, the same literal twice, a path and its prefix — is still refused. And the clause names what reads a container's header: len, count and is_empty, and nothing else, so each is allowed beside a moved or claimed element, while every other method and a slice read the whole container. v0.2.20 made arguments two-phase, [mem.tier0.excl.4]: within one call a later argument may read what an earlier mut argument claims and may not write, move, re-claim or lend it, so grow(mut xs, xs.len) runs and a write to the claimed place inside a later argument is E1002. The same release let one call's two claims reach two elements through an offset or a loop index the clause proves distinct. v0.2.21 wrote down when an errdefer runs: when its scope is left on the error path, and a block whose own value is an error has left that way, exactly as a function has. v0.2.22 made a for binding carry its iterable's sites, as xs[i] does, so a piece of a region-built str held past the region by a loop is E1010 — the compiled tiers had printed it from freed bytes — and copy s of a region-built str, which shares its bytes, is held to the same rule. And [mem.unsafe.sig] says where a *T may stand: in a module-private signature, whose every caller is inside the audit granule, and on either side of the C membrane, never in a pub one. v0.2.23 converted integers and pointers both ways ([mem.prov.expose]), prefix *p is p[0] by another name, and on the freestanding target an address no allocation owns is the platform's ([mem.prov.device]), where a hosted target keeps it undefined. [mem.unsafe.volatile] defines p.read_volatile() and p.write_volatile(v): one aligned access of the pointee's width, never elided, split, merged or reordered against another, on a pointee that is one fixed-width integer or byte, anything else E1307. And [mem.static] gives modules state: every initializer is evaluated at compile time into the image, one that reads a var or needs run-time code is E0705, and a var is raw-tier, E1301 outside unsafe. v0.2.24 made alignment part of a raw access, [mem.unsafe.raw.4]: an ordinary access through a *T at an address that is not a multiple of T's alignment is undefined, row L4, which licenses the compiled tiers to emit every raw access aligned, with no check and no split, and a packed struct's field is not that row, the struct's alignment being one byte. v0.2.25's [mem.static.2] says what a call does to that memory on every machine: a call may write any module state and any storage a raw pointer or an extern "c" let reaches, so no value read or stored before a call stands for that memory after it, which native and release had broken. At v0.2.26 [mem.region.copyout] gives a region block one way out: copy region { … } deep-copies the block's value into the region it was entered from and then frees its own, and every other escape is still E1010. [mem.region.escape] makes a call to a function returning a str an allocation site where it is made, which closes a read of freed bytes on the checked, native and release tiers. [mem.static.4] reads another module's pub item as m.K, a var so named raw-tier; [mem.unsafe.raw.5] stores one field of a raw element at its C offset, touching no other byte; and [mem.list.bytes] gives a List[byte] the find and count a str has. At v0.2.28 [mem.region.escape] came to read a call's signature for what its callee can keep: a mut argument, the receiver included, or a non-Copy result shares the bytes of every other argument that can reach it, so a string built in a region block and handed to a callee that may keep it in an outer list is E1010 at the call, where only the direct store had been refused; a copy of an aggregate shares its strings and a Map keeps the key it is written through, so each of those is held to the same rule. The callee's body is never read, so some correct calls are refused too, and the clause names the edit that is sound at the call. A net_read result is an allocation site where it is read. Anchors [mem.*].
03. Concurrency (53 KiB)
Structured tasks, supervised procs, channels with region transfer, select, the memory model's happens-before rules, and the deterministic schedule a seed selects. Written in the shape of the Go memory model document, with the advice in front of the formalism. At v0.2.11 [conc.chan.close] finally typed what it had described for two: send is () ! {closed, cancelled}, the same two tags the receive side carries, because a send blocked on a full buffer is a cancellation point too. v0.2.15 added the first combinator that is a scope: [conc.task.par] defines xs.par(f) by desugaring to a scope of spawns, so it adds shape and no semantics. Results come back in input order whatever the schedule ([conc.task.par.order]), the tasks join inside the call, a write to captured enclosing state is E1101 at the write with the par named as the spawn site ([conc.task.par.capture]), and the index range is cut into contiguous chunks — never a task per element ([conc.task.par.chunk]). The cost clause, [conc.task.par.cost], promises no speedup and says so in the document: a list shorter than the worker count, or an f cheaper than a spawn, can make it slower than map, and both are conforming. v0.2.16 gave the two handles names a signature can write. [conc.task.scope] has said since the first edition that a function which spawns into its caller's scope takes the handle as a parameter, and until v0.2.16 there was no way to spell that parameter at all — the capitalised names were unknown and the lowercase keywords were refused in type position. Scope and Proc[T] elaborate in a signature now, the way a channel does, and the handle carries the completion type rather than standing bare. Beside it a proc talks back: [conc.proc.join] blocks until the proc exits and answers the value it produced, or the tag for the class it failed in, where before a supervisor could read only the class and the output. [conc.proc.handle] and [conc.proc.arg] are the clauses under it. The playground has the other half of that story: the corpus program that writes Proc in a signature was the one the tab refused at v0.2.16's pin, and the interpreter it runs has known the name since. v0.2.24 grew it more than any other document, with atomics on raw pointers: [conc.mm.atomic.raw] defines a load, a store, a swap, the read-modify-writes add, sub, and, or and xor, and a compare-and-swap on a *T whose pointee is a fixed-width integer, inside unsafe, each with an order written at the call ([conc.mm.atomic.order]), and [conc.mm.fence] defines fence(o), of which fence(Order.seq_cst) is the one a safe program may spell. A pointee that is not an integer is E1308, an order the operation does not admit is E1309, and the checked machine runs every order as seq_cst on its one task ([conc.mm.atomic.raw.5]). Since v0.2.28 the checked machine runs the concurrency tier it had refused by name — scope, spawn, procs, select, when, closures and xs.par(f) — one task at a time under the deterministic scheduler a seed selects ([exec.checked.task], [exec.checked.sched], [exec.checked.race]). And the call rule [mem.region.escape] states carries to a channel: a callee handed a channel that can carry bytes, and every spawn proc argument, must outlive the frame, as a sent payload must ([conc.chan.payload], [conc.proc.arg]). At v0.2.29 [conc.task.order] restates what a parked task costs the host. Compensation has no cap of its own: the native runtime holds a thread for every task parked in a wait, so a program that parks a task for each connection holds as many as the host lets it start, and a thread the host refuses is reported once on stderr. A parked task costs its thread and no cpu, a wait in the reactor, a channel operation or a when sleeping until its completion, its deadline or a cancellation with no system call meanwhile; os_signal_wait is the wait that still polls, and the clause says how often. Anchors [conc.*].
04. ABI (38 KiB)
Layout, calling convention, and the foreign boundary, differentially fuzzed against platform C compilers. It is the document v0.2.22 grew most, and v0.2.23 grew it most again. v0.2.22's [abi.target] and [abi.target.none] define the freestanding target: an object with no main shim and no runtime, one trap hook the program supplies, the code generation a kernel needs, and everything that needs the hosted runtime or an allocator refused by name. [abi.asm] covers the assembly a manifest lists and the roster a call into it must name. [abi.c.export] and [abi.c.import] are the two directions across the C membrane — an export fn is a C function, and a hand-declared extern "c" fn is callable inside unsafe — and [abi.layout.c] holds a #[repr(c)] struct to C's layout through a raw pointer, where the compiled tiers had packed it. At v0.2.23 [abi.layout.packed] and [abi.layout.align] lay out #[repr(c, packed)] and #[repr(c, align(N))] as C does, and [abi.layout.query] answers size_of, align_of and offset_of at compile time; a packed field lent is E0819. [abi.link] places code and data with #[section(".name")], names a symbol the link defines with extern "c" let, and leaves the image to the consuming build's linker script. [abi.interrupt] says how an interrupt reaches wolf, which has no interrupt calling convention: an assembly trampoline saves the registers and calls an export fn with the frame. v0.2.24's [abi.target.none.alloc] lifts the freestanding target's refusal of allocation: List, Map, interpolation, a capturing closure and region compile against libwolf_rt_none.a, the region runtime built without the standard library and shipped beside wolf, over a wolf_alloc and wolf_free the program supplies, while Pool, freeze, channels, spawn and par stay refused by name. v0.2.27's [abi.target.none.ambient] gives each of a kernel's threads its own ambient region. Anchors [abi.*].
05. Conformance (34 KiB)
What makes the rest of it testable: clause anchors, test tagging, the closed trap vocabulary, and coverage reporting. The interpreter reads only the specification and the corpus, and this document is part of that sealed interface. Its own register of anchor namespaces has been wrong in both directions, and is checked in both now. At v0.2.6 it named seven while the documents were publishing anchors in eleven, and the four unlisted ones were appended. At v0.2.7 the mirror closed: 07-schedule-points.md had been declaring seven [sched.*] anchors that no register carried, so no extractor read the document and no gate anywhere held an opinion, while the runtime cited four of the seven from live scheduler code. sched is registered now and its anchors are published, 424 to 431. Neither change renumbered anything, because a published anchor is never moved and the citations that would have had to move number in the thousands. At v0.2.16 it ruled the exit status, which had been left to habit for every class but the trap: [conf.exit] makes a rejection 2 and a refusal 4 on every implementation, where the compiler had spelled both 1 — and 1 is what a program that ran and returned an error out of main exits with, so three outcomes had been collapsed onto one number. The trap keeps its per-implementation numbers, because those are forced by a platform convention and these are forced by nothing. The status is not injective and the clause says so: the guarantee is that a rejection or a refusal never exits 0 or 1, and a harness that needs the exact answer reads the record. [conf.exit.class], [conf.exit.refused], [conf.exit.static], [conf.exit.collide] and [conf.exit.test] are the clauses under it. Anchors [conf.*].
06. Differential Protocol (18 KiB)
The two-implementation contract, and the only shared artifact between the tracks. It defines the observation record: the phase reached, the verdict, the diagnostics as code and span with no message, and the output digest. Diagnostic wording is deliberately excluded, because wording is a per-implementation concern and comparing it would manufacture disagreements. The playground can show you a record for any program you run. At v0.2.16 the record gained a trap's own words and where it happened ([proto.record.trap]), and a passing comparison gets a shape of its own ([proto.record.pass], [proto.cmp.pass]). The message is still never compared, because wording is a per-implementation concern; it is carried so a reader of the record can see what the machine said. v0.2.17 let a diagnostic say which file it is in ([proto.record.diag]): a span from a sibling module used to read as an offset into the entry file, a wrong place given silently, and the record carries a files table and a file index on each diagnostic — only when some diagnostic lies outside the entry file, so every other record is byte-identical to what it was. v0.2.22 made a refusal carry one code on every lane ([proto.record.first]): the diagnostic at the earliest byte offset is first, and at one offset the earlier phase, where the record and the screen had been able to name different codes for one file. Anchors [proto.*].
07. Schedule Points (11 KiB)
Where a schedule can make a decision, and the numbering a replayable counterexample uses. Normative for shape and vocabulary, and append-only from here. Its [sched.*] anchors are published in the index as of v0.2.7; before that they were declared here and carried by no register, so a program citing one was rejected for citing a clause that existed. Anchors [sched.*].
08. Packages (7.5 KiB)
The package layer's formats, under one covenant: reading the dependency graph never executes anything — no build scripts, no hooks, no fallbacks. The manifest is data, resolution is minimal-version, and the registry entry's shape is fixed before any hosted registry exists to serve it. Since v0.2.26 a package's capabilities are what its code reaches: a host builtin named in its own files charges the capability its family carries, as an import of std.fs always did, a std module charges its importer for what its own code reaches, and E1504 refuses whichever one the manifest does not declare; wolf audit derives the same set from source, with a reason per capability. v0.2.27 counted a C declaration among the reaches: a bodyless extern "c" fn or an extern "c" let carries ffi where it is declared, called or not, as import c carries it at its line. Anchors [pkg.*].
09. Constant-Time (8.9 KiB)
#[consttime] as a verified contract: secret-tainted data cannot decide a branch, index memory, pick a call target, or feed a variable-time instruction, refused fail-closed on every lane in every build mode. Anchors [ct.*].
10. Types (97 KiB)
The numeric-literal typing rules written down in one place: how a bare literal takes its type, int adopting float in mixed arithmetic, and the casts that trap instead of wrapping. It is also where byte is declared — the 8-bit unsigned octet the toolchain gained at v0.2.4, with the clauses for its cast in both directions, the widening its operators do before they compute, and what an interpolation hole prints. It is the document v0.2.13 grew most: [type.list.lit] types a list literal from its elements or from the type expected of it, [type.range] makes range[int] and range[char] types in every position with end always exclusive, and [type.err.alias] makes an alias the same type as the row it names, both ways, a cycle being E0610. v0.2.11's clauses are still here: [type.unit] writes down what a unit context is — a loop body, an else-less if, a unit function — and rules that a !() tail in one is a warned discard rather than a mismatch, with the counter-case for a task closure whose scope consumes the row. [type.fn.ret] checks a body's tail against the declared result and [type.row.operand] refuses a bare row as an operand; both were already enforced and neither was written. It is the document v0.2.15 grew most, as it was v0.2.13's: a method call is defined as the function it is ([type.method] — xs.map(f) IS list.map(xs, f), resolved in the receiver's home module), the combinator set is closed at ten functions plus one builtin ([type.comb.set], [type.comb.builtin]) with what each one copies written down ([type.comb], [type.comb.eager]), take is the ownership verb and never a method name ([type.method.take]), a type parameter binds once ([type.generic.bind]), and the interpolation format specifier gets the clause the grammar's new production points at ([type.interp.spec]). v0.2.16 gave it one clause, [type.err.alias.qualified]: an error alias named through the module that declares it is the same type as the row it names, exactly as the unqualified spelling is. v0.2.20 wrote [type.row.else]: an else handles its scrutinee's own row and nothing a nested call raised. v0.2.21 gave the row its two remaining clauses. [type.row.match] splits a match over a fallible value into two halves — an arm naming a tag of the row handles the error, any other pattern handles the value, both must be covered, and a tag that is also a variant of the value's type is E0816 — and [type.row.defer] refuses a ? inside a deferred expression, E0611. And [type.numlit.value] settles a literal written at a binding with no annotation: it takes i32 there, and a later use no longer widens it. v0.2.22's [type.numlit.cast.narrow] makes an integer cast keep its value or trap overflow on every machine. A change of sign is a narrowing cast — native and release had reinterpreted the bits, so -1 as u64 printed -1 — and truncation is spelled v as wrapping[T] as T. At v0.2.23 [type.unit.context]'s list of unit contexts has no exception: an else-less if that is itself the tail of a fallible function discards its branch's raise, warned, where every machine had handed it to the caller with no warning. At v0.2.26 [type.fn.never] named the bottom type: a function declared -> never never returns, a call to it fits wherever a value is wanted, and a body that can reach its end or holds a return is E0401; and [type.int.not] makes ! on an integer its bitwise complement at the operand's own width, -n - 1, total, where it was E0409. v0.2.27's [type.wrap.div] makes / and % on a wrapping[T] divide the values the operands hold, so wrapping[u64]'s (2^64 - 1) / 10 is 1844674407370955161, and an unsigned or wrapping integer prints as the value its type holds; the compiled tiers had printed a wrapping[u64] as signed and refused or trapped its division. Anchors [type.*].
11. OS surface (157 KiB)
The operating-system surface the builtin tier exposes. v1 covers signal reception and OS randomness — the latter with no fallback and no seeding, so failure traps honestly. The socket section opened at v0.2.4 with unix-domain streams in the TCP pair's shape and the error rows that tell an unserved host apart from a bad path; at v0.2.5 it gained what a serving loop needs — one wait over a whole set of handles ([os.net.wait]), a listener several processes may hold at once ([os.net.listen.opts]), descriptors handed down across a spawn by position ([os.proc.inherit]), and the count of cores this process may be scheduled on ([os.cpus]). At v0.2.6 it stated what those hands actually see when a connection arrives ([os.net.accept]): a wake is not a connection, so the hand that loses the race carries no answer and waits again against the budget its deadline armed, in the runtime's wait rather than the kernel's. At v0.2.8 it stated when the syscall happens ([os.net.io]: a parking call tries the syscall before it waits, and waits only when the kernel says it would block), and gained three calls that name what a serving loop was doing by hand — the Nagle switch every stream is handed with ([os.net.nodelay]), the gathered write ([os.net.writev]), and one metadata read on an open handle ([os.fs.fstat]) in place of a path stat per fact. The first of those moved a cost and no row; the other three were new surface, and every one of them serves every tier-1 host. v0.2.14 added one call beside the gathered write, [os.net.writev.head]: a str head handed to writev with no List[byte] built for it. At v0.2.15 the document stopped being about sockets alone: it gains the host a program is running on and the signals that host can deliver ([os.host], [os.host.sigs]), the path of the running executable ([os.proc.exe]), what a spawn does with the streams it is not handed ([os.proc.spawn], beside [os.proc.inherit]'s existing rule), and the two filesystem calls a program writing files needs before it needs anything else — path composition and removal ([os.fs.path], [os.fs.remove]). v0.2.17 ruled which paths those calls take ([os.fs.path.domain]): every path the host lets the process reach — absolute, relative, climbing, built at run time — with what the host refuses coming back as the host's own row. An implementation that serves the tier over a narrower set declines the rest by name, never by a row, and judges the boundary on the resolved path, not its spelling. The compiler already served the whole domain; lupin 0.1.40 was the first release of the interpreter to serve, at a terminal, a path that climbs out and back in. At v0.2.22 a program could name an offset for the first time: [os.fs.seek], [os.fs.tell] and [os.fs.read_at] move, report and bypass a handle's cursor, with a new tag, unseekable, for a pipe, a socket or a terminal, because a program falls back to reading forward there. And [os.fs.std] makes the process's standard streams the handles C gives them, so the first handle an open answers is no longer the same number as standard input. At v0.2.26 those streams carried bytes, the byte and text calls serving 0, 1 and 2 in program order with print; a child's descriptors are the program's to set ([os.proc.fds], with the two ends [os.proc.pipe] answers); the working directory moves ([os.fs.chdir]); a descriptor says whether it is a terminal ([os.fs.isatty]); one call copies between two handles the fastest way the host offers ([os.fs.copy]); and the host's error number travels beside the io row, never on it ([os.fs.error]). v0.2.27 let a program survive a signal and own a terminal — a meaning's disposition ([os.signal.disp]), a queue it can poll without waiting ([os.signal.poll]), a job spawned into its own process group and how a child ended ([os.proc.job], [os.proc.status]), and the terminal's foreground group and mode ([os.term], [os.term.mode]); become another program and remove a variable ([os.proc.exec], [os.env.unset]); read the full stat record, a link and a typed listing ([os.fs.stat], [os.fs.readlink], [os.fs.readdir]); and it ruled a repeated json name last-wins ([os.json.dup]). Since v0.2.28 a program changes a file as well as reads one: its mode, a creation's permission bits, a symbolic or a hard link, its times, its length and the process's umask ([os.fs.mode], [os.fs.create.mode], [os.fs.symlink], [os.fs.link], [os.fs.times], [os.fs.truncate], [os.fs.umask]), each judging its arguments first, then answering unsupported by name where the host has no such operation. At v0.2.29 the signal set holds every signal POSIX names ([os.signal.set]): the meanings a program may listen for, ignore, wait for and poll, and the ones it may only send, KILL and STOP among them, and every call that takes a set answers io for an empty one or for a bit that is not a listenable meaning. A published meaning keeps its bit in every later release ([os.signal.set.stable]). A program reads the meanings ignored now, at start the ones it was started ignoring ([os.signal.ignored]), signals a process by its id ([os.signal.send]), asks the host's number for a meaning ([os.signal.number]) and names its own id and its parent's ([os.proc.self]), and a poll lets pending signals be delivered before it answers, so it never trails one ([os.signal.poll]). Each of the new calls is hosted: the freestanding target refuses it by name. Anchors [os.*].

Extracted artifacts

grammar.ebnf (11 KiB)
The grammar, extracted from document 01 by a command and checked in CI to be in sync with the prose. The book's grammar appendix and the editor highlighting both come from this file, so there is one lineage rather than three transcriptions.
anchors.json (26 KiB)
The clause-anchor index: every stable anchor the documents define. Both implementations validate their citations against it.
README.md
The directory's own note on which sprint authored what.

Reading it against a program

The conformance corpus is the specification's working end. Each program states its own expected outcome in a header directive and lists the clauses it conforms to, and CI in the compiler repository checks the claim on both implementations. The playground loads thirty-odd of those programs with their headers intact, which is the shortest path from a clause to a program that exercises it.