Changes
Every change to the specification since it was written, dated. Useful precisely because it shows the design moving and why.
Two kinds of change are recorded. The change logs are the spec author’s: each entry says what changed in the documents and what the codebase must do about it, and is marked (to apply) until the code has caught up. The changes forced by implementation are the compiler’s: each is a rule that turned out unworkable or underspecified when built, marked in the spec text with changed: and pinned by a fixture. Nothing is changed silently, in either direction.
Contents
- Change log 1 — effects, the loop, targets
- 2026-09-03 — Effect marker ! replaced by may (applied to docs)
- 2026-09-03 — Regeneration loop specified as a separate document (applied to docs)
- 2026-09-03 — Targets: dual backends as a design goal (applied; M11 and M12 done 2026-09-05, docs/CHANGES.md items 93–104)
- 2026-09-03 — Documents added, no spec impact
- Not changed, but decided
- Change log 2 — testing
- Change log 3 — trust zones
- Changes forced by implementation
- M1 — front end
- M2 — resolve and types
- M3 — effects
- M4 — const evaluator
- M5 — codegen, everything checked
- M6 — verification
- M7 — reports
- M8 — claims, capabilities, paths
- M9 — onus next
- M10 — the review tool
- Testing model (docs/CHANGE-LOG-02.md, applied 2026-09-04)
- M11 — native backend
- M12 — targets complete
- M13 — contract mutation and coverage
- M14 — regeneration loop
- M15.0 — prerequisites for the compiler in Onus
- M15.1 — the front end in Onus, first part: the lexer
- M15.1 — the front end in Onus, second part: the parser
- M15.1 — the front end in Onus, third part: structural recursion and the printer
- M15.2 — the checker in Onus, first part: loading, resolution and the type layer
- M15.2 — the checker in Onus, second part: types, constants and effects
CHANGES.md — Onus specification changes
Changes to onus-spec-v0.md and onus-impl-spec-v0.md made since the implementation started. Apply in order. Each entry states what changed, why, and what to do in the codebase. Entries marked (applied to docs) are already in the spec files; entries marked (to apply) need the spec text added as well as the code.
2026-09-03 — Effect marker ! replaced by may (applied to docs)
Change. The effect list after a return type is introduced by the keyword may instead of !. may is a reserved word.
fn main(args: List[Text], files: io.Files) -> Result[Unit, io.Error] may io.file, alloc
fn map[T, U, e](xs: List[T], f: fn(T) -> U may e) -> List[U] may e, allocGrammar (§2.3): every [ "!" effects ] is now [ "may" effects ] — in fn_decl, type (function types), iface_item, and the closure form of primary. Stream[T] may e likewise.
Why. ! reads as “not” in every language a model knows; may reads correctly for every effect (may allocate, may panic, may write) and states the declaration as the claim it is.
Codebase. Lexer: add may to keywords; ! is no longer a token outside !=. Parser: four productions. Printer: emit may. All fixtures and the three examples updated. No semantic change.
2026-09-03 — Regeneration loop specified as a separate document (applied to docs)
Change. docs/onus-loop-v0.md added. It is a candidate spec for the component that drives the model against the compiler. It does not change the language.
Codebase. Nothing now. It depends on the compiler’s JSON outputs (§9.1, §11.1, §13) being stable and on onus next (M9), which are already in the plan. Do not start on it until M10 is done.
2026-09-03 — Targets: dual backends as a design goal (applied; M11 and M12 done 2026-09-05, docs/CHANGES.md items 93–104)
Change. Onus programs compile to JavaScript and to native code from the same source, and later to WebAssembly. This is a stated goal, not an accident of architecture, and it adds a section to the language spec and two milestones to the implementation spec.
Language spec: new §19 “Targets” (insert after §18 Worked examples)
19. Targets
An Onus program compiles unchanged to every supported target. Observable behaviour is defined by this specification, never by the host. Where this specification is silent on something a program can observe, that is a defect in the specification.
19.1 Runtime primitive surface
Each target provides a runtime implementing exactly the following primitives. Everything else in
std.*is written in Onus and compiled by the same backend as user code.
- Memory: allocate, free-at-scope-exit (native) or no-op (collected hosts).
Int: 64-bit signed arithmetic with overflow detection; see 19.3.Float: IEEE 754 binary64 arithmetic;classify; formatting to text per the algorithm instd.float(shortest round-trip representation).Text: UTF-8 storage; grapheme-cluster segmentation per Unicode 16.0 (pinned; the runtime carries the tables); byte and grapheme views; equality by code point sequence.Bytes: contiguous byte sequence with bounds-checked access.- Panic: raise with an obligation id and optional model;
recoverboundary.- Capabilities: opaque handles for
io.Files,io.Env,io.Net,io.Clock,io.Rand,sql.Db, plus the__fakeconstructor available only to test modules.io.*andsql.*raw calls, each mapped one-to-one from anassumeleaf instd.io/std.sql.The primitive surface is versioned with the specification. A backend that lacks a primitive reports
E0800 primitive unavailable on targetat build time for any program reaching it.19.2 Host claims
Code that can only run on one host declares it with a claim:
host.js,host.native,host.wasm. These are asserted claims (§7.1) introduced only atassumeleaves that call host-specific facilities, and they propagate like any claim. Apathmayforbid { host.js, host.native, host.wasm }to require portability, and the compiler then rejects anything reachable that depends on a host.19.3 Integer representation
Intis 64-bit signed on every target. On targets without native 64-bit integers (JavaScript), the backend chooses a representation per value: a double-precision number where the verifier has proved|x| <= 2^53 - 1for every value the binding can hold, and an arbitrary-precision integer otherwise. The choice appears in the ledger as an obligation of kindrepresentation, so a reviewer can see which values are running on the slow path and tighten refinements to move them.19.4 Fully specified behaviour
The following are specified so that all targets agree:
Mapiteration is in key order underOrd[K]; integer division truncates toward zero and%takes the sign of the dividend;FloattoTextis the shortest round-trip form;exampleblocks run in source order;Streamelements are produced on demand and never buffered beyond one element by the runtime.19.5 Differential testing
Every
exampleandpropertyruns on every built target. Any disagreement between targets on a program the compiler accepted is a backend defect, reported asE0801 target disagreementwith the example, the two results and the targets.
Language spec: §17 Open questions
Add: Concurrency, when designed, must fit both a single-threaded event-loop host and a native multi-threaded one; structured concurrency over immutable inputs with channels by value is the working assumption.
Implementation spec: decisions table
Add a row:
| Native target | LLVM IR text emitted by the compiler; clang assembles and links against a small C runtime | Own lowering (semantics stay ours), borrowed instruction selection and optimisation; nothing to install beyond Xcode CLT / clang on Linux |
Add to §6 Codegen: the codegen pass has two emitters behind one interface, emit(ctx, target). The lowering from checked AST plus obligation statuses to a target-neutral form is shared; only the final emission differs. Do not duplicate lowering logic per target.
Implementation spec: milestones
M11 — Native backend. LLVM IR emitter; C runtime for the 19.1 primitive surface (no sql yet); onus build --target native produces an executable via clang. proved obligations emit no code; checked obligations emit compare-and-branch to onus_panic with the obligation id; recover via setjmp/longjmp. Int is i64 with llvm.*.with.overflow intrinsics. Accept: Mandelbrot builds natively and writes an identical PGM to the JS build; every example passes on both targets; E0801 fires on a deliberately broken runtime primitive.
M12 — Targets complete. sql primitives in the C runtime over libpq; host claims; Int representation obligations in the JS backend; differential test harness running all fixtures on both targets; WebAssembly emission via the same LLVM path (--target wasm), with io.* mapped to WASI. Accept: all three examples build and agree on both native and JS; the reporting example runs natively against Postgres; a path with forbid { host.js } rejects a JS-only assume leaf.
Codebase now. Nothing changes before M10. When starting M11: the shared lowering in codegen/ is the design constraint — if the JS emitter has lowering logic tangled into emission, separate it first, and add a fixture set for the target-neutral form.
2026-09-03 — Documents added, no spec impact
docs/onus-pitch.md— a short pitch. Its example usesE0201on amay sql.read, allocsignature; if the compiler’sE0201text ends up different from the illustrative one, the pitch follows the compiler, not the reverse.
Not changed, but decided
- The syntax borrows F#’s data model only (unions with
of,match ... with,when,{ x with ... }) and is deliberately not F# elsewhere. Already in §2 “Borrowing policy”. Do not import F# conventions the spec does not name. - Product output is JavaScript in one step;
--emit tsis a fixture-suite oracle only. Already in the implementation spec and CLAUDE.md. - The compiler is the only checker. No warnings, no lint, ever.
CHANGE-LOG-02.md — Onus specification changes: testing
Follows CHANGES.md. Apply after it. Adds the testing model to onus-spec-v0.md and onus-impl-spec-v0.md. Nothing here changes the type system or the verifier; it adds two syntax forms, two ledger fields, two onus test modes, and one reported metric.
2026-09-03 — Testing model (applied; M13 done 2026-09-05, docs/CHANGES.md items 105–109)
Principle. Behaviour is established by contracts (proved) or by examples and properties (checked); dependencies are supplied as capabilities and faked in test modules; only assume leaves need testing against reality. There is no assertion library, no mocking library, and no separate test runner: the assertion language is the contract language, the mock mechanism is fake, and the runner is the compiler.
Language spec: new §20 “Testing” (insert after §19 Targets)
20. Testing
20.1 What is tested and where
Concern Mechanism Lives in Behaviour, all inputs requires/ensures/invariant, provedthe function’s interface Behaviour, specific inputs example(§5.2)the function’s interface Behaviour, generated inputs property(§5.2)the function’s interface Dependencies capabilities passed as parameters; fake(§8.4)test moduleScenarios across modules exampleblocks in atest module, with fakes at the edgestest moduleContact with reality verifyblocks onassumeleaves (20.2)next to the assumption Regressions exampleblocks pinned from counterexamples (loop spec §7)the function’s interface Strength of the contracts regeneration audits (loop spec §8) and contract mutation (20.4) onus testThere is no test tree parallel to the source. An example is attached to what it exemplifies.
Functions with the
nondeteffect take their source of nondeterminism (io.Clock,io.Rand) as a capability, so a test supplies a fixed one. A test that could be flaky is not expressible.20.2 Assumption verification
An
assumemay carry averifyblock: an Onus function body that exercises the assumption against the real resource and yieldsBool.assume Idempotent "Vendor API deduplicates on req.key for 24h; see contract §4.2" verify(client: payments.Client) may io.net, alloc { let a: Receipt = try payments.charge(client: client, key: "verify-1", amount: 100) else _: false let b: Receipt = try payments.charge(client: client, key: "verify-1", amount: 100) else _: false a.id == b.id }
- The block’s parameters are capabilities, supplied by the environment running
onus test --assumptions, never constructed by the block.- The block declares its effects like any function and may not exceed the effects of the function containing the
assume.verifyblocks are never run byonus check; they run only underonus test --assumptions, which is expected to be pointed at a staging or test environment.- An
assumewithout averifyblock is permitted and is reported as unverifiable in the ledger.20.3 Ledger fields
Each
assumeentry in the ledger (§9.1, §11.1) gains:
verifiable: bool— whether averifyblock exists.last_verified: { at: timestamp, target: string, result: "passed" | "failed" } | null— recorded byonus test --assumptions, persisted in.onus/ledger/.The review tool shows assumptions as assumed, verified
against or assumed, unverified. Apathmay requirepolicy verified_assumptions_only, which fails the build if any reachableassumelacks a passing verification within a repository-configured age.20.4 Contract mutation
onus test --mutateweakens contracts one at a time and reports which weakenings no example or property detects. Mutations applied, per obligation: drop anensuresclause; replace a refinement bound with its base type; negate a guard in aproperty; drop alaw. A mutation that survives — every example and property still passes — is reported asM0001 undetected contract weakeningwith the mutation and the function. It is not an error; it is the signal that the examples are not carrying the contract’s meaning.Mutation never touches bodies. Bodies are the model’s; weakening them is what the loop already does implicitly by regenerating.
20.5 Obligation coverage
The reported test metric is obligation coverage, per module and per path:
- obligations proved;
- obligations checked, and of those, how many are exercised by at least one
exampleorproperty;- assumptions, and of those, how many are verifiable and how many have a current passing verification;
- contract mutations detected versus surviving.
Line coverage is not reported and cannot be enabled.
20.6 The runner
onus testevaluatesexampleandpropertyblocks (already done byonus check), runstest modules, and on multi-target builds runs everything on each target, reporting disagreement asE0801(§19.5).onus test --assumptionsrunsverifyblocks against supplied capabilities.onus test --mutateruns contract mutation. There is no plugin mechanism and no configuration file beyond the repository’s target and environment settings.
Grammar (§2.3)
stmt = ...
| "assume" TNAME STRING [ NL verify_block ]
verify_block = "verify" "(" [ params ] ")" [ "may" effects ] block ;verify is a reserved word.
Implementation spec
§4 Passes. Pass 9 (claims) records verify blocks on assume sites; they are type- and effect-checked like functions in pass 4/6 but excluded from codegen except under --assumptions.
§5 Runtime. .onus/ledger/ gains assumptions.json, keyed by module and assume location hash, holding last_verified.
§7 Reports. interface.json and path.json gain the two ledger fields per assumption and an obligation_coverage block per module/path.
Milestones. Add to M8 (claims, capabilities, paths): verify blocks parsed, checked, and stored; onus test --assumptions runs them against capabilities constructed from a repository config; ledger fields populated; policy verified_assumptions_only. Accept: the checkout example’s Idempotent assumption has a verify block that passes against a fake payments service and the path report shows it as verified.
Add to M10 (review tool): assumption freshness shown in the path and ledger views.
Add M13 — Contract mutation and coverage. onus test --mutate with the four mutation kinds; M0001 reporting; obligation coverage in interface.json, path.json and the review tool. Accept: dropping the ensures on recent_orders is detected by its property; dropping a deliberately unexercised refinement in a fixture survives and is reported.
Codebase now
Nothing until M8. The fake mechanism and test module already planned for M8 are the foundation; verify blocks reuse the same capability-construction path.
Not changed, but decided
- There is no assertion library. Contracts are the assertion language.
- There is no mocking library. Capabilities and
fakeare the whole mechanism. - There is no separate test runner or plugin system.
onus testis the compiler. - Line coverage is not a concept in Onus.
CHANGE-LOG-03.md — Onus specification changes: trust zones
Follows CHANGE-LOG-02.md. Apply after it. Adds zones — per-module levels of strictness — to onus-spec-v0.md, onus-impl-spec-v0.md and onus-loop-v0.md. The type system and verifier are unchanged; zones add a manifest, a dependency rule, per-zone policy bundles, a promotion command, and zone-aware loop behaviour.
2026-09-05 — Trust zones (to apply)
Principle. Trust in Onus is per artefact: interfaces are the human’s, bodies are the model’s, and the ledger records what each obligation rests on. A project is not uniformly trusted at any moment in its life — a hardened core coexists with a subsystem being prototyped against it — so strictness is declared per module, and the only rule that matters is that nothing at a higher level of trust ever rests on a claim from a lower one.
Language spec: new §21 “Zones” (insert after §20 Testing)
21. Zones
Every module belongs to exactly one zone. A zone is a level of strictness. There are three:
Zone Meaning draftBeing designed. The ledger is recorded but not authoritative. Interfaces may change freely, by human or model. Bodies may be human-edited. hardenedIn service. Interfaces are the human’s, bodies are the model’s (loop spec §1). The ledger is authoritative. criticalIn service and load-bearing. hardened, plus every public entry is on apath; no unverified assumptions; norecover; nocheckedobligation without an exercisingexampleorproperty.Zones are declared in the project manifest (21.4), not in modules, because a zone change is a decision about the project and its diff is what a reviewer approves.
21.1 The dependency rule
A module may depend on another module’s interface only if that interface is at the same zone or higher.
draftmay depend on anything.hardenedmay depend onhardenedandcritical.criticalmay depend only oncritical.One exception makes integration possible: a
draftmodule may mark individual public itemshardened. A hardened or critical module may depend on those items — and only those — from the draft module. A hardened item in a draft module is checked to the hardened standard: its contracts may not be changed by the loop, its obligations appear in the authoritative ledger, and itsassumeleaves are subject to the depending zone’s policies. Its body remains draft.-- in a draft module pub hardened fn charge(client: Client, req: ChargeRequest) -> Result[Receipt, Error] may io.net, alloc ensures ...This is the mechanism for building a new subsystem against a stable core: harden the boundary first, and the core is permitted to see only the boundary.
Violations are
E0900 dependency crosses zone boundary, naming both modules, the zones, and the item.21.2 Zone policies
Each zone applies a fixed bundle of the policies that already exist:
draft: none.assumeunrestricted;recoverunrestricted;checkedunrestricted.hardened:no_loop_authored_claims(the loop may not edit interfaces; loop spec §1 and §5); assumptions must have a justification string.critical:hardenedplusverified_assumptions_only(§20.3),forbid { recover }on every path,checked_requires_example, andno_third_party_assumesunless individually excepted in the manifest.A module may add policies beyond its zone’s bundle. It may not remove any.
21.3 Promotion and demotion
onus zone promote <module> <zone>runs the regeneration audit (loop spec §8) on the module at the target zone’s standard: bodies are regenerated from interfaces alone, and every finding becomes a proposal. Promotion succeeds only when the audit reports no findings and the zone’s policies pass; the manifest change is opened for review like any other change. The audit result is stored in the ledger as the promotion record.
onus zone demote <module> <zone>is always permitted and always recorded. Demoting a module that others depend on does not break the build; it marks every dependent’s guarantees that rest on the demoted module as conditional in the ledger and the path reports, until the module is promoted again.Zones only ever change through these commands. Editing the manifest directly is
E0901 manifest edited outside zone command.21.4 Manifest
onus.tomlat the repository root:[zones] "app.core.*" = "critical" "app.reporting" = "hardened" "app.payments.*" = "draft" default = "draft" [zones.exceptions] "app.core.checkout" = { third_party_assumes = ["vendor.payments.charge"] }Patterns match module names; the most specific match wins.
defaultapplies to modules not matched. A new module isdraftunless the manifest says otherwise.21.5 Reporting
The interface document, path report and ledger carry the zone of every item. A path report additionally lists the zones it crosses and every hardened-item-in-draft-module it depends on. The review tool renders zones as regions, with draft regions visibly distinct, and the promotion history of each module.
Grammar (§2.3)
visibility = [ "pub" ] [ "hardened" ] [ "sealed" ] ;hardened as a visibility modifier is permitted only on pub items in draft modules; elsewhere it is E0902 hardened modifier outside draft zone. hardened is a reserved word.
Implementation spec
§2 Layout. Add zones/ under compiler/src/: manifest parsing, zone resolution, dependency rule, policy bundles.
§4 Passes. Add pass 12a, after paths: zones — resolve every module’s zone from the manifest, check the dependency rule (E0900), apply zone policy bundles (feeding the same checks paths and policies already run), record zone per item for reports.
§7 Reports. interface.json items gain zone; path.json gains zones_crossed and draft_dependencies; ledger gains promotions (module, from, to, audit result, timestamp) and conditional flags on obligations resting on demoted modules.
CLI. onus zone promote, onus zone demote, onus zone show. Promote depends on the loop for the regeneration audit; before the loop exists it runs the audit’s static half only (policies) and records that the body-regeneration half was skipped.
Milestones. Add M14 — Zones. Manifest, dependency rule, policy bundles, hardened modifier, zone fields in reports, onus zone commands (static half). Accept: the checkout example split into app.core.* critical, app.reporting hardened and a new app.payments draft module with one hardened item that app.core.checkout depends on; a dependency on a non-hardened draft item fails E0900; demoting app.reporting marks the reporting path conditional.
Add to M10 (review tool): zones as regions; promotion history.
Loop spec
§3 What the model sees. Context policy defaults by zone: draft → scope, and the model may also see the conversation history for the module (design mode); hardened → module; critical → none.
§1 and §5. The rule “the loop never edits claims” applies in hardened and critical. In draft the loop may edit interfaces directly and proposals are unnecessary; the ledger records the edits as loop-authored so the promotion audit can find them.
§4.1 Escalation. In critical, escalation goes to the frontier model on the first stall, and widen_effects proposals are never emitted — an effect widening on a critical module is a human decision from the start.
§8 Regeneration audits. The audit is the promotion mechanism; its standard is the target zone’s policy bundle.
Codebase now
The manifest format and the dependency rule can be implemented as soon as M8 is done, since they reuse policy checks. The hardened modifier is a one-token grammar change; do it with the next grammar touch rather than separately.
Not changed, but decided
- Zones are per module, declared in the manifest, changed only by command. There is no per-function zone; the
hardenedmodifier is the sole finer grain, and it exists only to expose a boundary from a draft module. - Nothing at a higher zone rests on a claim from a lower one. This is the invariant everything else serves.
- Promotion is earned by the regeneration audit; there is no manual override.
Spec changes
Changes to onus-spec-v0.md forced by implementation, by milestone. Each is
marked in the spec with <!-- changed: reason --> and pinned by a fixture.
Changes the spec author makes are logged in CHANGE-LOG.md and its sequels, dated, with
what the codebase must do about each; this file records only what
implementation forced.
M1 — front end
Grammar (§2.3)
The provisional EBNF was made LL(1) and brought into line with the spec’s
own examples. The grammar as implemented is grammar-v0.md. Differences:
- Continuation newlines. A newline is not significant before
->,else,{,claims,requires,ensures,invariantordecreases. The EBNF placedNLtokens insidefn_decl,iface_itemandloopin ways that did not tokenise consistently (e.g. a signature followed by contracts needed two consecutive newlines). Fixtures:roundtrip/messy/continuations. - Single-line blocks.
blockaccepts{ stmt }on one line (the §18.3 example writesif ... { return Err(Empty) }); the canonical form is always multi-line. Fixture:roundtrip/messy/single_line_block. inoutposition.param = NAME ":" ["inout"] type, matching §4.1’sgrid: inout Grid[T, w, h]and the call-site formgrid: inout grid, which is now also in the grammar (call_args). The EBNF hadinoutbefore the name. Fixtures:roundtrip/14_fn_signatures,roundtrip/23_expr_postfix.- Labelled and explicit type arguments.
targ = [NAME ":"] (type | expr)forDb[ReadOnly, schema: "orders"](§8.2), and a call may carry explicit[...]arguments forsql.select[text: "..."](...)(§18.2). Fixtures:roundtrip/23_expr_postfix,roundtrip/34_types. - Ranges.
a ..< bis a domain form offorand of quantifierinclauses (§5.1, §5.3), not an expression. Fixtures:roundtrip/19_for,27_quantifiers. impliesandis.impliesis the lowest-precedence, non-associative operator (§3.6 laws);x is Patternsits at comparison level (§3.8.1). ChainingimpliesisE0011. Fixtures:roundtrip/22_expr_logic,syntax/e0011.- Claim predicates. A derived claim’s body is the small effect-predicate
language of §6.3 (
effects == { ... }, effects, claims,and/or/not), not a general expression. Fixture:roundtrip/09_claims. - Policy scopes.
outside { self, std.* }isscope = "self" | QNAME [".*"]. Fixture:roundtrip/12_policy. - Test modules and
fake.["test"] "module"and thefake QTNAME { ... }primary of §8.4 are in the grammar;fakeoutside a test module isE0012. Fixtures:roundtrip/31_test_module_fake,syntax/e0012. recoveras an effect name in effect sets, forforbid { recover }(§10.2). Fixture:roundtrip/11_path.- Effect lists inside parameter lists.
fn(T) -> U ! e, xs: List[T]is ambiguous; a comma followed byNAME ":"ends the effect list, and effect names are lowercase (QNAME), never claims. Fixture:roundtrip/34_types. - Quantifier binder types have no
whereclause of their own;whereafter the binder belongs to the quantifier (§5.3). - Mixed
and/or(E0007) and chained comparisons (E0006) are parse errors as §2.1 requires. Fixtures:syntax/e0006,syntax/e0007. E0002(bare expression statement is not a call) is checked by the parser;example,propertyandlawblocks are exempt because their bare expressions are assertions (§5.2). Fixture:syntax/e0002.- Soft keywords. The spec’s examples use
of,requireandpathas names (Float.of,auth.require,path: "..."). Item and clause keywords that cannot occur inside an expression are therefore reserved only where an item or clause can begin. Listed ingrammar-v0.md. - Parentheses are not AST nodes; the printer emits the minimal set.
and/orare n-ary nodes. Fixture:roundtrip/messy/parens.
Lexical (§2, §3.1)
- Text literals are single-line. A raw newline in a text literal is
E0004; use\n. The §18.2 SQL literal is rewritten on one line. This keeps one canonical spelling per string value. Fixture:syntax/e0004. - Comments are preserved by the canonical printer (attached to the
line-level construct they precede or follow) and excluded from hashes.
Fixture:
roundtrip/29_comments. - Literal normalisation in canonical form:
_separators dropped, durations in the largest exact unit, floats in shortest form. Fixture:roundtrip/messy/literals.
Canonical form (§2.2)
- The layout rules are stated precisely in
grammar-v0.md(“Canonical form”). Notably: bracketed lists break one element per line at 100 columns,else ifcanonicalises to a nested block (as §2.3 already said), and blank lines inside blocks are removed.
Named arguments (§5, §10.1, §18)
Ok(x)/Err(e)in the prose and examples contradicted “arguments are passed by name at every call” and thecall_argsgrammar. The examples now writeOk(value: x)andErr(error: e);Result’s fields arevalueanderror. Fixtures: the three worked examples.
Claims are type names (§6.3, §9, §18.3)
- §2 and the grammar make claims
TNAMEs; the §6.3 examples used lowercase (pure,total). The examples now readPure,Total,RealtimeSafe, andrequire { Total, Idempotent }.
Capabilities (§8)
capability Db[mode: DbMode]is writtencapability Db[const mode: DbMode]per thetparamsgrammar, andmode in { ReadOnly, ReadWrite }is writtenmode == ReadOnly or mode == ReadWrite(there is no set-membership expression in v0). Fixture:roundtrip/10_capability.
Diagnostics (§13)
location.defisnullfor a diagnostic outside any definition (e.g. a malformed module header).
M2 — resolve and types
- Intrinsics (§3.12, new).
intrinsic fn(no body) andintrinsic typedeclare runtime-provided primitives, legal only undermodule std.…(E0102elsewhere). Their contracts and effects are assumed obligations in the ledger (§12.2 extended). Chosen over a hardcoded primitive table (contracts would live outside Onus) and over a generalextern(the FFI §17 defers). Fixtures:roundtrip/35_intrinsic,syntax/e0102,syntax/e0003_intrinsic_with_body. - Function types carry parameter names (§3.7).
fn(x: T) -> Urather thanfn(T) -> U: calls are named, so a call through a function value needs labels. A closure assigned to a function type may use its own parameter names. Fixtures:roundtrip/03_type_alias,28_closures,34_types.
Modules and resolution (§11, §3.4, §3.6, §3.10)
- Module files.
a.b.clives at<root>/a/b/c.onus;std.*lives under the standard library root and no other file may declare astd.*name (E0112). A file declaring a name other than its path isE0104; an import that finds no file isE0103. The root is--rootor is inferred from the entry file and its module name. Fixtures:checker/e0103,e0104,e0112. - Prelude. Every module implicitly sees the public types and variants
(not the functions) of
std.results,std.option,std.list,std.grid,std.map,std.int,std.float,std.text,std.bool,std.bytesandstd.duration. These implicit imports are type-only and are not edges for cycle detection. TheResultmodule isstd.resultsbecauseresultis a keyword. - Companion functions.
T.fdenotes functionfof the module that declaresT; for a primitive, that module isstd.<lowercase name>(Int.to_text→std.int.to_text). Functions are never in scope unqualified across modules. - Variant scope. A bare variant resolves in this module’s unions, then
the prelude’s, then the imports’ public unions; more than one candidate is
E0108and must be qualified with the module alias. Two unions in one module may not share a variant name (E0107), since there is noUnion.Variantsyntax. - Module aliases win in dotted names.
auth.require(...)denotes the module even when a parameterauthis in scope (§18.3 relies on this); a bareauthis the parameter. Aliases live in their own namespace. - No shadowing. A local may not reuse the name of another local or
parameter (
E0113). Module-level functions and constants may be shadowed, because parameters are the labels callers read and the spec’s own API pairsselect(..., statement:)withsql.statement. Fixture:checker/e0113. - Examples and properties share a namespace separate from functions, so
example escape_countmay accompanyfn escape_count(§18.1); paths and policies likewise. Unitis a built-in value;TypeInfoandSpecare nameable types.- Interface dispatch is written
Ord.compare(a: x, b: y): the interface’s type parameter is instantiated from the arguments (or an explicit argument) and animplmust exist (E0333) unless the type is a parameter bounded by that interface. Inside an interface or impl its functions are in scope bare. Fixture:checker/e0333.
Typing (§3, §4, §5, §10)
- Generic instantiation takes type arguments from an explicit
[...], then from the expected type in checking position, then from the arguments; an unbound parameter isE0324. This is instantiation, not inference onto declarations. Fixture:checker/ok_types,e0324. - Type indices (
Grid[T, width, height]) must be literals,consts or parameters; at a call whose result type uses a parameter as an index, the argument must be such an expression (E0337). Fixture:checker/e0337. - Capability restrictions. Labelled arguments beyond a capability’s
declared parameters (
schema: "orders") are restrictions; a capability with more restrictions is accepted where one with fewer is required (§8.2). This is the one subtyping rule beyond refinement subsumption in impl spec §3.3. - Expression statements must have type
Unit(E0339): a discardedResultis never silent.example,propertyandlawbodies are assertions and must beBool. - Unreachable code is an error, not a warning: a statement after a
returnon every path (E0332) and amatcharm no value can reach (E0327). Fixtures:checker/e0332,e0326_e0327_match. recoverblocks yield the value of their final expression statement and may notreturn;Panickedis a record instd.results.- Closures may not capture capabilities (
E0330), in addition tovars andinoutparameters (§3.7). Fixture:checker/e0330_capture_capability.
M3 — effects
Function-level
decreases(§5.1). Recursion needs a measure but the grammar only haddecreasesas a loop clause; it is now also a contract clause of a function (decreases nafterrequires/ensures). A recursive cycle whose functions lack one and do not declaredivergeisE0320. Fixtures:roundtrip/14_fn_signatures,checker/e0320.Resource effects are declared by
grants(§6.1, §8).sql.readis the effectreadof modulesql, declared by a capability in that module grantingsql.read; it is spelled the same everywhere and is reachable only where that module is imported. Any other effect name isE0202. The primitive set stays closed.mutateis about the caller’s own parameters (§6.1). A function needsmutateiff it assigns to or passes on one of itsinoutparameters; a callee’smutatedoes not propagate through a localvar(Mandelbrot’srendercallsGrid.setwithmay alloconly).What allocates (§6.1). List literals,
++and closure creation arealloc; records and variants are values and are not. Aloop whilewithoutdecreasesisdiverge.recoverabsorbspanic.Effect polymorphism (§6.2). Passing a function value to a parameter of type
fn(...) -> U ! ebindseto the value’s effects beyond those the parameter lists; the call contributes the callee’s effects withesubstituted. A function value may not flow into a function-typed position (binding, argument, return) declaring fewer effects (E0201). Fixtures:checker/ok_effects,e0201_fn_value_flow.Purity of contracts. A
const fndeclares no effects;requires,ensures,decreasesandwhereclauses may allocate and nothing else.Impl effects (§3.6). An impl function declaring effects beyond the interface’s is
E0334. Fixture:checker/e0334_impl_effects.Examples completed (§18.2, §18.3). The reporting and checkout examples referenced modules and functions the spec did not show;
app.config,app.auth,vendor.payments,Request,Order,Basket,load_basket,record_orderand theno_third_party_assumespolicy are now inexamples/, andReceiptispayments.Receipt.Contract conveniences (§3.9, §5.3). A bare variant in a pattern (
result is Ok,| Ok ->) matches any payload, likeOk(..); and a quantifier whose domain has typeResult[List[T], E]orOption[List[T]]ranges over the contained list and is vacuously true forErr/None. Both appear in the spec’s own examples (§3.8.1, §18.2, §18.3).
M4 — const evaluator
const fnmay allocate (§3.8.1). The spec’s ownparse_selectreturns an AST, which allocates; aconst fntherefore declares at mostalloc(M3 item 48 narrowed). Its signature in §3.8.1 gainsmay alloc.ConstErrorandTypeInfoin the library (§3.8.1).ConstErroris the recordstd.check.ConstError { offset, message }(prelude);offsetindexes the graphemes of the constant text. Aconst fnreads a type throughstd.typeinfo(TypeInfo.name,TypeInfo.fields), whose intrinsics exist only at check time.Specvalues wait for the verifier.- When check-time checks run. At a call whose arguments are all
constant, the callee’s
requires provedclauses are evaluated; false isE0700, located at the offending grapheme of the literal passed for the callee’s firstconstText parameter when aConstErrorwas produced. Clauses with runtime arguments are left to the verifier.selectno longer needs.ok:columns_matchtakes the text and the row type. - Check-time failures. A contract failing or an intrinsic panicking
during evaluation is
E0701; aconstthat is not evaluable isE0701; exceeding the step budget isE0501, naming the function. - Examples at check time (§5.2). An
examplewhose statements are all evaluable (pure functions, constant values) runs at check time and a false assertion isE0702; one that needs the runtime is deferred to the generated tests of milestone 5.
M5 — codegen, everything checked
Obligations are objects (impl spec §3.5). The contracts pass creates one per site of §12.1 with status
checked, exceptrequires provedclauses the const evaluator discharged (proved). Codegen inserts a runtime check iffchecked.Where checks live. A callee checks its own non-pinned
requiresand parameter refinements on entry, on behalf of every call site; call-site refinement obligations stay in the ledger but emit no second check.ensures, the return type’s refinement,let/var/assignment flows, record and variant field refinements, loop invariants anddecreasesare checked at their sites. Int and Duration+ - * / %go through checked runtime arithmetic (overflow).inoutconvention (impl spec §6). A function withinoutparameters returns[result, ...parameters]and the caller reassigns its variables; intrinsics follow the same convention (Grid.set). Intrinsic shims passconsttype parameters first, then parameters, positionally.tryunwinds with an exception (EarlyReturn) caught by the enclosing function, instead of the impl spec’sif (r.tag === 'Err') return r;, so that atrynested inside a larger expression keeps evaluation order.matchis a labelled block of pattern tests in arm order, which is how guards fall through.Generics and interfaces. Type parameters are erased; a bounded parameter
T: Ibecomes a hidden dictionary argument andI.f(...)dispatches through it; impl functions are emitted asI$Type$fand each impl exports its dictionaryI$Type.Tests and
main. Everyexample,propertyandlawbecomes a vitest case in<module>.examples.test.js(properties and laws under fast-check generators derived from parameter types and filtered by their refinements).onus runemits a launcher that constructs the root capabilitiesmainnames (§8.3) and mapsOk/Err/Panicto exit codes 0/1/2.std.sqlat runtime has no driver in v0:connectreturnsErr(Connection).Function values are positional at runtime. A closure takes its parameters positionally, a call through a function value passes the arguments in the type’s parameter order, and a declared function used as a value is wrapped in an adapter to its named-argument form. This is what lets a closure use its own parameter names against a function type (§3.7, item 26) without a runtime mismatch; the
tsc --strictoracle caught the original defect.
M6 — verification
Lowering (impl spec §7.1). Records and unions are not SMT datatypes: field access is an uninterpreted projection per type instantiation, a variant test compares an uninterpreted integer tag, lists have uninterpreted
len/get,Textis an opaque sort whose literals are pairwise distinct, and every call is an uninterpreted function per instantiation (a fresh constant when effectful) with the callee’sensuresand return refinement asserted about the result. A value’s declared refinements are facts, recursively through record fields, union payloads and list elements. Floats are opaque values; a float operation makes only the operand it appears in unknown.Path knowledge (§3.2.1). A body is walked once with fresh SMT constants per
varassignment;ifconditions,matcharms (with the failure of earlier arms), loop conditions and invariants inside loops, their negation and the invariants after exit,forranges and list membership, andtrysuccess are facts. Loops and branch joins forget the variables they assign. An early-returning branch leaves its negated condition in force afterwards.Constant discharge. An obligation without a solver condition whose predicate and inputs are constants (the
Viewportliteral of §18.1) is decided by evaluation; this is how float refinements over constants are proved.Statuses and codes.
unsat→ proved;sat→ checked, or for a pinned clausefailedwith the model as counterexample (E0302ensures,E0342requires);unknown/timeout → checked for unpinned nonlinear obligations, otherwiseE0501. The panic rule of §6.1 isE0343(a checked obligation in a function withoutpanic); aconst fnwith a checked obligation isE0703. Overflow obligations are exempt from both in v0: the ±2^53 range is the runtime’s assumption (impl spec §12.1) and they stay runtime checks.Codegen consumes statuses. A callee’s entry check for a
requiresclause or a parameter refinement is omitted when every call site proved it (whole-program), so Mandelbrot’s generated code carries no checks.CLI.
onus check --ledgerprints the obligations of the entry file with their statuses and provenance;--budget <ms>sets the per-obligation solver budget (default 500);--no-cachebypasses.onus/cache/;ONUS_DUMP_SMT=<dir>writes every problem for inspection.Checkout example (§18.3).
recent_orders’sensures forall o: Order in result: o.customer == who.idneeds theSpecmechanism and is commented out until it exists; its proof from the statement’swhereclause is the open item of item 53.Sequential solving. The impl spec (§7.2) runs obligations in parallel up to the CPU count; v0 runs one
z3 -in -smt2process at a time withspawnSync, relying on the proof cache for repeat runs. Mandelbrot, reporting and checkout verify in a few seconds each; parallelism is a performance item for later.
M7 — reports
- Elided bodies (§2.3, §11.1).
onus interfacemust render “canonical source syntax with bodies elided to{ ... }” and the rendering must be valid Onus, so...is a token and{ ... }is a function body the parser accepts (Block.elided). Outside an interface document it isE0115 elided body outside an interface document, reported by the resolver; the checker never sees an elided body. - Interface document shape (§11.1). Beyond the example in the spec the
document carries: every item of the module with its
visibility(private items included, since the ledger and the assumptions must be complete); a module-levelledgerof every obligation with status and provenance and moduleobligationstotals, both of which the prose of §11 asks for; afailedcount; on each contractpinned,sites(how many obligations the clause generated) andchecked_atas afile:line:colof the first runtime check; loopinvariant/decreasesclauses listed as contracts of their function;examples andpropertys reported under the function of the same name (§18.1) and as items of their own otherwise.hashisb3:+ BLAKE3 of the module’s canonical text. - Diagnostics (§13).
canonical_hashis filled for every diagnostic whose file has a canonical form.onus check --jsonprints one object per line.repairsare still only produced for E0001. - Schemas. JSON Schema (draft-07) for both documents lives in
packages/compiler/src/report/schema/; the test suite validates every fixture’s diagnostics and the three examples’ interfaces against them.
M8 — claims, capabilities, paths
- Claim participation (§7.1). “Participates in the relevant effect” is
given a definition: a callee participates in an asserted claim when it has
an observable effect —
io.file,io.netor a resource effect. The quiet effects (alloc,mutate,panic,diverge,nondet,io.env,io.clock,io.rand) change nothing an observer could see twice, so a callee with only those never has to carry the claim. Anassumecovers the function and everything beneath it. Intrinsics carry only what they declare, like their contracts (§3.12). Codes:E0203derived claim not satisfied,E0204asserted claim not propagated,E0205assumeof a derived claim,E0206assumeof an undeclared claim. - Capability rules (§8, §8.3). A record field of capability type is
E0601; apub fn mainparameter of a non-root capability type isE0602; a non-test module importing atest moduleisE0600.fakeoutside a test module was already the parser’sE0012. - Paths (§9). Reachability is breadth-first over calls in bodies,
closures included, with interface calls resolved on concrete receivers
through the impl table. Function values and dispatch on type parameters
are
E0410. New codesE0412–E0415for the bound,forbid,requireandpolicyclauses;E0411for a bound that allows a forbidden effect. Policy scopes:selfis the path’s module;std.*matchesstdand every module beneath it. - Path report (§9.1). Adds
effects.forbid,obligations.failed,ok, andpermitted_by∈ {"scope","except", null }.checked_atandconstructed_ataremodule.fn:line:col. Capability construction sites are every reachable call returning a capability, including attenuation; theirassumesare empty until the stdlib records connect-time assumptions.onus path <file> [<name>] --json. - Checkout example (§18.3). Under item 76,
handle_checkout’s claim requiresauth.require(network) andload_basket(sql.read) to carryIdempotent.Idempotentmoves to a sharedapp.contractsmodule (vendor.payments already imports app.auth, so app.auth cannot import vendor.payments);auth.requireclaims it, justified by having no participating callees;load_basketclaims it with anassumethat a select reads only. The path therefore lists three assumptions —load_basketandrecord_orderin the module’s own scope,chargepermitted byexcept— and “exactly one assumption” (impl spec §9, M8) is read as exactly one external assumption, which is what the reviewer is trusting on another party’s word. The spec’s “1 assumed” presumedstd.sqlderivesrecord_order’s idempotency from the statement (item 53), which v0 does not do.
M9 — onus next
- Constrained decoding (§14, impl spec §8).
onus next <file> --offset <n>takes a UTF-16 index (the implementation plan’s--offset, not §14’s--at file:offset) and returnstokens,expectedTypeandinScope. Tokens are the kinds the parser tests at the cursor after parsing the prefix; because the lexer drops a newline before a continuation token, a position after a newline reports the union of both tokenisations. Names in the vocabulary: keywords and punctuation as themselves,ident(names and soft keywords),type-ident,literal:int|float|text|duration,newline,eof. The expected type comes from aHoleexpression at the cursor with every open bracket and block closed after it; it is null when the cursor is not in expression position or nothing expects a type there (a bare statement start). Refinements are spelled out in the type text and not enforced. Locals in scope are listed outermost first; module items are not. v0 keeps no resident state between calls (impl spec §12, item 5). mayreplaces!(§2.3, §6). Requested by the spec author (docs/CHANGE-LOG.md, 2026-09-03):!reads as negation.mayis a reserved word and!is no longer a token (!=remains). Applied to the grammar, every example in the spec, the standard library, the examples and the fixtures.
M10 — the review tool
- Path report additions (§9.1, §15.1). The path view must draw the
reachable graph, and the tool computes nothing, so the report carries
graph.nodes(qualified name, module, entry/fn/intrinsic, effects, carried claims, obligation counts, assume and recover counts) andgraph.edges(caller, callee, effects at the site, location);gates(a sealed record type some reachable function returns and others demand as a parameter — the typestate of §18.3, drawn as the gate region);recovers; andledgerrows for every obligation of a reachable function. - Interface item locations (§11.1). Each item carries
at, so the review page can show an item’s canonical source when the reviewer opens a body; the interface itself still contains no bodies. - Interface diff (§11.1, §15.1).
onus interface <file> --diff <old.json>andonus review --against <old.json>compare two documents of one module. v0 decides compatibility textually: arequiresadded or anensuresremoved is breaking, the reverse compatible; a widened effect set or a changed signature line is breaking; new assumptions, recover sites and obligations that leftprovedare listed. Implication between clauses (a weakerrequireswritten differently) is not checked; the module is breaking when a public item is. Schema ininterface-diff.schema.json. - The review tool (§15).
packages/reviewis dependency-free and renders one self-contained HTML page from the reports (impl spec §12, item 4 resolved: no framework). Views: paths (graph laid out top-down from the entry, assume leaves in amber and recover sites in purple as the only colours, unresolvable calls as a break, gate regions shaded), interfaces (bodies collapsed to{ ... }, opening counted per module in the page), ledger (filterable by state, with assumptions, recover sites and capability construction sites), diff, and diagnostics with the solver’s counterexample.onus review <entry> [--out <dir>] [--against <old.json>]writesindex.htmlandreview.json. Not in v0: the path condition in the counterexample view, promotion drafts, and decisions or contract edits flowing back as tasks (§15.1); an invalid program’s page shows its diagnostics only.
Testing model (docs/CHANGE-LOG-02.md, applied 2026-09-04)
_intry ... else(§2.3). The verify example in §20.2 writeselse _: false; the binder may now be_, which binds nothing. The printer keeps it.verifyblocks (§20.2).verifyis a reserved word and a continuation token, soverify(...)on the line after anassumeattaches to it. A block is a definition of its own (kindverify, parent the function): it sees the module and its parameters, not the function’s locals; its calls are not the function’s (no false recursion); its obligations are its own and thepanicrule applies to it; it yields Bool, each bare expression being an assertion andtry ... else _: vyieldingv. Its declared effects must contain its body’s and may not exceed its function’s (E0207); its parameters must be capabilities (E0208).The environment (§20.2, §20.6).
onus test --assumptionssupplies each parameter from atest modulewhose public zero-parameter functions return capabilities —fakes in practice — named byonus.json(test.env) or--env;io.Files,io.Env,io.Netandio.Clockcome from the runtime when the environment gives none. A parameter with no source isE0603. Generated code exports each block asverify$<n>only in that mode, and a generated launcher runs them and prints the outcomes.The ledger (§20.3).
.onus/ledger/assumptions.json, keyed by module name and the BLAKE3 of the assumption’s canonical text; each record hasat,target,result,claim,def. The interface and path reports carryverifiableandlast_verifiedper assumption; the review page shows assumed, verifiedagainst or unverified.policy verified_assumptions_onlyis the compiler’s own policy name:E0416when a reachable assumption has no passing record younger thanonus.jsontest.max_assumption_age_days(default 7).onus test(§20.6) and the checkout example. Without flags it builds and runs the generated vitest suite;--mutatewaits for M13.charge’s assumption gained averifyblock that callschargetwice with one key;auth.requireno longer declaresio.clockit never used, so the block’s effects fitcharge’s;examples/checkout/test_env.onusandonus.jsonsupply the fakes. The block passes, and the path report lists it as verified.Verify blocks in the reports (§20.2). Bodies are elided from the interface, so the block a reviewer must read to judge a verification travels with the assumption: interface and path assumption entries carry
verify, the block’s canonical text or null, and the review page shows it under the assumption in the path, interface and ledger views.
M11 — native backend
One lowering, two emitters (impl spec §6). Code generation is now
lower.ts(checked AST plus obligation statuses → the target-neutral form inir.ts) and two renderers,js.tsandnative.ts. Every decision about what generated code does is made once in the lowering; the form is printed byonus build --emit irand pinned for the fixture suite intest/codegen/lowered/. The JavaScript output is unchanged in behaviour.Native representation (§19.1).
Int/Durationarei64,Floatisdouble,Boolisi1, and everything else is a pointer to an array of 64-bit slots (a variant’s tag first), so generic code and runtime primitives move slots and callers convert at the boundary.inoutparameters are pointers.provedobligations emit nothing;checkedones branch toonus_panic, whose message matches the JavaScript runtime’s;Intarithmetic uses the overflow intrinsics;tryis a branch that returns the error. The runtime ispackages/runtime/native/(onus.c,onus.h), compiled and linked byclangfrom the emitted.ll.FloattoTextfollows JavaScript’s shortest round-trip layout (§19.4).The v0 native subset. Programs reaching closures or function values, interfaces, runtime quantifiers,
recover,fake,TypeInfo,old(...)in a checked postcondition, structural equality on aggregates,Map,Bytes,sql, orTextoperations needing grapheme tables are refused withE0800 primitive unavailable on target, as §19.1 allows. Allocation is never freed (free-at-scope-exit is deferred).recoverviasetjmp/longjmpand theIntrepresentation obligations are M12.Differential testing (§19.5).
onus test --target allruns the examples on both targets and reports each disagreement asE0801; properties and laws run on JavaScript only.onus build --target nativewrites<out>/native/<module>andonus run --target nativeruns it. The C runtime’s-DONUS_BROKEN_INT_TO_TEXTexists for the acceptance test that E0801 fires on a broken primitive.M12 names the JavaScript
sqlimplementation. M12’s text listed only the C runtime’slibpqprimitives, but its acceptance (all three examples agree on both targets; reporting runs against Postgres) needssqlreal on the JavaScript side too, which impl spec §5 has always described overpgand which v0 shipped as a stub (item 62). The milestone now says so, and takesrecoverfrom M11.
M12 — targets complete
- Host claims (§19.2).
std.hostdeclares the asserted claimsjs,nativeandwasm; a claim’s name may be lowercase (grammar §2.3) so they read ashost.js. The JavaScript-only intrinsics (Text.len,graphemes,bytes,lower,trim,Map.*,Bytes.len,TypeInfo.*) carryclaims host.js; the native emitter refuses any reached function carrying it (E0800). Aforbidclause may name claims as well as effects: a reachable function carrying one isE0413. Derived-claim predicates may name a claim by a lowercase qualified name. - Representation obligations (§19.3). Every
IntorDurationparameter andlet/vargets an obligation of kindrepresentation, proved when the binding’s declared type keeps every value within ±2^53 - 1 and otherwisechecked. They are reported in the ledger and exempt from thepanicrule like overflow. The slow path is not implemented: a checked binding keeps the number representation, and the existing overflow checks panic rather than switch to arbitrary precision. The ledger says which values that concerns. std.sqlon both targets (§8.1, §18.2; impl spec §5). JavaScript:sql.tsoverpg, driven synchronously by a worker thread withAtomics.wait(Onus calls are synchronous;pgis not). Native:onus_sql.coverlibpq, found throughpg_config, Homebrew’s keg orONUS_LIBPQ; without itstd.sqlisE0800.connect(mode: ReadOnly)setsdefault_transaction_read_only = on, verifies it, and refuses a superuser role, which could not be held to it; the remaining assumption is named at the construction site in the path report.restrictsets the search path,deadlinethe statement timeout (Err(Timeout)).- Row decoders (§18.2). For each
sql.selectwhose row type is a record the compiler generates a decoder in the target-neutral form (decoderon the call,rejectstatements): one column per primitive field, then the record’s refinements; a failure isErr(Refinement)with the row and column, a missing or ill-typed columnErr(Malformed). JavaScript passes it as$decode; natively it is a generated function the C runtime calls per row. recovernatively (§10.2). The body becomes a function over the enclosing locals’ addresses, run undersetjmp; a panic insidelongjmps back and becomesErr(Panicked { obligation, location })with the same texts as the JavaScript runtime.- WebAssembly (§19).
--target wasmcompiles the same LLVM IR with a WASI SDK (WASI_SDK_PATHor/opt/wasi-sdk) toprogram.wasmand writesrun_wasm.mjsfor Node’s built-in WASI;std.sqlisE0800there. No SDK was available where this was written, so the path is untested end to end;onus build --target wasmreports the missing SDK. - Differential harness (§19.5). Every fixture and example with
exampleblocks is built for both targets: those in the native subset must agree on every example, the rest must be refused withE0800. The SQL tests run against a Postgres atONUS_TEST_DSN(default: thepostgres:17Docker container with passwordonus) and skip with a notice otherwise.
M13 — contract mutation and coverage
- Assertion obligations (§5.2, §20.4). Every bare Bool statement of
an
example,propertyorlawbody is an obligation of kindassertion: proved when the contracts of what it calls entail it, and otherwisechecked“run as a test”. A proved assertion is a fact for the assertions after it. Tests are not functions, so these are exempt from the panic rule. Lowering test bodies exposed a contract that calls its own function (ensures compare(a: a, b: a) == 0); the verifier now states such a contract once instead of unfolding it forever. - What “detected” means (§20.4). Weakening a contract never changes a
body, so re-running the tests cannot notice it. A mutation of an
ensuresclause, a result refinement or a record field refinement is detected when an assertion the verifier proved from the contracts stops being provable without the clause: the test restates what the clause promised. Negating a property’s guards is the one dynamic mutation: the property is re-run over the complement of its domain and detects the mutation by failing.onus test --mutateprints one row per mutation,M0001 undetected contract weakeningfor the survivors, exits 0, and writes.onus/ledger/mutations.json, which the reports read. Static mutations need z3 and are skipped with a notice without it. - Two of §20.4’s mutations are not applied. Laws are not dropped: a law is itself the only test of the interface clause it states, so its absence could never be detected and every law would be reported. And parameter refinements are not widened: accepting more inputs is a stronger promise by the callee, not a weaker one, and nothing a caller’s test asserts can depend on it. Result and field refinements are widened.
- Obligation coverage (§20.5). The runtime records a hit per check
reached when
ONUS_COVERAGE_DIRis set; the generated test file writes them after its tests, since test runners end their workers without running exit handlers.onus testmerges the hits into.onus/ledger/coverage.json, keeping the larger count per check across runs, and prints the coverage line.interface.json,path.jsonand the review page carryobligation_coverage: proved; checked and how many of those a test reached; assumptions, verifiable and verified; and mutations detected and surviving. Representation obligations have no runtime check and are not counted as checks. Coverage is measured on the JavaScript target only. - The acceptance test is pinned on Mandelbrot. The
ensuresonrecent_ordersstays deferred (item 36), so the milestone’s acceptance is droppingensures result <= limitonescape_count, whichproperty escape_boundeddetects, and widening the result refinement of a fixture function no test restates, which survives and is reported. The build directory foronus testis resolved to an absolute path, which vitest needs, and the mutated programs are written beside it inout-mutateso the program’s own test run does not see them.
M14 — regeneration loop
- The loop package (loop spec §1, §10).
packages/loopwithonus-loop run <task.json>;onus loop runforwards to it, since the compiler cannot depend on a package that depends on the compiler.watchneeds task intake and is not in v0. Model access is one interface with three implementations: scripted, for the tests; Claude Code as a subprocess (claude -p, the nested-session markers stripped from its environment); the Anthropic Messages API overfetch, which could not be exercised here for want of a key. The constrained-decoding hook of §3.7 is declared and supplied by nothing. - The context (§3). Assembled through the compiler library, never
from an import’s source: the targets with bodies elided and the
examples and properties that name them; the interfaces of every module
in scope and every import; sibling bodies per the context policy; every
diagnostic of the last check as §13 JSON, except
E0115on a target, which is the task itself; failing examples with their text; counterexamples from the task and from the diagnostics; the standard library interfaces the targets’ types select. The one fixed text describes the rules and Onus syntax, which is language knowledge, not a convention. Simplification: diagnostics are not narrowed to callees; everything in scope is shown. - Never a claim (§1, §4). Model output is parsed as Onus. A target
whose signature, contracts, effects or claims differ from the baseline
is refused with a note; a second refusal is out of scope, with a
proposal built from the difference. An added function is refused once
the same way, since helper introduction (§4.1 step 3) is off. Only
bodies are spliced, under the target’s own signature, and the file is
put in canonical form so
E0001never reaches the model. Mechanical repairs apply only to spans inside a target body. - Classification and the ladder (§4, §4.1). A stall is an outcome
identical to an earlier one, or one that grew twice running. A contract
conflict is a counterexample against a target’s clause with the same
body proposed twice; it ends in a
weaken_postconditionoradd_preconditionproposal carrying the counterexample. The ladder walks full history, then a wider context policy, skips steps 3 and 4 as configured off, and stops. Examples are evaluated at check time (E0702), so a wrong body usually fails there before the verifier runs; the loop treats those like any other diagnostic. - Changes (§6).
.onus/changes/<task>/change.json: the interface diff per module in scope (empty by construction when the baseline had diagnostics, as animplementbaseline always has, since only target bodies are spliced and signatures are compared textually), the ledger delta, the body diff, the trace, metrics, proposals and audit findings. A blocked report adds the cause, the last diagnostics and the best attempt, and the working tree is left as found.onus reviewgains a Changes view, proposals marked proposed by loop. - Regeneration audits (§8). Findings are
obligation_regressedandexample_failed, each becoming a proposal (add_example,add_claim). Bodies that differ in callees are not reported: the interface documents carry no call graph. - A live run. With Claude Code as the model,
escape_countwas regenerated from its interface alone and was green on the first iteration in 41 seconds, with the loop invariant and measure intact. The test runs only withONUS_LOOP_LIVE=1. - Deferred. Production feedback (§7),
onus loop watch, and the per-repository aggregates of §11; metrics are per change. - OpenRouter and key files. A fourth model,
openrouter[:<model>], over the chat-completions protocol; the default model isOPENROUTER_MODELordeepseek/deepseek-v4-flash, chosen on the results in item 119;moonshotai/kimi-k2.7-codeis the alternative. The CLI reads.envand.env.localfrom the project root and the current directory, never overriding the real environment, so keys stay off the command line; both files are ignored by git. The live test takes its model fromONUS_LOOP_MODEL. - What running four open-weight models taught the loop. (a) A
provider that never answers hangs the task; API requests now time out
after three minutes and the task ends as a model error. (b) Each
iteration’s prompt and answer are kept in the change’s work directory
(
change.jsonstill carries only the prompt hash, §6), so a blocked task can be read. (c) A blocked report’slast_diagnosticsare the last iteration’s, including syntax diagnostics of an answer that never parsed. (d) When an answer does not parse, the notes quote each offending line and the tokens the grammar admits there, fromonus next’s machinery (§14): a model that does not know Onus writeswhile, is told “expected an expression”, and writeswhileagain; told that a line may start withloop, it has what it needs. Results on the Mandelbrot task: DeepSeek V4 Flash and Kimi K2.7 Code green on the first iteration (5 s and 20 s), GLM 5.3 Flash green on the second (98 s), Qwen3 Coder Next blocked on the budget without a parseable answer before or after (d), and Claude Sonnet 5 through OpenRouter green on the second (15 s) after a syntax slip on the first. The OpenRouter default is DeepSeek V4 Flash on these results. The runs are logged indocs/BENCHMARK.md, andpackages/loop/bench/run.mjsappends a row per model so the log can be revisited.
M15.0 — prerequisites for the compiler in Onus
- Recursion measures are obligations (§5.1). The effects pass
records every recursive cycle; the contracts pass puts a
decreasesobligation at every call within a cycle (the callee’s measure over the arguments strictly below the caller’s measure taken at entry) and one at entry (the measure non-negative); the verifier discharges both; a checked one becomes a runtime check on both targets, the arguments bound to temporaries so they are evaluated once. Before this, a recursive function only had to declare a measure. Fixtures: direct, mutual, structural over a list throughslice’s length contract, and a measure over record fields, all proved; a measure that grows, checked and panicking on both targets. - A cycle shares one measure (§5.1).
E0320now also fires when the functions of a cycle declare measures that differ once parameters are numbered by position, which is the spec’s “same expression up to renaming” made mechanical. - The standard library grew (§16, provisional).
std.text:count,code_points,of_code_points,of_code_point,slice,index_of,contains,ends_with,split,join,repeat,replace,compare,upper, positions counted in code points (lenstays grapheme-based and JavaScript-only); an empty separator splits into code points and an emptyfromleavesreplacealone, since a refinementcount(t: it) > 0on those arguments cannot be proved for a literal.std.int.parseandstd.float.parsereturnOption.std.list:Builder[T]withbuilder,push,built,finish, andmap,filter,fold,index_of,contains,reversewritten in Onus.std.io:read, and aConsolecapability withprintandeprint, a fifth root the runtime supplies tomain. Every new function has contracts and examples; the examples run at check time, as generated tests on JavaScript, and natively through the harness. A builder is a value the runtime shares: bind it once and push through that binding (a rule the type system does not yet enforce). - Structural equality natively (§19.1). The native emitter
generates a comparer per concrete type: primitives by value,
Textby the runtime, records field by field, unions by tag then fields, lists element by element throughonus_rt_list_eqwith the element comparer. Equality on a value of a type parameter staysE0800, soList.index_ofandList.containsare JavaScript-only, like the closure-taking combinators. - Two runtime fixes the library work exposed. A native file write
is flushed at once, so a read after a write sees it as on JavaScript;
and the JavaScript emitter gives each
inoutcall its own temporaries, since two such calls on one variable in a block redeclared them. A program’s build no longer emits the standard library’s own test files, which the library’s examples had just introduced. - Deferred from M15.0.
Mapon the native target and theProcesscapability for z3, which the checker and verifier stages need, not the front end; the stack-depth story, which the parser stage will settle.
M15.1 — the front end in Onus, first part: the lexer
- The lexer in Onus (
self/).self/tokens.onus,self/lexer.onusandself/lexdump.onus: the token stream oflexer.tsreproduced, positions in code points rather than UTF-16 units, every loop and helper contracted so that the verifier proves termination and every index obligation; the file checks with no diagnostic and nomay panic. The differential test runs the dump program over every.onusfile in the repository and compares it with the TypeScript lexer’s stream, tokens, comments and diagnostics alike, and the same natively on Mandelbrot. One agreed-on limit: past 2^53 the JavaScript runtime’sIntcannot hold a literal exactly (item 99), so the dump compares digits as written and the value stays a known gap. - What writing it taught the compiler. (a) The verifier now gives the
right operand of
and,orandimpliesthe left operand (or its negation) as a fact, soi < n and List.get(xs: xs, i: i)proves its index. (b) A float literal lowers to an opaque constant instead of sinking every obligation around it; an obligation the solver cannot settle is still tried by constant evaluation afterwards. (c) The native emitter labels every function’s entry block, without which a short-circuit as a function’s first statement referred to a block that did not exist; it emits aggregate and computedconstitems as slots filled on first use, and builds compile-time lists, records and variants. (d) Grammar facts a model would also need: constants are names, not upper-case; a multi-statement match arm needs braces; there is no conditional expression; patterns bind a variant’s field by its own name and may not rename it; a name from an imported module is always qualified by the alias. - Still to come in M15.1. The canonical printer in Onus, then
onus fmtreimplemented and the byte-identical acceptance.
M15.1 — the front end in Onus, second part: the parser
- The parser in Onus (
self/ast.onus,self/parser.onus,self/astdump.onus). The syntax tree ofast.tsas records and recursive unions (field names that are reserved words carry a suffix or prefix:ty,where_,is_pub), and the recursive-descent parser ofparser.tsreproduced: the thrownParseErrorbecameResultandtry, with recovery at statement and item boundaries; the parser state is one record passedinout, its position bound by the record’s own refinements; and every function of the recursive cycle takes arankand shares the measure(tokens left) * 64 + rank, so a call at a lower rank, or after a token was consumed, is strictly smaller. The verifier proves the whole parser: 160 measure obligations, 287 postconditions, every index. Not carried over: the cursor and hole ofonus next(§14). A dump program prints the tree one node per line, spans in code points, and the differential test compares it with the TypeScript parser’s tree on every source in the repository, syntax diagnostics included; they agree on all of them. - What the parser taught the compiler. (a) The verifier’s join after
an
ifforgot everything a branch assigned; it now keeps each branch’s new facts under that branch’s condition and relates a joined variable to each branch’s value, which is whatif p.pos < n { p = { p with pos: p.pos + 1 } }needs to keepp.toksknown. (b) The code generator snapshotted everyold(x)at entry whether or not any obligation would read it; a recursive-descent parser copying its token list on every call ran in quadratic time (76 s on the lexer’s own source, now 70 ms). Snapshots are taken only for contracts some obligation of which is checked at runtime. (c) Atrythat returned early from a function withinoutparameters returned the bare value on JavaScript, not the value with the parameters; pinned bytest/codegen/inout_try.onus. (d) More language facts a model needs:itis reserved even as a variable name; a call’s non-Unitresult may not be discarded, so token-consuming helpers come inUnitflavours;old(...)is not allowed in a loop invariant, so a loop binds what it needs before it; a pattern cannot shadow an enclosing binding, so nested matches on twoOptions move into a helper. - Contract shapes that proved.
advancepromises a step only below the last token, since the stream’s finaleofis a lexer invariant the record does not state; the helpers that consume exactly one token sayresult implies p.pos == old(p).pos + 1; every production that always consumes saysresult is Ok implies p.pos > old(p).pos, which is what the loops’ measures and the up-rank calls rest on. - Still to come in M15.1. The canonical printer in Onus, then
onus fmtreimplemented and the byte-identical acceptance.
M15.1 — the front end in Onus, third part: structural recursion and the printer
- Structural measures (§5.1, spec change).
decreasesmay name a value of a record, union or list type, meaning the structural order: at each recursive call the argument must be a proper part of the measure at entry, reached by pattern matching, field access orList.get. The verifier decides this from the terms themselves (a pattern field is a projection of the scrutinee, an element read is a projection of the list) and marks the obligation proved by the structural order; otherwise it reportsE0344and never falls back to a runtime check, since no general size exists to compare. No entry obligation is generated for a structural measure. Every walk over a syntax tree in the compiler in Onus needs this; the dump program declareddivergeuntil now. - The document renderer and comment attachment in Onus.
self/doc.onusisdoc.ts: the sameDocforms, the samefitsandrender, driven by aBuilderused as a stack (List.atandList.pop, added tostd.listfor it).self/comments.onusiscomments.ts: the site walk over the tree, keyed by node kind and span, the leading, trailing and dangling sets, and the same choice of owner for every comment. One known divergence: the renderer measures width in code points where the TypeScript renderer counts UTF-16 units, so a line holding a character outside the basic plane could break differently; no source in the repository has one. - The printer and
fmtin Onus.self/printer.onusisprinter.tswith one structural change: where the TypeScript printer has anoperandhelper, the Onus printer haswrap(d, paren)with the precedence test at each call, so that every recursive call passes a proper part of the node anddecreases <node>is proved for every walk.self/fmt.onusisonus fmt --stdout: read, lex, parse, attach comments, print; syntax diagnostics go to the error stream and the exit code is nonzero.packages/compiler/test/self/printer.test.tsbuilds it once and runs it over every source in the repository: a source without syntax errors must print byte-for-byte as the TypeScript printer prints it, and a source with syntax errors must be refused by both. All agree, which is the M15.1 acceptance. - Integer literals carry their digits.
tokens.IntLitandast.IntLitin Onus have atextfield besidevalue: the digits with underscores and leading zeros removed, which is what the canonical printer writes.valuealone was not enough:Inton the JavaScript target is a double (item 99), so a literal above 2^53 such as the9223372036854775807intest/roundtrip/21_expr_arith.onusloses precision in the lexer’s own arithmetic and printed wrongly. The same limitation means the lexer in Onus does not hold such a literal’s value exactly; it is therepresentationconcern of §19.3 and is left for the arbitrary-precision path. - What the printer taught the compiler. (a) A pattern binder
shadows a top-level function of the same name, and a call to the
function inside the arm is then
E0323 not callable: Expr is not a function; binders cannot be renamed (item 130), so the walkers areexpr_doc,block_doc,stmt_doc,params_doc,effects_docandpattern_doc. The diagnostic is correct but does not say why the name changed meaning; a better message is deferred. (b) A closure’s parameters put the parameter walk into the expression cycle, socollect_paramsneeds a structural measure too; the effects pass reports the cycle member without one (E0320). (c) An example that calls a generic function evaluates that function’s body at check time with its type parameter unsubstituted, so an intrinsic returningT(List.getinhead[T]) handed the evaluator a value it could not convert, and the pass threw (cannot convert a T, reported asE0999). Such a call is now not a constant: the example is left to test time, as any non-constant example is. (d)ensures result is Nonein a function returningOption[T]for a type parameterTlowered the variant test in the wrong sort and z3 rejected the query (E0999); the test is now lowered in the context of the scrutinee’s type. Both are pinned bytest/verify/ok_generic_is_in_ensures.onus, whose example reached the first and whose contracts reached the second. - No
divergeleft inself/.self/astdump.onusnow gives every walk the node as its measure (item 133) and dropsdiverge; the parser’s differential test is unchanged.
M15.2 — the checker in Onus, first part: loading, resolution and the type layer
- A structural measure may be passed on unchanged (§5.1, spec change).
The type checker in Onus, like the one in TypeScript, routes
expr(e)through helpers that take the same node and recurse on its parts, soexprcallscall(e), which callsexpr(a.value). The structural rule of item 133 rejected the first call: the argument is the measure itself, not a proper part. The verifier now classifies every call on a structural measure as strict (a proper part), equal (the measure itself, through aliases) or neither; strict calls are proved as before, neither isE0344as before, and equal calls are settled once every function is lowered: they are proved when the calls passing the measure unchanged form no cycle of their own, since every cycle then takes a proper part somewhere, and areE0344when they do. Pinned bytest/verify/ok_structural_helper.onusandtest/verify/e0344_equal_cycle.onus. Dictinstd.map.Map.putcopies the whole map on every write, which a checker’s definition and scope tables cannot afford.Dict[K, V]is an in-place table with value keys forIntandText, on theBuildermodel (bind once, write through that binding):dict,count,set,find,contains,remove,keysandvaluesin insertion order. JavaScript runtime and--emit tstype; pinned bytest/stdlib/map_ops.onus.- Nodes are keyed, not numbered. The syntax tree in Onus has no node
ids. Side tables are keyed by
defs.node_key(file, tag, span): the file, a syntactic class (expression, type, pattern, statement, item, signature part, other) and the span, packed into oneInt(a file is limited to 2^20 code points and a compilation to 1024 files). Two nodes of one class never share a span, so the key is unique; comment attachment already keyed sites the same way. A definition carries its declaring node (defs.DeclNode) so that later passes reach the declaration without a node table. - Loading and resolution in Onus.
self/report.onus(diagnostics and source files),self/defs.onus,self/context.onus(the compilation context, one record of tables that dicts and builders make writable through a copy),self/loader.onus(resolve/loader.ts: the module graph, the prelude,E0101,E0103,E0104,E0112, and pass 2 for every loaded file) andself/resolve.onus(resolve/resolve.ts: definition collection and every rule of §3.10 and §11).self/check.onusisonus checkin Onus: it runs the passes implemented so far and prints every diagnostic as one line.packages/compiler/test/self/checker.test.tsbuilds it once and runs it over every source in the repository against the TypeScript pipeline up to the same pass; codes, files, spans and order must agree, and they do up toresolve. - What the resolver taught. (a)
fn,claimsandmoduleare keywords, so they cannot be field names; the records usefn_def,claim_tableandmod. (b) ASome(value)arm inside anotherSome(value)arm is shadowing (item 130), so nested optionals go through small total accessors (or_neg,find_or) or one match per level in a helper. (c) The measure of a mutual recursion is a parameter position (item 121), so afuelmeasure must sit at the same position in every function of its cycle; the basic evaluator in Onus puts it first. (d) A dict inside a record is written through a local copy (var d = r.table; Map.set(d: inout d, …)), which the runtime shares; the verifier does not see the record change, so no contract speaks about a dict’s contents.
M15.2 — the checker in Onus, second part: types, constants and effects
- The type checker in Onus.
self/effectset.onus(effects/set.ts),self/types.onus(types/type.ts: the type representation, equality after stripping refinements, assignability, substitution, the type variable tests),self/basic.onus(consteval/basic.ts) andself/typecheck.onus(types/check.ts, with exhaustiveness fromtypes/exhaustive.ts). The port keeps the TypeScript structure, which item 139 made possible:check_expr(e)handsetoctor,call,binaryand the rest, and each recurses on the parts. One structural change: a refinement’s predicate is not checked where the type is elaborated but from a queue once the enclosing item is done, so that elaborating a type never re-enters the expression checker and the elaboration functions form a cycle of their own with a depth measure. The diagnostics are the same, in another order, so the differential test compares them sorted. - The constant evaluator and pass in Onus.
self/values.onus(consteval/values.ts),self/evaluator.onus(consteval/eval.tswithconsteval/intrinsics.ts) andself/consteval.onus(consteval/pass.tswithconsteval/offsets.ts). What TypeScript does with exceptions the evaluator does with aResultwhose error isNotConstF,PanicF,BudgetForReturnF, propagated bytry; integer overflow is caught withrecover, so a constant that leaves ±2^53 isE0701as before instead of a panic in the compiler. An intrinsic is evaluated by calling the standard library function it names, after its refinements are checked by hand so that the evaluator itself has nopaniceffect. Union results (Int.parse,Float.classify) are rebuilt from the union’s variants by name. - The effects pass in Onus.
self/effects.onus(effects/check.ts): sites, containment, closures, verify blocks, the flow of function values into function-typed positions, the call graph, Tarjan’s strongly connected components with a depth measure, and the measure keys of §5.1 computed over the printed measure.printer.print_expris the printer’s new entry point for it. - M15.2 acceptance.
self/check.onusruns load, resolve, types, constants and effects in the order ofdriver.ts, stopping at the first pass that reports as the driver does.packages/compiler/test/self/checker.test.tsruns it over every source in the repository against the TypeScript pipeline up toeffects; codes, files and spans agree on all of them. - A function named
eval(codegen bug). The JavaScript emitter renamed reserved words where a local was declared and where a function was referenced, but not where a function was declared, so a module withfn evalproduced a file Node refuses in strict mode. Every declaration site now goes through the same renaming. Pinned bytest/codegen/reserved_names.onus, whose functions are namedeval,deleteandnew. - What the checker taught. (a) A loop or a recursion whose measure
is a field of an
inoutrecord cannot be proved to decrease once the body passes that record to a callee, since the callee may change the field; a local counter written back afterwards is the shape that proves. (b) A counter declaredvar i: Int = 0losesi >= 0at a loop head; declaring itvar i: Int where it >= 0 = 0keeps it. (c) The evaluator’sEvrecord refinesstepsandbudgetto be non-negative for the same reason. (d) A function that takes the same node as a helper and re-matches it is the idiom that lets a largematchbe split, now that item 139 admits the call.
2026-09-05 — M15.3, first part: the verifier in Onus, and what porting it found
- A callee’s
ensuresabout aninoutparameter was a contradiction (verifier soundness bug). The lowering bound both a parameter andold(param)to the argument’s term, soList.push’sensures built(b: b) == built(b: old(b)) + 1becamebuilt(b) == built(b) + 1; and since a definition’s callee axioms are shared by every condition of its body, every obligation in a function that pushed to a builder was proved vacuously. Found when the verification-condition builder in Onus provedfuel < fuel. Now aninoutargument gets a fresh post-call term: the callee’sensuressees it for the parameter and the passed term forold(param), and the body walker re-binds the variable to it after the expression (§3.2.1: a call throughinoutis an assignment).verify/lower.ts(rebound,calleeFactswith pre and post bindings),verify/vc.ts(lower), and the same inself/lower.onusandself/vc.onus. Pinned bytest/verify/ok_inout_post.onus(the exit value is known through theensures) andtest/verify/e0343_inout_stale.onus(the value known before the call is stale, and nothing after the call is vacuous). - The
matchjoin keeps what each arm learned (verifier precision). After amatchwhose arms assigned a variable the walker forgot everything about it, unlike theifjoin; the parser’s loops overmatch parse_x(p: inout p) withhad only ever “verified” through item 150’s contradiction. The join now mirrorsif: a fresh constant per assigned variable, equal to each fall-through arm’s value under that arm’s condition (the earlier arms’ failure, its test and its guard), the arm’s facts under the same condition, and the disjunction of the fall-through conditions, which is sound because arms are exhaustive (§4.4).verify/vc.ts(match) andself/vc.onus(match_stmt). Pinned bytest/verify/ok_match_join.onus. - What the two fixes uncovered in
self/. Fifty obligations across the compiler in Onus had been proved vacuously; the match join settled fifteen, and the rest were real gaps, fixed at the source: loop invariants that lost a bound (j <= n,i < List.len(xs: parts),List.built(b: segs) >= 1,steps <= 100000000), counters declaredIntwhereInt where it >= 0(orit < List.len(...)) was meant, avar end: Intthat lost>= 0,digits_endgainingensures is_digit(at(src, start)) implies result > start, a slice in the constant evaluator’sgrapheme_spanwhose lower bound could exceed the text (a bug), and in the parser:take(consume or fail at the end of input) in place ofskipwhere the next token is known, sop.pos > old(p).posfollows;type_argsandcall_argspromisingresult is Ok implies p.pos > old(p).pos; and the measure(List.len(xs: p.toks) - p.pos) * 2 + flag(b: more)for loops that end by clearingmore, since adecreasesclause must fall on every iteration, the last included. - The verifier in Onus.
self/formula.onus(formulas and SMT-LIB text),self/z3.onus(finding and running z3 throughio.Process, the proof cache),self/lower.onus(lowering expressions, callee contracts, type facts, structural measures),self/vc.onus(the body walker ofverify/vc.ts: bindings, havoc, joins, loops,for, obligations at calls, fields and arithmetic),self/constant.onus(constant discharge),self/verifier.onus(the pass: z3 outcomes, counterexamples, the equal-measure cycle rule, the panic and const-fn rules). Obligation ids and node keys replace object identity; the contracts pass records the expression at each refinement andrequiressite and the constructor of each field initialiser (expr_nodes,ctor_of) for constant discharge.report.Diagnosticcarries an optionalObligationInfo(kind, text, status, counterexample) as the TypeScript one does. Left out for now: theONUS_DUMP_SMTdebugging dump and contract mutation (§20.4, an option ofbuildVCsthe Onus builder does not take). - Claims, capabilities and paths in Onus.
self/claimcheck.onus(tiers, carried claims,assumesites keyed by module and BLAKE3 of the canonical statement, propagation, E0203–E0206;claimsis a keyword, hence the module name),self/capabilities.onus(E0600–E0602),self/paths.onus(reachability, bounds, forbids, required claims, policies, gates and capability sites intoPathAnalysisrecords for the path report; E0410–E0416). The driver in Onus reads no assumption ledger yet, sopolicy verified_assumptions_onlyreports E0416 for every assumption, as the TypeScript compiler does without a ledger.self/check.onusruns the passes in the driver’s order (contracts, claims, capabilities, verify, paths) with--z3,--budgetand--cache;maintakesio.Process. Text.splitfor a prefix. Adecreasestext is trimmed of itsat the call to …suffix by taking the first piece ofText.split, which needs no index proof;Text.index_ofplusText.slicewould have required one the verifier cannot give.- The reports in Onus.
self/json.onus(JSON values, compact and two-space-indented text asJSON.stringifywrites them),self/loc.onus(line tables per file, locations as the reports print them),self/codes.onus(the code titles, generated fromreport/codes.ts),self/interface.onus(the §11.1 document and the elided canonical text; the coverage line, with the test coverage table and mutation records the driver in Onus does not read yet at zero),self/pathreport.onus(the §9.1 document),self/diagjson.onus(the §13 object, with the E0001 repair and the canonical hash). The printer gainedprint_item,print_signature,print_verify,print_typeandprint_module_elided; the loader keeps each file’s comment table for them;report.Diagnosticcarries repairs.self/check.onusprints the documents with--interface-json,--path-jsonand--diag-json, andpackages/compiler/test/self/reports.test.tscompares them byte for byte withonus interface --json,onus path --jsonandonus check --jsonon every source in the repository. The one known difference, not exercised by any source: positions count code points here and UTF-16 units there, so a line with a character outside the basic plane would place a later column differently. - What the report differential found in the checker in Onus. The
earlier differentials compared codes and spans; the JSON compares
everything. Brought into line with the TypeScript compiler: a parser
diagnostic names the definition being parsed (
tokens.Diagnosticcarries it; animplisIface[Target]with the target’s source text rebuilt from its tokens); a file that already carries a diagnostic gets no canonical text, hence nocanonical_hash; E0113 says at which line the earlier binding is; E0700 carries its obligation (requires, failed); the evaluator’s precondition messages forList.get,List.replicate,List.sliceandBytes.getare the runtime’s, word for word. The verifier’s body walker looked parameters of examples, properties and laws up under the wrong key tag, so their names were “not in the verifier’s scope” and their obligations stayedchecked; the fixture suite had not caught it because every property and law fixture is exercised through the TypeScript walker. - A nested generic call evaluates at check time (TypeScript
evaluator).
first_or(xs: [3, 4], fallback: 7)callinghead[T]callingList.getconverted the intrinsic’s result with the typeTofhead, not theIntit was called at, and threw “cannot convert a T”, so the example wasdeferred; the evaluator in Onus, whose values need no conversion, ran it. The TypeScript evaluator now keeps the instantiation of each frame and resolves a nested call’s type arguments through it, so the two agree and the example passes at check time.
Deferred, not changed
Stream[T] ! eas a type (§3.11) is not parsed:-> Stream[T] ! eis ambiguous between the stream’s effect and the function’s. To be settled when streams land.- Multi-binder quantifiers and tuple comparisons in §4.1 (
forall px: Int, py: Int where (px, py) != (x, y)) are not in the grammar; nested quantifiers (§5.3 allows depth two) express the same thing. budgetannotations (§12.3) andproves float(§3.2) have no syntax yet.- Generated tests import
vitestandfast-checkby name and resolve them from the nearestnode_modules; a project outside this repository needs both installed.