Skip to content

Axiom 0.6.0

Choose a tag to compare

@github-actions github-actions released this 31 Aug 11:26
· 299 commits to trunk since this release

0.6.0 — 2026-08-31

  • KNOWN ISSUE: module resolution matches case-insensitively on macOS, so
    a project shadows a standard-library module it did not mean to.

    Shadowing itself is intended and documented — the resolution ladder
    puts the entry file's own directory first, and a project is meant to
    be able to supply its own Str. What is not intended is that on a
    case-insensitive filesystem the FILESYSTEM decides the match: a file
    named str.ax or STR.ax satisfies a lookup for Str, so the same
    tree resolves differently on macOS and Linux. Measured 2026-08-31:
    with str.ax beside the entry file, (import IO) fails with
    AX3001 undefined variable strLen on macOS and builds on a
    case-sensitive filesystem.

    The second half is the diagnostic. Nothing in that error mentions
    that Str resolved to a local file, so the message a user gets for a
    shadowed module is an undefined name from inside the standard
    library — accurate and unhelpful. A resolution that silently picks a
    different file than the reader expects should say which file it
    picked.

    Not fixed in 0.6.0, deliberately: it is pre-existing, and changing
    module resolution semantics under release pressure is how a worse bug
    ships. Recorded here so someone who hits it recognises it rather than
    debugging their own code.

  • The seed lineage is verified end to end on every push, and the thing
    that makes that cheap cannot be used to hide a broken link.

    scripts/check-seed-lineage.sh --full replays every row of
    bootstrap/CHAIN from the Rust anchor bb730db, and its cost is
    linear in the number of reseeds this project has ever done - 10m40s
    over fourteen rows, measured on darwin-aarch64 - so it ran nightly and
    a default run replayed the newest row and said nothing at all about
    the thirteen before it. bootstrap/CHAIN.checkpoint is the record
    that run was missing: it names a PREFIX of the table and the sha256 of
    exactly that prefix - the rows and orphan lines verbatim, every short
    hash resolved to a full commit, the git object id of every seed those
    commits carry, the sha256 of every walk list and patch file they name,
    and the anchor - and the gate RECOMPUTES that digest from
    bootstrap/CHAIN on every run before it skips a single row. 0.7s to
    recompute; a default run costs 38s against the 36s it cost when it
    checked nothing about the prefix. A covered row that moved by one byte
    digests differently, the checkpoint is void, and the gate replays
    every row from the anchor and goes red: editing an old row cannot
    shrink the work, only enlarge it. Five probes assert exactly that on
    every invocation, over a synthetic checkpoint the gate builds from the
    table in front of it - the passing direction included, so a verifier
    that refuses everything cannot satisfy the four refusals. The
    checkpoint is advanced only by AXIOM_BLESS=1 scripts/check-seed-lineage.sh --full, over rows that same process
    replayed from the anchor, and never as a side effect of a passing run;
    it never covers the newest row, so the link a push adds is replayed on
    that push. It is a record, not a signature - whoever can edit a row can
    recompute the digest, and what the file buys is that they must do it in
    the same reviewable diff while the nightly --full re-derives
    everything from bb730db regardless. AXIOM_LINEAGE_FULL=1 is
    --full for a caller that cannot pass an argument.

  • Traits and impl are gone from the language. An interface is a
    CAPABILITY RECORD now — a parameterised struct holding the functions,
    bound with fn and passed as a value — so dispatch is application and
    there is no resolution rule to learn. trait and impl are reserved
    and draw AX2004 with the migration in the message. stdlib/Show.ax's
    trait and its four impl blocks are deleted: showResolve in the
    checker renders String, Bool, Int, Float, Char and every
    data/struct from the argument's static type, and always did.
    deriveEq/deriveShow are unaffected — they generate plain functions
    and always did. What is actually lost, stated rather than left to
    be discovered: a user could override the built-in rendering for their
    own type with (impl (Show Color) ...), and there is no override hook
    now; deriveShow still generates showColor to call deliberately.
    compat/BREAKING declares it. Nineteen fixtures went with the
    construct, but THREE were pinning properties that outlive it and were
    rewritten rather than deleted: 010-trait-scope and
    020-trait-duplicate asked whether a global value-namespace occupant
    captures a spelling a let or parameter should shadow, and now ask it
    of an effect OPERATION, which is the occupant a program can still
    declare (isBuiltinName, isMacroName and isEffectName are the
    others the resolver asks about). 373-shared-default-binder could not
    be rewritten, and why is the substantive result — see the next entry.

  • A guard whose only reachable path was traits, and the check that
    asserted it becoming a check that cannot fail.

    stampPatBinderTy keeps a three-state stamp whose third state is "two
    checks disagreed", because a binder node could be checked twice at two
    types and last-write-wins would hand codegen an Int for a binder that
    is really a String — a release of a block the binder still points
    into. The only path was a trait DEFAULT body, which checkImplComplete
    synthesized into every impl without copying the nodes. Measured
    2026-08-31 by building the last-write-wins compiler and diffing emitted
    IR: 278 fixtures, every stdlib/ module and self_host/main.ax itself
    are BYTE-IDENTICAL. So scripts/check-fallible-reclaim.sh's second
    half had quietly become a check that cannot fail — this repository's
    most-refused defect. It is INVERTED rather than deleted: it now proves
    nothing in the tree reaches the arm, with the whole compiler as the
    subject, and goes red the day a future construct re-checks a body per
    instantiation. The arm stays, because it guards a class of mistake
    rather than one construct, and it now has a live check on it.

  • Restriction profiles had zero production use, and the gate only
    worked because of it.
    no-io, no-alloc, no-cast, no-recursion
    and no-foreign shipped in Ada round 1, are enforced, and appeared
    only in tests/diagnostics/. Tagging one function turned
    scripts/check-restrictions.sh red twice for reasons unrelated to that
    function: section 1 stripped #restrict= from only the tagged side of
    its diff, and section 5's manifest credited a fixture with any tag it
    could see through an import. One root cause — both assumed no
    #restrict= existed outside the gate's own fixtures, true only while
    adoption was zero. Both fixed, then 239 tags across the base layer
    everything else calls: stdlib/{Vec,Mem,Str,Utf8,Fmt}.ax and
    self_host/{core,style,lexer,diag}.ax, chosen because symbols --diagnostic-format=ai found 1393 functions tree-wide satisfying both
    no-io and no-alloc and these nine files are the ones whose import
    graph provably reaches no extern (Ffi.ax and rustbind.ax are the
    only files in the tree with one). The five #effects-incomplete
    declarations — vecSortBy, vecSiftDownBy and three in Http.ax, all
    indirect-call sites — are deliberately untagged, because a claim there
    draws AX3051 rather than passing, which is the effect walk correctly
    refusing to vouch for a call it cannot follow. ABLATED, and the number
    is the point: breaking three functions produced 108 AX3049s, each
    naming its own witness path, so the transitive fixpoint is doing the
    work rather than a single accepted claim. 173 programs still emit
    byte-identical IR, diagnostics and AXSYM with every one restricted.
    tests/agent/restrictions.allow 49 → 288 rows.

  • restrict(no-wrap), and it is LEXICAL where three of its siblings
    are transitive.
    +, - and * lower to plain add/sub/mul
    with no nsw, so overflow wraps silently; claiming no-wrap and
    writing a raw operator is an act this body performs rather than a fact
    the effect row carries, which is the same reason no-cast is lexical.
    tests/diagnostics/383-restrict-no-wrap.ax; explain documents the
    distinction and scripts/check-tools-selfhost.sh holds that text to
    the compiler's own.

  • Removing a keyword broke the direction reseed.sh's rule does not
    mention.
    scripts/check-seed-provenance.sh regenerates the seed from
    the commit that last touched the six .ll files using the CURRENT
    compiler; that commit's tree still said trait, the current compiler
    answers AX2004, and the gate went red — while check-bootstrap
    stayed green throughout, because the old seed understands a SUPERSET of
    the language and could still build the new tree. The seed is a TWO-WAY
    compatibility boundary and reseed.sh documents one direction ("the
    seed moves when it can no longer compile self_host/"); the other is
    that the seed's recorded source must stay buildable BY the tree. Budget
    a reseed with any construct removal. bootstrap/CHAIN gains a
    stage2 row and not bridge-needed, so check-seed-lineage.sh still
    replays back to the Rust compiler at bb730db. Verified before
    committing: seeds match SHA256SUMS, the seed built darwin-aarch64
    with no Rust, compiled self_host/ into stage1, and stage2 and stage3
    are byte-identical.

  • gate_init resolved the compiler as ${AXIOM:-.axiom-bin/axiom} and
    never mentioned AXIOM_AXC.
    Twelve gates that call it without
    gate_build_axc silently measured the installed binary while a caller
    believed it was testing theirs. It now resolves AXIOMAXIOM_AXC
    bootstrap and prints which it took and why, every run; and it runs the
    compiler once before handing control to the caller's loop, so a binary
    that cannot exec produces a diagnosis instead of N empty failures —
    exit 137 names the macOS signature-cache mechanism and the rm-then-cp
    fix. The sweep in scripts/check-gate-lib.sh proving no site states a
    stale gate count looped seq 15 38 while its own table went to 46, and
    that was not hypothetical: scripts/build-shared-axc.sh disagreed with
    itself, stating a superseded count in one breath and the correct
    forty-six in four others, live in the tree and unseen. The bound is
    derived from the table now. 187 checks → 236. (The stale figure is
    described rather than quoted here on purpose: this sweep reads the
    ## Unreleased section as a claim about the tree, correctly, so a
    changelog entry that quotes a wrong count verbatim re-introduces the
    defect it is reporting.)

  • Dogfooding the two features above: a one-constructor data becomes
    the struct it always was, and four sentinels answer Option.

    stdlib/Http.ax's HttpHandler was (data HttpHandler (HttpFn (-> Int HttpReq Int))), matched apart in httpCall to reach its one
    field - exactly the shape parameterised structs exist to replace,
    even though this one needed no type parameter. It is now (struct HttpHandler (run : (-> Int HttpReq Int))), built the same way
    ((HttpHandler (lambda (fd r) ...))) and read with ((h.run) fd r), no match required. stdlib/Path.ax's
    pathLastSlash/pathExtIndex and stdlib/Agent/Tags.ax's
    axsymHexVal/axsymPctAt answered a raw -1 for "not found";
    docs/error-model.md ERR-REC-3 says absence wants Option, and
    both modules - named in compat/SENTINELS as exceptions to the
    file's own direction rule - now read zero. All four had zero
    external callers (checked by grep over the whole tree before
    porting), so each port stayed inside its own file; pathExtIndex
    keeps going straight to the private -1-returning helper rather
    than through the new pathLastSlash, because it needs the sentinel
    back in arithmetic, not a value to branch on. Five lines in
    compat/BREAKING declare the surface change; #effects= gains
    nothing, matching the Option Int functions the census already left
    alone (strFind). tests/stdlib/055-filesystem.ax,
    tests/stdlib/380-agent-tags.ax and
    tests/stdlib/432-http-router.ax pin the observable behaviour
    unchanged.

  • Structs take type parameters, and that is what an interface is now.
    (struct ShowOf (a) (render : (-> a String))) - the parenthesised
    spelling data already uses, one convention rather than two. Before
    this the form was AX2001, so a capability record had to be a data
    type destructured by match, because only a struct has named fields
    and .field access. (fn (showInt) (ShowOf fmtInt)) is the
    instance, ((c.render) 7) is the call, and nothing dispatches at run
    time.
    The first group is ambiguous in a struct and not in a data, which
    is the whole difficulty. A data declaration's other groups are
    CONSTRUCTORS and start uppercase, so (a) can only be parameters. A
    struct's other groups are FIELDS and start lowercase too. The rule is
    that a parameter list is lowercase names and nothing else: (a b)
    yes, (start : Int) no - a colon makes it a field - and
    (msg String) no, because String is uppercase, which keeps
    tests/diagnostics/388's three AX3056 refusals intact. Both halves
    were found by the tree refusing to build, not by inspection: handing
    the group to collectTyParams unguarded swallowed Span's first
    field and the SEED stopped compiling self_host/main.ax; testing
    only for the colon then swallowed (msg String) and one AX3056
    vanished.
    Two more caught the same way. Writing the parameter vector onto
    every D_STRUCT node's ty slot gave the macro expander a handle it
    walked as a type node - 381-macro-type-templates and
    396-macro-struct-field-types died of SIGSEGV. An unparameterised
    struct now leaves the slot at 0, and checkStructConAt guards
    (== tvs 0) before vecLen, which dereferences null on 0.
    The grammar agreed with the text and disagreed with the compiler.
    tree-sitter parsed (a) as a typeless field_declaration with
    zero ERROR nodes - accepted, and wrong. A dynamic precedence on
    type_parameters settles it, and only where it is genuinely
    ambiguous, since (x : Int) cannot match that rule at all.
    And the formatter broke its own input, printing a single variable
    bare: right for data, which accepts both spellings, fatal for
    struct, which accepts one. axiom fmt was producing files the
    compiler could no longer read. fpTyvars now takes the choice from
    its caller.
    tests/selfhost/901-parameterised-struct.ax pins it at exit 12: two
    parameters staying independent, and a capability answering through
    its field.

  • sysWriteAllFd stopped truncating output silently, and a claim about
    why the Result migration is hard, CORRECTED below.

    When write returned exactly 0 - a legal answer, and the one case
    the loop cannot retry - this answered done: a short, NON-NEGATIVE
    count indistinguishable from the complete one. stdlib/Sys.ax's own
    comment calls treating a short write as success "the classic way to
    truncate output". It answers -errShortWrite now, so the callers
    that already ask < 0 - all of them - see it.
    It was ported to (Result Int Error) first, and that is the
    interesting part.
    ERR-ADOPT-1 records (Some v) at 7.4x a -1
    and treats cost as the reason Sys.ax is hard. Measured here,
    2,000,000 calls at --opt 2: 1.399 / 1.065 / 1.054 s sentinel
    against 1.298 / 1.092 / 1.093 s Result
    - the same number. A write
    is ~500 ns and the constructor ~9, so on a wrapper that reaches the
    kernel the channel is free. The 7.4x figure is real and was taken
    with no syscall in the loop.
    The port DID widen the effect row from IO to IO, Alloc, Mut and
    ten gates went red - 300-effect-handlers could no longer
    handle what it declares, and AXTAG claims began reading body performs Alloc, IO, Mut where they had said IO. The cause written
    here first was "the row comes from the constructor". That is
    wrong
    , and the correction matters more than the port: measured, a
    Result-returning function whose error message is a LITERAL has no
    effect row at all, and the same function with a strConcated message
    has Alloc,Mut. Ok/Err/mkError are effect-free. The tax came
    from sysResult, which builds "op: errno N" eagerly with two
    strConcats and an fmtInt on a path most callers never read. The
    narrowing test that "proved" otherwise replaced one message with a
    literal and left sysResult untouched.
    So the Result migration is not blocked by the effect system.
    It is blocked by eager message construction, which is one line.
    Also corrected: Sys does NOT sit below Err in the dependency
    order, which ERR-ADOPT-1 gives as the reason its slice is hard.
    The order is Mem -> Vec -> Str -> Err -> Sys; Sys.ax already
    imports Err and already uses sysResult. And self_host/ reaches
    only 4 of the 27 sentinel-answering functions (20 sites), so 23 of
    them can be ported without touching the compiler at all.

  • freebsd-x86_64 and windows-x86_64 are supported targets. Both
    legs had been green on 13 of the previous 15 runs with no failures,
    and README's rule is that continue-on-error comes off after the
    evidence — never in the commit that adds the job, which cannot have
    seen anything. Both lines are off; both legs can now fail the
    workflow, which is the whole content of the word.
    The two legs do not cover the same amount, and the documents say
    so rather than letting one word mean two things.
    FreeBSD boots a
    real 14.4 kernel in a VM, bootstraps from the committed seed and runs
    the whole standard-library corpus plus five gates, including
    check-net.sh opening a real listener on ::1. Windows runs one
    program
    hello.exe, executed on windows-latest against
    tests/stdlib/010-hello.out, imports held to an allowlist with a
    leaky probe proving the allowlist refuses. Both satisfy the rule;
    only one would catch a Windows-only miscompile in a module hello
    does not touch. Widening it means running the corpus the cross job
    already assembles.
    Supported as a TARGET is not supported as a HOST: the compiler
    does not run on Windows, there is no Windows seed, and install.sh
    refuses a Windows host outright.
    freebsd-x86_64 is supported and UNSHIPPED, so install.sh now
    gives it the build-from-source paragraph linux-x86_64 gets rather
    than the not-supported one — while freebsd-aarch64, same seed and
    same syscall table but no leg, keeps the latter. That split inside
    one operating system is the clearest statement of the two axes the
    previous release note introduced.
    Three defects found on the way, two of them in checks:
    SECURITY.md's exclusion was keyed on an OS (**Windows.** Not a supported target) and check-doc-drift.sh requires at least one
    such bullet — but every OS in the list now has a supported target, so
    the premise had become unsatisfiable. Both are keyed on the TARGET
    now, which is what the rule was always about.
    check-release-targets.sh matched CI legs with grep "name: $t",
    which finds a matrix entry but not name: Tests (freebsd-x86_64)
    it would have reported both newly-promoted targets as supported with
    no leg.
    And nothing anywhere asserted continue-on-error: the single
    line that decides whether a leg can fail, whose removal every
    document defines promotion as, was checked by no gate — a leg could
    have been made advisory again to turn a red build green and every
    check would have stayed quiet. There is now an arm for it, and it was
    vacuous on its first write: awk -v pat="Tests \($t\)" has its
    escapes processed by -v before the regex sees them, so \( became
    a group and the pattern matched nothing. Caught by ablating it —
    injecting the line back and watching the check stay green — which is
    the only reason it is a check today.

  • The evidence words travel by DEPTH, and two documented claims were
    wrong in opposite directions.
    Both were read off the emitter's
    shape instead of measured, and both were corrected by probes that
    should have been written first.
    The effect-operation path was never a hole. It passes the constant
    0, which is what the claim was read from — but a handler's
    parameter is bound to the OPERATION's declared type and AX3017
    refuses a type variable there, so it is always ground and the retain
    is unconditional. Measured: call @Vec$vecPush(i64 %.t2, i64 %m, i64 1), and a handler parking a struct field answers correctly. The
    word that path passes is one the handler never reads.
    The outer parameter of a curried lambda was worse than stated. It
    was called a leak; it was a live use-after-free. (lambda (a b) ..) is (lambda (a) (lambda (b) ..)) by the parser, so a store of
    a runs inside the inner lambda with a read out of the closure
    record, and the inner lambda's own word is about b. Nothing carried
    the word that classifies a.
    So curLamVar is a stack rather than a name, evClassOf answers
    EV_LAMARG - d for a parameter d lambdas out, collectCapNames
    takes the enclosing lambdas' words into the nested record the way it
    already took the enclosing function's, and bindCaps shifts each one
    level as it binds — every lambda's own argument stays depth 0 and no
    witness is renumbered after the fact.
    New fixture tests/stdlib/461-curried-closure-arg.ax pins four
    depths across two- and three-parameter lambdas. It is a separate file
    for an arithmetic reason worth recording: 460's exit-status bitmask
    uses all eight bits a process exit code has, so a ninth term
    there is 511 & 0xFF — 255, the same answer it gives today, while
    asserting one more thing than it can say. Built by the compiler one
    commit back this file does not answer wrongly, it exits 139: four
    uncounted parks in one process recycle blocks into each other until a
    header read lands outside the heap.
    What remains is unmeasured rather than known safe — the surplus
    arguments of a cast spine, and the over-applied path. That is
    deliberately the same sentence shape that was wrong twice above, now
    said about what has no probe instead of about what is fine.

  • The seventh closure shape closes, and the reason it was left open
    was wrong.
    A factory whose declared result arrow carries a type
    variable — (:: mk (-> Int (-> a Int))) — emitted a correct chain
    and still under-retained, because a appears only in mk's result,
    so nothing at mk's call site witnesses it and the caller passes
    0. It was left open on the ground that a SOURCE variable cannot be
    matched by name the way a minted placeholder can, since "a may
    denote other values in the same body".
    That conflates values with types. a does denote several values —
    the parameter, a capture, a temporary — but the evidence word is a
    fact about a TYPE
    , and a type variable denotes one type throughout
    its scope: if the application hands over a String for a, every
    a in that body is a String. Nothing can shadow it, because type
    variables come from the signature being checked and expressions
    cannot introduce their own. So checkLamAgainst names a
    type-variable parameter and evClassOf answers it EV_LAMARG, the
    application's word beating the enclosing function's — which is the
    one that arrives as 0.
    All seven measured shapes now read a where they read z. Two
    probes hold the reasoning rather than the conclusion: a lambda whose
    parameter and a capture share a parks both and answers correctly,
    and the same factory applied at Int stores and reads back 41
    evidence 0, no retain, no reference count on an integer. The
    remaining unclassified applications are down to two, both leaking
    rather than freeing early: the effect-operation path and the outer
    parameter of a curried lambda.

  • The gate battery can be run on Linux from a Mac, before CI sees
    it
    scripts/run-gates-linux.sh, the same battery and the same
    scripts inside a container. This is the feedback-loop repair the two
    entries below are symptoms of: both were gates written and validated
    on darwin, where the local battery runs, going green on the machine
    that wrote them and red on a leg that had never seen them. Neither
    was a defect in the target.
    It copies the tree into the container and mounts the repo
    read-only, which is the one decision worth reading: gate_init
    bootstraps a compiler into $repo_root/.axiom-bin when it finds
    none, so a writable bind mount would leave a Linux binary in the
    checkout and the next darwin gate to reuse it would fail for reasons
    unrelated to the change under test. Defaults to the host's native
    architecture — linux-aarch64 on Apple Silicon, at full speed;
    --arch amd64 runs linux-x86_64 emulated and says so, that being
    the leg both defects came from. A missing container runtime is an
    error with install instructions, not a silent skip. Not a gate: it
    asserts nothing about the tree and run-gates.sh does not glob it.
    What is verified and what is not: argument handling, every exit
    path, the runtime-absent message, the image-tag hash, and the tar
    copy exercised against the real repo (.git and .axiom-bin
    excluded, exec bits preserved). The in-container run itself is
    unexercised — no container runtime is installed on the machine
    this was written on, and the header does not claim otherwise.

  • check-steady-state.sh failed a run whose memory went DOWN. The
    plateau arm compared |b - a| against a 256 KiB band, so a fall past
    the band failed exactly as growth did. It fired on trunk, on the
    linux-x86_64 leg: aggregate/owning moved 1460 -> 1196 KiB over ten times the work - that is not a plateau — 264 KiB apart, eight past
    the band, in the shrinking direction
    .
    The number being compared is ru_maxrss, a high-water mark taken
    from two separate process lifetimes. It cannot fall because the
    program held less live data at 100× the work; only because the
    runtime, the loader or the allocator touched fewer pages that time. A
    leak has no way to express itself as a smaller peak, so the symmetric
    band was testing a property the gate's own subject does not imply.
    The band is directional now: growth past it fails, a fall past it is
    reported and not hidden — a drop that large is worth a reader's
    eye even though it is not this gate's subject. Nothing weakens.
    Memory that is merely large is still caught by the 4096 KiB ceiling,
    the ablated twins by the ratio arms, and both ablations recorded at
    the bottom of that file grow (2,928 → 17,008 → 157,616 KiB for one),
    so every FAIL line they are documented as producing is still
    produced. Boundary-checked at ±256 and ±257 either way.
    That is the second Linux-only gate defect in two days, after
    check-thread-local.sh asserted a Darwin fact as a universal one.
    Neither was a defect in the target: both were gates written and
    validated on darwin, where the local battery runs, going green on the
    machine that wrote them and red on the leg that had never seen them.

  • linux-x86_64 is no longer a release artifact, and is still a
    supported target.
    Those are two axes, not one, and until now the
    project only had a word for the first. release.yml builds
    linux-aarch64 and darwin-aarch64; the linux-x86_64 leg was the
    slowest and the most frequently re-run part of cutting a release, and
    it served the platform whose users are most likely to already have a
    toolchain. Nothing about testing changed: Tests (linux-x86_64)
    runs the whole gate battery on every pull request, exactly as before.
    scripts/install.sh refuses that host with a build-from-source
    message naming bootstrap-from-seed.sh rather than fetching a 404,
    and it is a different message from the one darwin-x86_64 and the
    two FreeBSD targets get — those are unsupported, this one is
    supported and unshipped, and telling a user their platform is
    unsupported when it is not would be the defect.
    New gate check-release-targets.sh holds the two lists to each
    other, because they live on opposite sides of the project and neither
    failure mode is loud: a target in both the matrix and the refusal
    list uploads an archive the installer will not fetch, and a target in
    neither gives the user a bare curl 404. Seven checks — the two
    lists are disjoint, together they cover every non-Windows target the
    compiler accepts, every shipped target is one README calls supported,
    and every supported-but-unshipped target still has a CI leg or a
    README paragraph explaining why it does not (darwin-x86_64, which
    is executed by no runner). Both directions were ablated: re-adding
    linux-x86_64 to the matrix fails on "built AND refused", and
    removing its refusal arm fails on "neither built nor refused".
    Forty-seven gates build the compiler under test, up from forty-five.

  • Terminal primitives, and the gate that keeps them honest. Sys
    grew the floor a REPL's line editor needs and nothing above it:
    sysIsatty, sysTermSave/sysTermRaw/sysTermRestore, and
    sysTermSize with sysTermRows/sysTermCols. Nineteen constants per
    platform module carry them, and every one records how it was
    established - measured on this host for Darwin, quoted from the
    kernel's uapi headers for Linux, derived from _IOC with the
    arithmetic shown for FreeBSD. windows-x86_64 answers
    ttyUsesTermios 0 and every call returns an error: it has no
    termios and no ioctl, nothing here can execute GetConsoleMode,
    and an honest unimplemented beats a plausible wrong number - an ioctl
    request is a command selector plus a byte count, so a number
    borrowed across platforms copies the wrong length rather than failing.
    struct termios is 72 bytes on Darwin, 36 on Linux and 44 on FreeBSD,
    which is why the state buffer's size is a call and not a constant.
    New gate check-terminal-restore.sh asserts the round trip is
    byte-exact on a pty it allocates itself - and asserts, in the same
    breath, that raw mode changed something first, because a sysTermRaw
    that does nothing round-trips perfectly and would satisfy the obvious
    check. Two independent witnesses: the library's memCmp, and
    tcgetattr from outside the process using Python's own termios.ISIG
    rather than the constant under test. Four ablations, all required.
    check-install.sh gained the branch this needs: on a host whose
    target ships no archive it asserts the refusal instead, and says
    plainly that the install path was not exercised there rather than
    skipping quietly.

  • check-thread-local.sh asserted a Darwin fact as a universal one.
    Its second arm required nm -u to be empty for a program that
    spawns no thread. That holds on Darwin and is false by construction on
    Linux, where the same program imports six symbols — four weak crt
    hooks (_ITM_*, __gmon_start__, __cxa_finalize) and two real ones
    (__libc_start_main, abort). Both Linux CI legs went red on a
    program behaving exactly as intended. The measurement that fixes it
    was in the failure text: six off and six on, identical — the flag
    adds nothing, which is the property the gate exists to hold and which
    "zero" could not express.
    The arm now asserts what the flag is actually about: a program that
    spawns no thread imports no TLS runtime symbol, read through
    imports_ofcheck-freestanding.sh's reader, which dispatches on
    the object's own magic rather than the host and strips ELF's
    @GLIBC_2.34 versions and Mach-O's leading underscore. And the cost
    is now measured as a delta: the ON binary's imports minus the OFF
    binary's must be TLS symbols and nothing else — one on Darwin
    (_tlv_bootstrap), none on Linux or FreeBSD, where local-exec needs
    no resolver. That is a stronger claim than the one it replaces, and a
    portable one. 14 checks, up from 12.
    This is the second time in one release that a gate of mine assumed one
    platform's shape was universal; the first was grepping only for
    __tls_get_addr, which AArch64 never emits.