Axiom 0.3.7
0.3.7 — 2026-08-29
The enterprise readiness plan's two remaining buildable items land — the
compile-time ceiling it called "a bug, not an architecture", and the
Fallible effect it said costs nothing — and check-name-scale.sh now
builds an ablated twin of the compiler to prove its new arm can fail,
which makes it the thirty-eighth gate that builds the compiler under
test through gate_build_axc: thirty-eight gates, up from thirty-seven.
Beside them, two more diagnostics carry a machine-applicable fix and
the language server gains three assists of its own - the code-action
surface an editor user reaches for most.
Added
-
stdlib/Fallible.ax— the batch loop's effect. The plan's
fault-containment section said aFallibleeffect whose operation
answers "skip / use this default" costs nothing today and needs no
compiler change; nothing in the tree spelled it. Now: one effect, one
operation,(fallibleMalformed message), performed by the callee that
finds a malformed record and answered — tail-resumptive, no unwinding
— by whichever handler the loop installed:fallibleSkip(answers the
sentinelfallibleSkipped, whichfallibleIsSkippedreads),
(fallibleDefault d), or(fallibleCounting tally next)around
either, withFallibleTally/fallibleTally/fallibleCount. An
unhandled operation is still exit 71 (ERR-REC-6class ii).
docs/error-model.mdERR-REC-7;tests/stdlib/410-fallible.axpins
both handlers, counting, nesting, a logging handler, the trap inside
and outside a recovery point, and four memory terms.
examples/batch-fallible/reads N generated records, every k-th
malformed, under both handlers, andcheck-steady-state.shgains a
batchprobe: 2,000,000 records underfallibleSkiphold 1,376 KiB,
the same as 200,000, with akeepingtwin required to grow past 5×
(measured 25×).The shape was chosen by measurement, not by the plan's sketch. With
the arena mark cell over 10,000 records: a one-argument operation with
a literal message costs 0 bytes per operation; the two-argument
spelling(op message fallback)costs 32 — the inner closure of a
curried handler, never released; a message built per record costs 80
— a string a type-variable handler parameter hides from the release
walk. A batch loop has no arena reset (MM-ALLOC-22), so both would
have grown the process by every record. Both are compiler facts the
module documents rather than defects it fixes. -
Two more diagnostics carry a machine-applicable fix, and
IO.todo.
AX3042(a function performs IO and does not say so) now carries
;@axiom:effect(io)as a line of its own at the start of the
declaration's line — right below a::signature, since a tag
attaches to the next declaration, and inside an indentedimpl
member; offered for the entry file only, and only where the name is
written as a declaration, so a macro-generated function keeps the
prose.AX3005(non-exhaustive match) now carries the missing arms,
one per constructor in declaration order,((Ctor _ ...) (todo "Ctor"))with one_per field, inserted before the match's closing
)at the first arm's column, with a second fix bringingtodointo
scope — appended to an existing(import IO (...))list, else a new
import line. A nested hole is refused rather than guessed at.
IO.todois(-> String a):todo: <what>on standard error and
exit 70 — written withwriteStrandexitrather thandie,
becausedie's effect row is an upper bound the checker will not
accuse a caller on, and atodorouted through it left an untagged
function holding the arm checking clean.tests/diagnostics/363,
364,366,367pin the~>fields; applying the compiler's own
fixes to364in rounds gives AX3005, then AX3042 on the functions
the arms made effectful, then a clean file. In the editor these
arrive as quickfixes with no server change:lspQuickfixesOf
surfaces every help that carries a span. -
Three code actions the compiler does not write. The language
server'stextDocument/codeActionanswered only what the checker
carried — a help with a fix span — and one assist from what it
inferred. Three more, each in SECTION FIX oflsp.ax, each refused
where its condition does not hold: ImportnamefromMod
(quickfix on AX3001 with a bare name; every directory the resolver's
ownmoduleSearchDirsanswers is searched, a file counted only when
moduleSrcPathresolves that name to it — the ladder's shadowing
rule asked rather than restated — and the edit adds the name to an
existing import list, else a new import line; 0.014 s for a stdlib
search); Makenamepublic inMod(quickfix on AX3023; a
WorkspaceEdit keyed by the DECLARING file's URI insertingpubon
thefnand its::, becausecheckrefuses either alone —
measured); Extract tolet(refactor.extracton a range covering
exactly one item in a fn body, hoisted above the innermost block
statement on the path, refused under a lambda, loop, branch or arm
and whenever the item references a binder bound inside that
statement, by NAV's occurrence walk).codeActionKindsgains
refactor.extract; drive.py applies each edit and requirescheck
clean or unchanged behaviour.
Changed
- The name map answers from an index, and doubling a module costs
1.9× rather than 3.9×.mangleHasInwas a linear scan of every
bare name asked once per declaration being mangled — the quadratic
check-name-scale.sh's header recorded at "55.7% of a check at
N=8000". Measured on the tree before the change, best of three: a
module of 8,000 declarations checked in 2.12 s (private helpers) /
3.26 s (public), doubling to 16,000 cost 3.25× / 3.58×. It was not
55.7% of a check; it was ~93%. AMangleIdx— anInternover the
bare names plus a position vector, fed by the five writers and
threaded besidebaresthrough the resolver — answers in 0.23 s /
0.22 s at 8,000, doubling at 1.91× / 1.93×; speedup 9.4× / 15×. Every
public signature keeps its shape.check-name-scale.shgains the
plan's N→2N arm (bound 2.80, floor N=8,000) and builds a twin with the
scan put back, which must fail the arm (measured 3.24× / 3.41×) — and
the same script run inside a pristinegit archiveof the previous
release fails it at 3.25× / 3.58×.