Releases: aallan/vera
Release list
v0.1.0
v0.1.0 — the first minor release: zero known bugs.
Over the bug burndown, every open bug-labelled issue — 37 in all — was fixed on a single release/v0.1.0 integration branch, each on its own adversarially-reviewed PR, then the release was cut with a full documentation sweep and the literal "No known bugs."
Highlights
- Silent-miscompile gates.
compare/ ordering on a user ADT (had compiled to a heap-pointer compare) and==on a non-Eqtype (pointer identity, not value equality) are now rejected at check withE242/E243— the severest failure class, closed. - Structural
show/hash/eqover composite, recursive, nested-generic, and generic-mutually-recursive ADTs; a polymorphically-recursiveBox<Box<T>>degrades to a clean skip rather than a traceback. - Effect handlers work over composite / parameterized type arguments.
- Module imports — transitive, nested fn-type-alias, single-uppercase ADT names, and closure-body generics — all resolve.
- Verifier no longer crashes on nested same-ADT constructors; injective Z3 sort names; ADT-typed call-site precondition obligations; contract-predicate degradation.
- Browser
State<T>composite-Floatdefault fixed. - Diagnostics. Zero-size
Array/ generic-@T/ refinement edge cases now reject cleanly at check with a singleE135/E206, never a codegen traceback or a duplicate error.
By the numbers
6,779 tests, 143 conformance programs, 37 examples, 91% code coverage, and a 14-chapter specification.
Full detail for every fix is in CHANGELOG.md and the release PR #938.
🤖 Release prepared with Claude Code
v0.0.196
Added
examples/async_http_fanout.vera— the 37th example and the showcase for the v0.0.192 concurrent<Async>: twoasync(Http.get(url))requests started back-to-back so network latency overlaps, then awaited and folded into a Z3-proved0..3status summary (4 obligations Tier-1 verified).examples/async_futures.veraandEXAMPLES.md's async section were reworded to stop teaching eager-only async: eager applies to non-whitelisted shapes, directHttp.get/Http.postcalls run concurrently (#841).- Two conformance programs pinning the
apply_fnfix (suite now 106):ch05_apply_fn_typing(verify level — alias-typed application, variadic two-parameter application, deliberately non-commutative subtraction so a slot swap is caught) andch05_apply_fn_arity(negative fixture,expected_error: E201— a wrong-arityapply_fnmust failcheck). - Spec completions after the server-effects sprint: §0.7 chapter index gains the Chapter 13 row; §7.7.6 documents the
Asyncmarker effect alongside its peers; §9.5.3 drops the stale "(future work)" framing on async Http composition; §9.3.4 scopes theFuture<T>zero-overhead claim to eagerly-evaluated futures; §11.10.5 documentsapply_fn's checker typing. wasmtime serveline-buffering caveat (spec §13.7 + TOOLCHAIN.md), empirically confirmed: under--world servera handler'sIO.printwithout a trailing newline is held in the host's line buffer (flushed by the next newline, dropped at shutdown if none comes) — end log lines with\n; nativevera servedoes not buffer this way.- veralang.dev surfaces the server/WASI work: two new Key-features cards (Verified HTTP handlers; WASI 0.2 components), a third Runs-Everywhere column with the
--target wasi-p2 --world server→wasmtime servecommand sequence,HttpServerin every effect list, and the 14-chapter/eight-effect counts — mirrored in thebuild_site.pyliterals that generateindex.md/llms.txt/llms-full.txt(the hardcoded chapter list there gains Chapter 13).
Changed
- Documentation sweep after the server-effects sprint (v0.0.192–v0.0.195):
HttpServerand the WASI targets are now surfaced in README (tagline, delivered-features), FAQ, and DESIGN.md's target/Async rows;KNOWN_ISSUES.md's WASI limitation row is rescoped from the closed #237 to the honest remainder tracked by #853 (wasi-p2 covers IO+Random only; the default target still uses ad-hocvera.*imports), and the same rescope retitles vera/README's row to "Partial WASI support"; stale coverage-gap rows for the shipped #592/#645 removed; vera/README's module map gainscodegen/wasi.py,runtime/wasi_host.py,runtime/server.py(plustail_position.py,text.py) with recounted package totals (~65,000 lines of Python); CONTRIBUTING.md's branch-protection section now recordsenforce_adminsand the actual review requirement; MUTATION.md documents the stale-.pycpurge step for hand-run mutation kills;ch10_float_predicatesrenamedch09_float_predicates(its content, manifest chapter, and spec_ref were all chapter 9); counts refreshed everywhere (37 examples, 106 conformance programs, 14 spec chapters, live test totals). Scheduled limitation-state checking in CI is tracked by #852.
Fixed
apply_fnis now typed by the checker — no more spurious E200, and misuse is a check-time error (#854). The documented primitive for applying a stored function value was unknown to the checker: every use fell into the unresolved-bare-call path, drawing a[E200] Unresolved function 'apply_fn'warning on green programs (vera check examples/closures.verashowed it) and typing the call asUnknown— so wrong arity or argument types passedcheckwith exit 0 and only surfaced at compile time (a raw WAT assembler error at line 0,0, or a silently skipped function), and applying aneffects(<IO>)-rowed fn value inside aneffects(pure)function passed entirely (a reachable effect-soundness hole: the fn-typed parameter route was open even though constructing such a closure in a pure fn already tripped E122).apply_fnis variadic and effect-polymorphic — arity, argument types, result type, and effect row all come from the applied value's fn type — so it cannot be a fixed-signature registry row; it is now a checker special form typed structurally against the appliedFunctionType: wrong arity →[E201], wrong argument type / non-function first argument →[E202], the applied row joins the caller's used effects and is checked against the declared row ([E122]/[E125]), and the result is the fn type's return type (a body returning anapply_fnresult against the wrong declared return is now[E121]). TypeVar-parameterised fn-type aliases (the prelude combinator shape, e.g.@Mapper<A, B>params) check as before. Redefiningfn apply_fn(...)— previously green at check time while codegen hijacked every 2+-argument call tocall_indirect, silently skipping the caller — is now the same[E151]one-canonical-form error as any built-in redefinition. Mutation-validated four ways (special form disabled, effect-row join skipped, arity check skipped, E151 reject-set entry dropped — each flips exactly its discriminating tests RED).- Prelude combinator skip-warnings no longer fire on every compile, and prelude diagnostics no longer misattribute to user source (#851). Every
vera compileused to emit five[E602]/[E604]warnings about the generic Option/Result prelude combinators (option_unwrap_or,option_map,option_and_then,result_unwrap_or,result_map) being skipped — code the user didn't write and, in most programs, never calls — with locations that resolved the prelude buffer's line numbers against the user's file (a 5-line hello cited "line 61"; longer files rendered unrelated user source under the caret). Two fixes: (1) synthetic origin — diagnostics about prelude-injected declarations (and their mono clones, and nodes inside their bodies) now cite the synthetic file<prelude>and quote the actual prelude source line, in both text and--jsonoutput, so a prelude span can never render user code (inject_preludereturns the buffer it parsed; codegen's_warning/_errorresolve prelude-origin nodes against it); (2) reachability suppression — E602/E604/E605 skip-warnings for prelude functions are dropped unless the program actually references them, via a transitive call-target scan rooted at every non-prelude declaration (including generic user fns the mono collector never visits, and imported module fns), so a minimal program compiles with zero warnings while a program that references a skipped combinator without a compilable instantiation keeps exactly that combinator's warning as the honest pre-runtime signal (now correctly attributed). A program that successfully callsoption_mapwas already clean for the called fn via the #604 mono-compiled suppression; with the unreferenced four now silent too, it also compiles warning-free. User-defined unsupported functions warn exactly as before, with user-file locations (pinned by test). Mutation-validated four ways (suppression deleted, origin tag dropped, transitivity broken, body-node flag stuck false — each flips its discriminating test RED).
v0.0.195
Added
--world server— verified HTTP handlers as portable wasi:http components (Stage D of the server-effects sprint; spec §13.7).vera compile --target wasi-p2 --world serverpackages the same contract-checkedhandle(@Request -> @Response)programvera servehosts natively (#305) as a component exportingwasi:http/incoming-handler@0.2.0— stockwasmtime serveruns it unmodified, no flags (live-tested: routing, handler-set headers, a 1 MiB body byte-identical under GC stress, trap → host 500; imports pinned at@0.2.0, which wasmtime's semver-compatible lookup links — the design study also probed@0.2.3acceptance). A generated adapter wrapper reads method / path-with-query / headers / body through the wasi:http interfaces into the arena, constructs theRequestADT in the guest heap from the compilation's own constructor layouts (WAT-level shadow-stack rooting throughout), callshandle, decodes theResponse, and drives the outgoing-response resource sequence (borrow-before-transfer, child-stream-drop-before-finish, 4096-byte write chunks). Headers without a host:Map<String, String>operations are host imports on the core target, but a Vera Map is two plain guest-heap blocks — the server world implements the String-keyed Map ops in guest WAT with the host's exact semantics (position-preserving update, later-insert-wins, power-of-two capacity growth), pinned by a host-vs-served differential battery (mixed-case / absent / duplicate / 41-header matrix + insertion-order agreement); non-String Map instantiations and all other collection families stay gated. The server-world surface is honest and explicit:IO.print/IO.stderrroute to the serve console;read_line/read_char/read_file/write_file/get_env/args/exitare rejected with a diagnostic (negative-probed: those imports do not link under the wasi:http proxy world); bodies are buffered (streaming is future work); request headers share the ~63 KiB arena; an out-of-range status or forbidden header answers 500 instead of trapping the server. The cli-world emission is byte-identical to v0.0.194 (pinned).vera runrejects server-world artifacts with a pointer towasmtime serve(wasmtime-py's built-in host has no wasi:http); the nativevera servedriver is unchanged — one handler, two deployment paths. Design decisions live-validated before implementation (topology: MAIN owns memory, serve wrapper in the adapter, dispatch table 16→32 slots; the check-7$Libcdodge andincoming-body.finishboth superseded — recorded inWASI.md); five mutation kills (later-wins parity, map update-vs-append, Request-build shadow-push via an in-suite eager-GC surgery test, cli-pin leak — which first false-greened on a stale.pyc, purged and confirmed RED — and a family-gate line).
v0.0.194
Added
- Experimental WASI Preview 2 target —
vera compile/run --target wasi-p2(#237).vera compile --target wasi-p2emits a binary WebAssembly component whosevera.*IO + Random host imports are implemented on top of WASI 0.2 interfaces, runnable by any stock wasip2 host —wasmtime runworks with no flags and no Vera bindings (wasmtime.wat2wasmaccepts component text, so no external componentizer and no new dependency). The component wraps the unchanged core module (the default--target wasmemission is untouched, pinned by test): each(import "vera" "op")becomes a same-namedcall_indirectshim through a funcref dispatch table the main module defines and exports, and a compiler-generated adapter core module implements all 14 IO/Random ops (plus thecontract_fail/overflow_trapchannels) over canon-lowered WASI imports, planting itself into the table via active elem segments strictly before any lifted export can run — the naive main↔adapter import topology is an instantiation cycle, which the component model forbids.cabi_reallocis a bump allocator over a GC-exempt 64 KiB scratch arena belowgc_heap_start(host-written data needs no rooting and a collection can never move a half-written block — the #593/#695 UAF class, host-side); data crossing back into Vera is copied into GC-heap blocks under explicit shadow-stack rooting, and a 500-arg GC-pressure stress pins it. Canonical-ABI variant discriminants are readi32.load8_u(they are u8; retptr-slab reuse leaves garbage in the padding — found live as an EOF misread aslast-operation-failedwith a stale list length as a "handle").vera run --target wasi-p2executes the component under wasmtime-py's built-inadd_wasip2()host (newvera/runtime/wasi_host.py) with the core path'sExecuteResultcontract: stdout/stderr capture (+ the #543 live tee),os.environsnapshot, cwd preopen for file ops, argv,IO.read_lineCRLF handling matching the core host's universal-newlines behavior (a lone\rseparator stays content — spec chapter 13), and the #516 trap-kind taxonomy — a contract violation classifies ascontract_violationwith the full violation text recovered from the WASI stderr channel (structured frames do not cross the component boundary; the JSON envelope'sframesis empty — spike check 5's documented degradation). A program using any host family beyond IO/Random gets a diagnostic naming the family — never a silent fallback to the core target. Both entry worlds are exported:wasi:cli/run@0.2.0(what stockwasmtime runinvokes) and a plain liftedmainfor scalar returns (Int/Natass64,Float64,Unit); aString/heap-returningmainruns viawasi:cli/runand reports no value. Inherent WASI 0.2 divergences are documented in the new spec chapter 13 and pinned by tests:wasi:cli/exit@0.2.0carries ok/err only, soIO.exit(n)degrades to exit status 0/1 under every wasip2 host. Validation is live, not parse-only (the design study caught a mis-spelled filesystemerror-codecase —quota, notdisk-quota— only atadd_wasip2instantiation): the suite instantiates and executes every artifact, a dual-target conformance differential runs all 88 run-level conformance programs under both targets and requires byte-identical stdout/stderr (71 execute identically; the rest are family-gated,main-less, or wall-clock-dependent — the sweep also surfaced thathandle[Exn]programs need the wasip2 runner to enable the exceptions proposal like the core engine does), and a stock-wasmtime-CLI smoke test runs where the CLI is installed. This closes #237 as written (its listed ops are exactly the IO family) as an experimental WASI Preview 2 target (IO + Random surface) — not a blanket "WASI 0.2 compliant" claim; the remainingvera.*families (Map, Set, Decimal, Json, Html, Md, Regex, Math, Http, Inference, State, Async) stay host-bound on the core target. Also: the doc-builtin-shadowing scanner now skips.claude/(session-tooling worktrees carry full repo copies that re-flagged every spec chapter).
Fixed
- GC use-after-free: pair-typed
letbindings (String/Array<T>) were never shadow-rooted (#847, fixed by #846). The plain-letpair branch intranslate_block(vera/wasm/context.py) stored the (ptr, len) pair into WASM locals without pushing the pointer onto the GC shadow stack — the last unrooted sibling of the #705 scalar-i32 let fix and the #707 let-destruct pair fix (whose review comment had already named this gap class). Unobservable for Vera-side producers — an array literal or string builtin shadow-pushes its own freshly-allocated block at the alloc site, and that push survives to the function epilogue, masking the missing let root — but a host-import pair (IO.args→Array<String>,IO.read_line→String) is rooted only host-side during construction (_ShadowGuard, popped on return), so the first Vera-side allocation after theletcould collect the block while the locals still pointed at it: the free list overwrites the payload's first words and reads through the binding see reclaimed bytes (IO.printof anIO.argselement after anat_to_stringcall printed2::++2@instead of2:aa+bb;string_joinover the swept backing chased overwritten element pointers). Found stress-testing #237 underVERA_EAGER_GC=1; no WASI code involved, and reachable under ordinary collection pressure. Pair lets are now rooted unconditionally — static and null pointers are ignored by the conservative scan's heap range check, so the push is harmless for non-heap values. New targeted eager-GC tests pin the fix (args / read_line reproducers) and confirm the neighbouring host-import ADT paths (IO.read_file→Result<String, String>,IO.get_env→Option<String>) were already rooted by the #705/#707-era fixes. - De-flaked the #841 live-interrupt test; one red combo no longer cancels the CI matrix (#848).
test_async_await_keyboard_interrupt_live_requestprinted its progress marker betweenasync(...)andawait, racing the worker thread's request — whose arrival gates the test's interrupt — against the guest reaching the print: a lost race yields the correct exit 130 with empty stdout and failed the stdout assertion (twice on macos-26 runners across PR #846's matrices, each time cancelling the other 11 fail-fast jobs). The print now precedes theasync(...), so program order supplies a real happens-before (buffer write → request issuance → arrival → interrupt) and the assertion strengthens frominto==; prompt issuance at theasyncpoint stays pinned by the #841 request-ordering and operand-stack tests, so the reorder loses nothing. The test matrix also setsfail-fast: false, so a single red combo reports alone instead of cancelling eleven healthy jobs. Round 2 (PR #849, closes #848): the reorder alone proved insufficient — server arrival is produced by the executor worker thread and says nothing about the main thread's position, sointerrupt_main()'s pending flag could materialize outsideexecute()'s protected region on slow runners (an xdist worker crash on two macOS jobs, a completed run withexit_code=Noneon ubuntu — 3/12 jobs at one head). The interrupter now also pollssys._current_frames()until the main thread is verifiably parked inhost_async_await→Future.result— the interruptible wait the #595-class machinery intercepts — before firing, with a bounded deadline so the test can never hang.
v0.0.193
Added
<HttpServer>— verified HTTP request handling, served byvera serve(#305). A server program defines a total, contract-checked handler —public fn handle(@Request -> @Response) effects(<HttpServer>)— andvera serve prog.vera [--port N]hosts the accept loop: the loop lives in the host, so handlers need noDiverge, are termination-checked like any function, and every contract on the handler (or its helpers) is an ordinary Tier-1/Tier-3 obligation (the newexamples/http_server.veraproves a status-range postcondition at Tier 1).HttpServeris a built-in marker effect (no operations; spec §7.7.5);Request(method, path, headers, body) andResponse(status, headers, body) are prelude ADTs injected when referenced (headers areMap<String, String>; user definitions shadow). Per-request isolation: each request runs on a fresh module instance (freshexecute()— instantiation measured at ~0.02 ms in the Stage-0 spike), soState<T>cannot leak between requests (pinned by test). A runtime contract violation (or any trap) inside a handler answers 500 with the trap diagnostic as a JSON body (trap_kind, message, frames — thevera run --jsonenvelope shape); the connection is always answered. Host marshalling is layout-driven:CompileResult.adt_layoutsexports the constructor layouts this compilation computed,build_request_adtshadow-roots every intermediate allocation (#570/#692 discipline),decode_response_adtreads the returned ADT, and both fail loudly on an unexpected layout shape; a newInstanceCalleradapts a (Store, Instance) pair to the caller protocol the heap helpers already use. HandlerIO.printgoes to the server console; request handling is sequential in v1 (concurrency: #406); native-only (the browser runtime does not serve HTTP — documented divergence, spec §12.9.3). Also fixed en route: Pass-1 function signatures that reference prelude ADTs in params/return were computed before prelude registration and recordedunsupportedinfn_param_types— a post-injection pass now re-registers exactly those. New:tests/conformance/ch09_http_server.vera(levelverify— needs no network, unlikech09_http),examples/http_server.vera, spec §9.5.6, and aTOOLCHAIN.mdserve recipe.
v0.0.192
Added
- Concurrent
<Async>:async(Http.get/post(...))now runs on a host worker thread (#841). The concurrency half deferred when #59 shipped the language surface:async(Http.get(url))/async(Http.post(url, body))— with call-free argument expressions — fuse into a singlevera.async_http_get/vera.async_http_posthost import that issues the request on a hostThreadPoolExecutorat theasync(...)point (request issuance keeps program order) and returns theFutureas a #578 bit-31-tagged handle wrapper (new wrap kind 4);awaitprobes the value's first word for the wrapper tag and blocks onvera.async_awaitfor fused handles, passing eager values through unchanged. Two overlappingasync(Http.get)s are pinned by a server-side request-ordering test (no wall-clock). Every otherasyncshape keeps the eager identity lowering (spec §9.5.4 now says an implementation MAY evaluate concurrently when the effect row is commutative; §7.4 records the ordering guarantee). The fusion predicate is strictly narrower thanW002's whitelist, so a "evaluates eagerly" warning can never coexist with concurrent execution — and it lives in one shared module (vera/wasm/async_fusion.py) consumed by both import-emission passes, so the pre-scan and the translator cannot desync. GC follows the full opaque-handle contract, mutation-validated: the wrapper is shadow-rooted at the async site (operand-stack window pinned) and an unawaited future's wrapper is reclaimed by Phase 2chost_decref_handle(4, handle), cancelling the task and evicting the store entry (observable viaExecuteResult.host_store_sizes["future"]). Worker threads run only the pure fetch halves (fetch_get/fetch_post, split out of theHttphost callbacks invera/runtime/http.py) and never touch guest memory; theResultADT is built at await time on the guest thread. Ctrl-C during a blockedawaitrides thewasmtime>=45BaseException trampoline to the exit-130 handler, and the executor is torn down (shutdown(cancel_futures=True)) on every exit path. The browser runtime stays eager (spec-conformant; identical values, request fires synchronously at theasyncpoint, outcome buffered host-side untilawait). A function may now declare aFuture<T>return type (previously[E605]-skipped —Future<T>is transparent in the type mapper). Theawaithandle-check keys on the literal typeFuture<Result<String, String>>and covers slots, parameters, direct compositions, and calls — bare, imported, or module-qualified — whose declared return is that type (derived from the cross-module return-type registry — with module-qualified calls resolved by(path, name), so a colliding local of a different future shape cannot misclassifyawait(m::grab(...))in either direction; the local-only first cut and the bare-name qualified keying were both caught in review with repros and fixed with RED-first tests). Alias-typed shapes are rejected before codegen today (an alias-typedlethas no WASM representation →[E602]skip); the one unclassifiable shape — an indirectly-called closure returning this future type — is a documented limitation in KNOWN_ISSUES.md. W002— async concurrency-eligibility warning (#841).async(e)is only made concurrent whene's effect row stays within the commutative whitelist ({Http}in v1; theAsyncmarker itself has no operations and cannot order anything). For any other row the checker now says so —async(IO.print(...))warns that it evaluates eagerly at theasync()site — instead of letting a program imply concurrency it does not get. The rule resolves effect-op calls to their parent effect and function calls to their declared rows, recursively, and treats anything unresolvable as conservatively non-commutative. Mutation-validated four ways (rule deleted, whitelist over-permissive, whitelist over-firing, fn-call rows ignored — each flips exactly its discriminating tests RED).
Changed
- Split
tests/test_checker.pyinto eight phase-focused test files (#420). The monolithic 6,752-line, 60-class checker test file is replaced by eight themed files —test_checker_types.py,test_checker_patterns.py,test_checker_functions.py,test_checker_effects.py,test_checker_modules.py,test_checker_errors.py,test_checker_builtins_collections.py,test_checker_builtins_strings.py— each under 1,200 lines, with the shared header (the_check/_errors/_warnings/_check_ok/_check_clean/_check_errhelpers and theEXAMPLES_DIR/EXAMPLE_FILES/CLEAN_EXAMPLES/WARN_EXAMPLESconstants) extracted to a newtests/checker_helpers.pyimported by all eight (matching the repo's existingfrom tests.<module> importprecedent). Mechanical and behaviour-preserving: all 572 tests are carried over unchanged and each class moves whole; no test is added, removed, or modified. (Companion to the #419test_codegen.pysplit.) - Split
tests/test_verifier.pyinto nine theme-focused test files (#839). The 9,356-line, 38-class verifier test file — the largest remaining after the #419 split, and the verifier/SMT oracle for the mutation sweep — is replaced by nine theme files (test_verifier_contracts.py,_nat_obligations,_primitive_ops,_calls_modules,_adt_decreases,_refinements,_shadow_audits,_mutation_obligations,_mutation_gates_smt), each under 1,500 lines, alongside the pre-existingtest_verifier_coverage.py(untouched). The shared harness — the_verify/_verify_ok/_verify_err/_verify_warn/_nat_sub_statushelpers plus theEXAMPLES_DIR/ALL_EXAMPLEScorpus constants and the_MKsource template — moves to a newtests/verifier_helpers.pyimported per file. Mechanical and behaviour-preserving: all 474 tests are carried over unchanged, every class / helper / constant moves byte-for-byte (46/46-block differential against the pre-split file), and the[tool.mutmut]oracle selection tracks the new files. The issue's 8-file plan became nine under the 1,500-line ceiling (the #746 refinement-predicate class alone is ~1,300 lines, and the #387 hardening battery split in two). Completes the oversized-test-oracle split program (#420, #419, #839). - Split
tests/test_codegen.pyinto twenty-one feature-focused test files (#419). The monolithic 21,225-line, 161-class codegen test file — the largest file in the tree, and the codegen oracle for the mutation sweep — is replaced by twenty-one feature files (test_codegen_expressions.py,_calls,_infrastructure,_interpolation,_effects,_data_types,_arrays,_refinements,_strings,_string_builtins,_numeric,_io,_collections,_json,_decimal,_host_effects,_nat_guards,_translator_fixes,_gc_alloc,_gc_rooting,_gc_reclamation), each under 1,500 lines (the issue's acceptance criterion), alongside the six pre-existingtest_codegen_*modules. The shared harness — the eleven_compile*/_run*/ WAT-and-GC assertion helpers (four of which lived interspersed between classes) plus the_IO_PRELUDE/_INLINE_BUILTIN_NAMESfixture constants — moves to a newtests/codegen_helpers.pyimported per file. Mechanical and behaviour-preserving: all 1,219 tests (including the 10stress-marked) are carried over unchanged and every class, helper, and constant moves byte-for-byte; per-file import blocks are computed from actual usage, and the[tool.mutmut]oracle selection tracks the new files. The issue's original 9-file plan was drawn when the file was 10,019 lines / 118 classes; at 21,225 / 161 the same theme boundaries yield twenty-one files. This closes out roadmap Tier 1 (safety net and runtime robustness). (Companion to the #420test_checker.pysplit.) - Consolidated the triplicated
_resolved()helper intests/test_checker_modules.py(#835).TestCrossModuleTyping,TestVisibilityEnforcement, andTestModuleCallParsedeach carried their own@staticmethod _resolved()that builds aResolvedModulefrom source text, and theTestModuleCallParsedcopy had drifted (file_path=Path("/fake")vs the others'Path(f"/fake/{'/'.join(path)}.vera")). Replaced all three with a single module-level_resolved_module()called directly, unifying on the non-driftedfile_pathform. Follow-up to the #420 split, which had deliberately preserved the duplication byte-for-byte (unifying changes thefile_pathvalue, which flows into diagnosticlocation.fileviavera/checker/modules.py); the 45 module tests are unaffected.
v0.0.191
[0.0.191] - 2026-07-01
Changed
- Codegen type-check-impossible guards now raise
CodegenInvariantError([E699]) (#657, follow-up to #626). The #626 audit classified everyreturn Noneinvera/codegen/**andvera/wasm/**; #658 converted the 104 user-actionable SILENT_SKIP sites toCodegenSkip([E602]). This converts the genuine INVARIANT_DEFENSIVE guards — non-forwarding dispatch fall-throughs and shape guards on states the type checker has already rejected — toraise CodegenInvariantError, surfaced at the_compile_fnboundary as an[E699]"internal compiler error, please file a bug" (severityerror) instead of a silent skip or a mis-attributed[E602]. 21 sites: 2 incodegen/closures.py, 19 inwasm/operators.py; pinned bytests/test_codegen_invariant_e699.py. Behaviour-preserving — the converted paths are# pragma: no cover(type-check-impossible). A closure-body invariant now propagates to_compile_fnfor a single[E699](rather than being caught in_compile_lifted_closure, which emitted[E699]and returned None so the enclosing function also got a spurious[E602]"closure skipped"); and the quantifier-predicate guard is tightened to require exactly one parameter (so a malformed multi-parameter predicate raises rather than silently using the first). - Corrected the #626 audit's PROPAGATE premise (#657). The audit assumed #658 made every codegen leaf raise, leaving the PROPAGATE
if x is None: return Noneforwards dead. That is false for the #630[E615]string-interpolation channel:_translate_interpolated_stringrecords failing segments and returnsNone, which propagates up and is dropped loudly as[E615]at the_compile_fnboundary. Sotranslate_expr/translate_blockstill returnNonereachably, and forwards of them are load-bearing PROPAGATE, preserved — not removed (satisfying the issue's "preserved (still reachable)" criterion). Fiveoperators.pysites the audit had tagged INVARIANT were in fact such forwards and are kept asreturn None; theinference.pydefensive sites are kept asOptional-by-contract. The invariant is now documented invera/skip.py("Reachable None via the [E615] channel") and at every preserved forward, so a future cleanup pass can't repeat the mistake (which the test suite caught as anAssertionErrorinTestE615LoudInterpolationFallthrough630).
v0.0.190
[0.0.190] - 2026-07-01
Changed
- Text I/O is UTF-8 regardless of the host locale, enforced by a gate (#645). Python's text-mode
open()/Path.read_text()/Path.write_text()— andsubprocess.run/Popen/check_output(..., text=True)captures — fall back tolocale.getpreferredencoding()(cp1252 on en-US Windows) when noencoding=is given, so a Vera source / doc / fixture / program output containing→,—, or any non-ASCII byte failed on a locale-default Windows shell. #641 papered over CI with aPYTHONUTF8=1backstop; this is the durable fix. A newscripts/check_explicit_encoding.pyAST-audits every text-modeopen()/read_text()/write_text()and everysubprocess(..., text=True)capture undervera/,scripts/,tests/, requiring an explicitencoding="utf-8"literal (binary / bytes mode skipped; a deliberate exception opts out with# encoding-exempt: <reason>), wired into pre-commit and the CIlintjob. It found and fixed 160 bare file-I/O sites across 16 files plus 97 subprocess text captures across 18 files (far more than the ~30 the issue estimated — the suite has grown ~5× since #641). The audit's scope also coverstempfile.NamedTemporaryFile/TemporaryFile/SpooledTemporaryFileopened in text mode (they default to binary, but a text one is a locale-encoded write just likeopen(..., "w")— the idiom test helpers use to stage.verasource containing→/—; 29 such sites across 9 files fixed). TheveraCLI reconfigures stdin as well as stdout/stderr to UTF-8 at startup, so a Vera program's non-ASCII output and input (e.g.IO.read_charon piped UTF-8) round-trip on any locale. Together these made text I/O locale-independent and thePYTHONUTF8=1CI backstop (#641) was removed — verified by a clean Windows matrix with the backstop gone (the tempfile and stdin gaps were surfaced by that matrix, not the audit, which is why removing the backstop is its own acceptance test). Pinned bytests/test_check_explicit_encoding.py(checker logic, scope-discovery coverage, and a repo-clean assertion). (Folds in the stdio / subprocess work that had briefly been split to #832.)
v0.0.189
Changed
- The UTF-8 "safe decode" invariant now lives in one helper (#592). Six sites decoded WASM-memory bytes with
errors="replace"so a corrupt String(ptr, len)pair from an upstream codegen bug surfaces as U+FFFD rather than a raw PythonUnicodeDecodeErrorescaping wasmtime's trampoline as a "python exception" cause — the #516 / #522 / #589 contract that a user-level program never produces a Python traceback. The invariant was re-implemented at all six (host_print/host_stderr/host_contract_fail/ the String-return extractor invera/codegen/api.py,_read_wasm_stringinvera/runtime/heap.py, and_read_stringinvera/wasm/markdown.py) and guarded by six structural source-grep tests plus one end-to-end test covering onlyhost_print— so five of the six sites had no behavioural coverage, and the greps would break under exactly the refactor the issue proposed. Theerrors="replace"decode now lives in one place — a newvera.runtime.text.safe_utf8_decode(the ROADMAP Tier-2 single-source-of-truth theme, same class as the #828 error-code registry) — reached only through a shared_slice_and_decodehelper (vera/runtime/heap.py) that the three WASM-memory string readers (_read_wasm_stringand a new_read_string_exportthere, and markdown_read_string) delegate to. Thehost_print/host_stderr/host_contract_failhost imports and the String-return extractor route through those readers instead of decoding inline, so nobytes(...).decode(...)call survives outside_slice_and_decode(a net simplification ofexecute()— the closures no longer re-implement the memory read). The six structural greps are replaced by one helper unit test plus three end-to-end tests that wire the production readers (_read_wasm_string,_read_string_export, markdown_read_string) over a memory region seeded with invalid UTF-8, so a strict-decode regression surfaces as aUnicodeDecodeErrorescaping the trampoline — caught at one point per reader rather than needing a grep per call site, and transitively covering the host imports / extractor that route through them; thehost_printend-to-end test is retained to pin the trampoline fact itself, independently of the production path. Behaviour-preserving. (The network-response decode sites inhttp.py/inference.pyare a separate, already-resolved family — #591, closed — with deliberate per-site handling (errors="replace"forHttp.get/Http.post; intentionally strict forInference.complete, so a non-UTF-8 LLM response fails as a structuredResult::Errrather than silently gaining U+FFFD), pinned by their ownTestNetworkResponseUtf8Hygiene591class; they are outside this WASM-memory helper's scope by design.)
v0.0.188
Added
- Diagnostic-field discipline is now enforced (#682).
spec/00-introduction.md§0.5.1 requires every diagnostic to carry arationale, afix, and aspec_ref, but theDiagnosticdataclass defaults all three to""— so a partially-tagged diagnostic compiled and shipped silently, weakening the "diagnostics as instructions" guarantee (DESIGN.md §Checkability). Newscripts/check_diagnostic_fields.py(wired into pre-commit and the CIlintjob, mirroring the #597 walker-coverage gate) AST-parses everyDiagnostic(...)constructor andself._error(...)/self._warning(...)call undervera/and fails when one of the three fields is missing — or when a presentspec_refdoes not resolve to a real spec section/chapter with a matching title (the present-but-wrong case, e.g. citing §4.3 "Operators" when §4.3 is "Slot References"). The exemption surface is explicit and reasoned, never silently inferred (DESIGN.md §"Explicitness over convenience"): awarningis not required to carry afix(it has no corrected-code template); the codegen_error/_warninghelpers — internal-compiler (E699) and "function skipped" diagnostics with no user fix or spec section — are exempt via a documented registry in the script; and a one-off internal/defensive site carries a# diag-fields-exempt: <reason>marker (a dedicated token, deliberately not# noqa:-prefixed so it cannot collide with ruff's suppression namespace). A cheap emission-side check also requires every literalerror_codeundervera/to be registered inERROR_CODES(catching typos / unregistered codes); enforcing each code's uniqueness per concept and its presence is tracked in #828. The pre-commit hook also triggers onspec/**/*.md(not onlyvera/**/*.py), since the validity pass reads the spec section/chapter titles — a spec-only retitle can invalidate an otherwise-unchanged citation, and now re-runs the check locally rather than only in CI.
Fixed
-
Corrected the pre-existing wrong
spec_refcitations and documented typed holes (#682). The adversarial audit behind the new validity gate found 30-plus diagnostics across the checker, the parser-error factories, the codegen path, and the verifier whosespec_refcited the wrong section number or title (e.g. all arithmetic/comparison/logical operator errors cited §4.3 "Operators", but §4.3 is "Slot References" — arithmetic is §4.4, comparison §4.5, logical §4.6) — a misleading instruction is worse than a missing one. Everyspec_refundervera/now resolves. Typed holes (?) had no spec section at all —W001(and the codegenE614) cited a fabricated §3.10 "Typed Holes" — so they are now documented in a newspec/04-expressions.md§4.17 "Typed Holes", which both diagnostics cite. Two further citations resolved to a real but semantically wrong section (which the validity gate, checking only that a ref resolves with a matching title, accepts — so they needed reviewer judgment): the unresolved-qualified-call warningE220cited §7.4.1 "Ambiguous Operations" (the multi-match case) but the error is an unresolved op, so it now cites §7.4 "Performing Effects"; and the type-alias-cycle errorE132cited the unrelated slot-namespace section §3.8 "Type Alias and Reference Resolution" — the rule it actually enforces ("an alias must resolve to a concrete type; the alias chain must be acyclic") had no spec home, so a sentence stating it was added tospec/02-types.md§2.6.3, whichE132now cites. Twofixtemplates were also corrected for machine-actionability: the unresolved-bare-call diagnostics (E200and the codegen cross-module backstop) replaced a literal-lookingimport the.module(f)placeholder withimport <module>(f)plus a "replace<module>with that module's path" hint; and the ADT-invariant error (E120) — whose oldfixsuggestedinvariant(@Field.0 > 0), a form that cannot resolve (@Fieldis not a type, anddatainvariants are NYI, #686) — now points at the documented refinement-type workaround (type Positive = { @Int | @Int.0 > 0 };). Finally, the missing-contract-block errorE001moved from §5.4 "Contract Clauses" (which states therequires/ensuresrule but not theeffectsone) to §5.2 "Function Declaration Syntax" — the one section that shows the complete mandatory block, matching the diagnostic's ownfixand the sibling function-structure errors (E1xxin the checker) that already cite it; its three rendered mirrors (README.md,docs/index.html,spec/00-introduction.md) moved in lockstep, pinned by theTestErrorDisplaySyncsuite. -
Four
error_codecollisions corrected (#682 review + audit). Each ofE130/E210/E320/E600was shared by two unrelated diagnostics, so a--jsonconsumer keying onerror_codewould mislabel one of each pair: the slot-resolutionE130(expressions.py) was reused by the "Decimal takes no type arguments" type-application error (resolution.py), nowE134"Type does not take type arguments"; the unknown-constructorE210(calls.py) by the empty-Tuple()error, nowE216"Empty tuple type"; the unknown-constructor-in-patternE320(control.py) by the empty-tuple-pattern error, nowE323"Empty tuple pattern"; and the "unsupported parameter type"E600(functions.py) by codegen's "refinement base resolves to another refinement" error (contracts.py), nowE618"Nested refinement base unsupported". Each stable code now maps to exactly one diagnostic concept; the new codes are registered inERROR_CODES, and the three reachable ones (E134,E216,E618) are pinned by collision-regression tests (E323's site is parser-unreachable). -
Five further semantically-wrong
spec_refs corrected (exhaustive audit) (#682). A final multi-agent audit of every diagnostic — each finding adversarially verified — caught five more of the resolves-but-wrong-section class the validity gate cannot detect (it checks resolution, not appropriateness):E122(pure violation) andE002(missingeffects()) cited effect-chapter sections describing how effects work, not the rules they enforce, and now both cite §5.5 "Effect Declaration", which states "Every function MUST declare its effects" and "A function that declareseffects(pure)MUST NOT perform any effects" verbatim; the alias-arity errorE133cited the slot-namespace §3.8 and moved to §2.6.3 alongsideE132; the@Int→@Natnarrowing obligationE503cited §4.7 "Let Bindings" (it fires far beyondlet) and now cites §2.2.1 "IntandNatcompatibility", where the obligation is stated; and the contract-testing skip warningE701moved from the chapter-levelChapter 6, "Contracts"to the dedicated §0.5.6 "Contract-Driven Testing". -
spec/04-expressions.md§4.17 "Typed Holes" gains a worked example (#682). The section now shows?in context (@Int.0 + ?), demonstrating it is inferred to type@Int, type-checks withW001, and is rejected at compile withE614. Validating it exposed thatscripts/check_spec_examples.py's check and verify stages treated warning-severity diagnostics as failures (unlike the CLIvera check, and unlike the verify stage's own error-only filter one line later) — both stages now filter to error severity, so a legitimately-checkable warning-emitting example is validated rather than mis-failed.
Changed
-
Backfilled 54 under-tagged checker diagnostics (#682). Every
self._error(...)site invera/checker/(calls,control,expressions,core,resolution) — plus partial directDiagnostic(...)constructions in the parser-error factories, the tester, the monomorphizer, and the verifier's obligation-fallback — now carries a concreterationale, afixtemplate, and aspec_ref. The canonical example from the issue,vera checkon a function mixing@Bool.0 + @Int.0(E140), now emits aFix:paragraph instead of stopping at the rationale; pinned bytests/test_checker.py::TestErrorCodes::test_E140_carries_a_fix_paragraph_682. -
The
TestHostHandleReclamation573GC-reclamation suite is markedstress(#738). The class compiles and runs full reclamation programs at scale (~minutes locally), so it is now deselected from the default per-PRpytestrun and runs underpytest -m stress(and nightly CI), reclaiming local inner-loop time. -
CI hardening — CodeRabbit Pro+ configuration (no compiler or runtime change).
.coderabbit.yamlgains three agentic pre-merge checks — changelog covers public-surface changes, spec and implementation move together, and diagnostics carry full metadata — all inwarning(advisory) mode, so they surface drift during review without blocking merge. The deterministic floor (mypy,scripts/check_*.py, pre-commit, the conformance suite) remains the only hard gate; these checks complement it and target judgment-gaps it cannot cheaply cover (e.g. "the changelog describes the change", not merely "a bullet exists"). Also raised the GitHub-Checks ingestion timeout to 15 min so CodeRabbit waits for the full multi-OS test matrix before commenting; addedAGENTS.mdto the Code Guidelines set; linkedaallan/vera-benchso a downstream-breaking CLI / exit-code / diagnostic-format change here is flagged during review; and extended thetests/**review guidance to call out the commutative-operations slot-ordering trap. The deterministic counterpart for asserts — ruffflake8-banditfor assert-as-guard ([#657](https://github.com/a...