v0.1.0 — zero known bugs (first minor release)#938
Conversation
…letters (#869) A user ADT named with a single uppercase letter (`data A`, plus a second `data B { MkB(A) }`, or any program that drags in the prelude Option combinators) made `vera run` trap at WASM instantiation with `unknown table 0` on a program that passed both `check` and `verify`. A `forall`-generic FnDecl is a template: it reaches WAT only through its monomorphized clones (call sites are rewritten to mangled clone names in vera/wasm/calls.py), so while a type parameter stays abstract the template body cannot lower and codegen's Pass-2 attempt is skipped. The prelude ships option_map / option_and_then as `forall<A, B>` and result_map as `forall<A, B, E>`; a user `data A` put `A` into the ADT layout table, so codegen resolved the identically-named prelude type parameter to the concrete user ADT, lowered the option_map template cleanly, and emitted it as a bare-named function whose passed-in-closure call_indirect referenced a function table the module never declared. The prelude combinators' and closure-type aliases' internal type-parameter identifiers now use reserved Vera-prefixed names (VeraA / VeraB / VeraE / VeraT / VeraU) that no ordinary user ADT spells, keeping prelude internals invisible to user namespace decisions (spec 0.2 principle 4 — structural references eliminate naming-coherence errors; DESIGN.md principle 2, explicitness). These names are substitution keys only and never appear in a mangled clone suffix (Monomorphizer._mangle_fn_name escapes the concrete type arguments), so the rename is orthogonal to the #775/#883 injective name mangling. New conformance program ch02_adt_single_letter_name runs a single-letter ADT end-to-end; TestSingleLetterAdtNamePreludeCollision869 pins the run behaviour and the absence of a bare option_map/option_and_then template in the WAT. Mutation-validated: reverting the rename flips the `data A` run tests and the bare-template WAT assertion RED with `unknown table 0`. Co-Authored-By: Claude <[email protected]>
…ering, generic-HOF consultors, refinement peeling (#867) One shared cycle-guarded resolver (resolve_fn_type_alias, relocated to vera/monomorphize.py to avoid an import cycle): apply_fn closure-return consultors, the generic higher-order-function monomorphization sites (calls.py + monomorphize.py twins), and FnType-terminal after refinement unwrapping. Flips the pre-existing still-fails-loud #867 pin to awaited-correctly. PR #880 review, 4th single-hop site: _slot_name_to_wasm_type (vera/wasm/inference.py) classified only a BARE FnType alias as an i32 closure pointer, so a let/param slot annotated with a refinement-wrapped fn alias (type Foo = { @fn(...) | p };) or one chained through a refinement resolved to None — the binding was rejected with CodegenSkip ("has no WASM representation") and its whole function dropped, on a check/verify-green program. A plain NamedType chain was already handled by the _resolve_base_type_name prefix, so only the refinement-involving shapes remained. Routed through the same shared resolver; mutation-kill: un-routing it flips exactly its two refinement-slot tests, all others green. Co-Authored-By: Claude <[email protected]>
…d arguments (#882) A call to a helper whose requires() constrains ADT-typed parameters produced no call_pre obligation at all — no E501, no Tier-3 warning — so `vera verify` reported ok:true for a call it never statically examined, while the identical Int-parameter program correctly fired E501 and `vera run` trapped at runtime. Root cause in SmtContext._translate_call_with_info: a constructor-call argument (MkP(1)) only translates once the callee's concrete ADT sort exists in the Z3 sort cache, but in a caller context that never declared that ADT the sort was never materialised, so _find_sort_for_ctor returned None, the whole call bailed, and the obligation vanished. The call translator now materialises each ADT-typed parameter's sort from the callee's declared types before checking the precondition, so — post-#879 — MkP(1) vs MkP(2) is statically refutable (E501 + counterexample), a satisfiable call discharges, and nested-ADT / Float64-field-NaN arguments refute correctly. Where the argument or the precondition genuinely can't be modelled (a host-handle field such as Map, an undecidable predicate like string_length(...) > 0) the obligation demotes to a loud Tier-3 E522 warning rather than silently not existing (DESIGN.md: degrade loudly; the runtime guard still enforces it). Return-value modelling is deliberately unchanged: an ADT-argument call whose result feeds a postcondition stays opaque exactly as before, so no corpus program's postcondition tier moves. A before/after verify --json census over all 145 conformance + example programs shows tier1 unchanged (1231); the only movement is three example call sites (http, inference, async_http_fanout) gaining one honest Tier-3 call_pre demotion each for their undecidable string_length preconditions (+3 Tier-3). Adds a run-level conformance program (ch06_requires_adt_arg) and eight unit tests (TestAdtCallSitePrecondition). Mutation-validated three ways. Co-Authored-By: Claude <[email protected]>
…ct position (#874) Contract-clause eq(a,b)/compare(a,b) now lower to the same canonical ==/Ordering form codegen already uses in bodies (was an uncaught CodegenSkip), and the verifier desugars them to the FP-correct == / Ordering encoding (was a silent Tier-3 deferral). Includes the review round: where-fn contract lowering, compare Tier-1 Ordering-sort materialization, the _is_user_fn shadowing guard, and a tightened trap assertion. Co-Authored-By: Claude <[email protected]>
…uses; dedicated E532 code (#882) Completes the #882 fix after the blast-radius review found it incomplete for its own scope plus a diagnostic-code semantics mismatch. GAP 1 (silent loss inside ensures): a contracted call whose precondition cannot be checked statically records its call-pre demotion during step-7 postcondition translation, which runs AFTER the step-6b demotion drain, so the obligation vanished — the exact silent-loss #882 closes, still open for calls in ensures position. The demotion drain now runs a second time (step 8b) after ensures / refined-return / decreases translation, before the where-fn recursion reset()s the shared SMT context. RED-first probe: a call in an ensures clause produced zero call_pre obligations while the statement-position twin produced one; mutation-killed by deleting the second drain (both ensures tests flip RED). GAP 2 (wrong code): the demotion was tagged E522, whose registered meaning is "Cannot verify postcondition (body undecidable)". Minted E532 "Cannot verify call-site precondition (undecidable)" so the class is one concept per code; a program can now carry E522 (postcondition) and E532 (call site) distinctly (async_http_fanout does). Tier census over 146 conformance + example programs: 1231/495/1726 (parent) -> 1235/499/1730, all movement accounted for (new all-Tier-1 fixture +4; four programs each gain one honest E532 call_pre demotion +4); zero silent regressions. Spec §6.4.2 documents the demotion; doc counts refreshed. Co-Authored-By: Claude <[email protected]>
…onger collide (#884) The verifier named Z3 datatype sorts with the old lossy sanitize (`key.replace("<", "_").replace(">", "").replace(", ", "_")`), so a monomorphized `Box<Int>` and a flat ADT literally named `Box_Int` both became the Z3 name `Box_Int`. Z3's per-context datatype cache conflates same-named sorts (the last `create()` wins and earlier same-named sorts silently adopt its structure), so `Box<Int>`'s sort acquired the flat ADT's `MkBoxInt(Bool)` constructor — a false E500 counterexample (`@Box<Int>.0 = MkBoxInt(False)`) on trivially-true valid code. Both Z3 sort-name sites (`_get_or_create_adt_sort` and `_get_or_create_tuple_sort` in `vera/smt.py`) now route through the injective `mangle_type_name` mangler #775 introduced for WAT symbols, so distinct type keys can never share a Z3 sort name. The full-type-string sort cache and the #871 nested-Float64 fpEQ walk are unaffected (the cache is keyed by the type string, not the Z3-visible name). Soundness-safe: the conflation only freed field constraints, producing conservative false negatives, never a false Tier-1 — a genuinely-false ensures over the colliding pair is still disproved after the fix, now with the correct `MkBox` constructor. Both sort-name sites are independently pinned, one collision per site, so neither `mangle_type_name` call can be reverted without flipping a test: - ADT site (`_get_or_create_adt_sort`): the `Box<Int>` / flat `Box_Int` collision. Reverting it re-collides the sorts and flips the three `Box`-collision tests (verifies-clean, correct-constructor, and the false-prove probe) back to the false E500. - Tuple site (`_get_or_create_tuple_sort`): a `Tuple<Tuple<Int>, Int>` vs `Tuple<Tuple<Int, Int>>` collision. Both keys lossy-sanitize to `Tuple_Tuple_Int_Int`; under the lossy name the two synthesised tuple sorts share one Z3 datatype, and a match arm projecting a field index the conflated sort does not carry crashes the verifier with `z3.z3types.Z3Exception: Invalid accessor index` at `sort.accessor` (in `_bind_pattern`). Reverting it flips the new tuple-collision test. Confirmed load-bearing: reverting this site with only the `Box` collision test present flipped nothing. The rename also broke the verifier's Array-element reverse lookup, fixed here since #884 caused it. `_get_element_sort_for_array`'s tier-3 fallback reconstructed the `_z3_sorts` key by string surgery (`elt_key.replace("_", "<", 1) + ">"`) that assumed the old lossy naming, so after the rename a generic-ADT element (`Array<Box<Int>>` → sort `Array_Box_LInt_R`) produced the non-matching candidate `Box<LInt_R>` and the lookup silently returned None. Latent — tier 1's creation-time direct map (`_array_element_sorts`) fires for every live path, and no corpus program indexes an `Array<Generic<T>>` in a contract this way — but dead-on-arrival for that shape. A new injective inverse `unmangle_type_name` (a left-to-right prefix-code decode, round-trip-tested against `mangle_type_name`) recovers the key correctly, and the stale tier-3 comment is corrected. Pinned by tests/test_verifier_sort_name_collision_884.py (ADT + tuple collisions, rename-control, false-prove disproved for the right reason), the ch06_adt_sort_disambiguation conformance program, TestSmtContextDirect.test_element_sort_reverse_lookup_tier3_generic_adt (the Array-element unit pin), and the unmangle round-trip / non-range-input tests in TestMangleInjectivity. Mutation-validated: reverting either sort-name site to the lossy sanitize flips its collision test RED, and reverting the Array-element lookup to the old string surgery flips the reverse-lookup unit test RED. Pre-existing on main; found by the PR #883 adversarial panel. Co-Authored-By: Claude <[email protected]>
…redicates (#887 review) The Pass 1.6 ability-op walker `_rewrite_ops_in_expr` fell through `ForallExpr`/`ExistsExpr` to the leaf return, so an `eq`/`compare` inside a `forall`/`exists` contract predicate was never rewritten to its canonical operator form. A quantifier in a contract is runtime-lowered by `_translate_quantifier`, which compiles the predicate `AnonFn` body — so the un-canonicalized ability op reached the WASM backend as a bare unregistered `FnCall` and tripped the same uncaught `CodegenSkip` as #874's original top-level case. The walker now descends into the quantifier's `domain` and `predicate`. Two new codegen tests (`forall`+`eq` and `exists`+`compare` in an `ensures`) are mutation-validated: both flip RED to the `CodegenSkip` when the new branch is disabled, GREEN when restored. Also syncs TESTING.md/ROADMAP.md test totals (6,076 -> 6,078) for the two new tests. Co-Authored-By: Claude <[email protected]>
…rose list (#892 review) Co-Authored-By: Claude <[email protected]>
#893 review) Co-Authored-By: Claude <[email protected]>
…, #873) An imported generic instantiated only by the importer was monomorphized nowhere — check passed but run failed WASM validation at `call $gid`, for both the bare and module-qualified call forms (#774). The importer now harvests imported public generics, merges the unshadowed ones into its `generic_decls`, and the shared discovery walks `ModuleCall` targets. The module-qualified-called — is now monomorphized under a distinct `mod$<path>$gen$…` name reached only by the qualified call. A generic whose only call site is inside a closure body failed run with `unknown func` (#873): `_compile_lifted_closure` now threads `generic_fn_info` + `known_fns`. Completeness gaps closed one level deeper (PR #888 review): a shadowed generic calling another generic emits the transitive clone (both sides); the statement-position result-shape predicates share one ModuleCall target resolver with the desugar; an unshadowed generic calling a shadowed sibling has its shadowed clone discovered by scanning the emitted normal clones. Soundness keystone (CR 3519156263): an imported generic's clone RUNS in the importer but its body lives in another module, so a LYING module contract ran unverified — a false Tier-1 (verify clean, run traps the clone's postcondition). Worse, a same-named LOCAL generic absorbed the module call's instantiation into its bare key, proving the local's contract in the module clone's place. The importer now records every shadowed module clone under its `mod$…` base (never the bare name a local generic owns), and verifies each imported generic's clone (shadowed and unshadowed) at its own instantiations, turning a lying module contract into an honest E500. The warm incremental session runs the same verification so warm == cold. Codegen and verifier discovery move in lockstep — proved by the #732 differential (namespaced keys distinguish module clone from same-named local generic). Mutation-validated: disable the imported-generic verify pass → lying-contract tests stop catching E500; desync either side's mod$… key → differential flips; disable either side's normal-clone scan → the unshadowed-caller case flips. Run-level conformance programs `ch08_cross_module_generic` and `ch09_generic_in_closure`; spec §11.16. Co-Authored-By: Claude <[email protected]>
Closes #884 Reconciled the #881 (mutual-recursion group-builder) and #884 (injective sort naming) changes to smt.py: kept #881's group-builder structure and its `_z3_sort_name` choke point, and made that choke point delegate to the injective `mangle_type_name` (#884's fix) so all sort-name generation — group members and tuples alike — is injective in one place.
…mance, 6234 tests) Post-merge reconciliation for the release/v0.1.0 integration branch: conformance 109→118, tests →6234 across 99 files, per-file table rows, verify/run level counts, and the passed/skipped subtotal (6,162 passed, 46 skipped) from the full integrated suite run. Site assets regenerated. All gate scripts green.
…orts returns the reachable closure (#890) A check-green program with ZERO generics crashed at `vera run` with `unknown func: failed to find name $via_mid` when `main` imported `mid` and `mid` imported `base`: `mid`'s body calls `base::wrap40`, but `base` was never registered or compiled into the importer's flat WASM module, so the emitted body was left with a dangling call. Root cause: `ModuleResolver.resolve_imports` (vera/resolver.py) returned only the top-level program's DIRECT imports, even though its recursion had already parsed and cached the transitively-reached `base`. Codegen, the checker, and the verifier all consumed a module list missing every transitive dependency. Fix: `resolve_imports` now returns the transitive closure of reachable modules, deduplicated (a `main -> {left, right} -> base` diamond yields `base` once), each tagged `direct` (imported by the top-level program) vs transitive-only. Codegen harvests and compiles every module in the closure so an imported body's call to a deeper module resolves. Spec §8.6.4 preserved — a transitive module's declarations are NOT visible to the original importer: the checker and verifier inject only `direct` modules into the importer's callable namespace and qualified-call registries, and codegen subtracts a `_transitive_only_names` set from its cross-module guard rail so a main-program body calling a purely transitive symbol fails loudly at compile rather than silently binding to the definition emitted for a sibling module. Cycle-safe via the resolver's existing `_in_progress` E011 guard. Test-first + mutation-validated: the RED tests (chain + diamond) fail on the pre-fix code with the exact `unknown func`; reverting the closure return flips them RED again, and disabling the `_transitive_only_names` subtraction flips the §8.6.4 visibility test RED. A run-level conformance program (ch08_transitive_module_import + _mid/_base companions) pins the end-to-end behaviour. Pre-existing on main; found by the PR #888 review. Co-Authored-By: Claude <[email protected]>
… mono clone (#891) A generic instantiated over a parameterized ADT — `gid(MkBox(7))` for `gid : forall<T> fn(@t -> @t)` and `data Box<T> { MkBox(T) }` — passed `vera check`/`vera verify` but trapped at `vera run` with `type mismatch: expected i64, found i32` while compiling the `gid$Box` clone. Root cause: `_infer_expr_wasm_type`'s `ResultRef` arm (`vera/wasm/inference.py`) inferred only the scalar primitives and returned `None` for everything else. Once `T = Box`, the clone's postcondition `@T.result == @T.0` compares two i32 heap pointers, but the `ResultRef` (`@Box.result`) defaulted to the scalar i64, so the equality fell through to the `i64.eq` default while the operands were i32 — an ill-typed clone body wasmtime rejects at instantiation. The `SlotRef` operand (`@Box.0`) and the clone's `param`/`result` signature were already i32; only the postcondition op disagreed. Fix: the `ResultRef` and `SlotRef` arms now share one `_ref_type_name_wasm_type` helper, so an ADT-bound (and pair-/handle-bound) type variable lowers to i32 consistently across the signature and every slot/result read. New conformance program `ch02_generic_over_param_adt` (run level; suite now 112) plus a regression test in `tests/test_codegen_monomorphize.py` pin `gid(MkBox(42))` unwrapping to 42. Mutation-validated: reverting the `ResultRef` arm to the pre-fix scalar-only logic restores the trap, flipping the test RED. Pre-existing on main; found by the PR #888 soundness review. Co-Authored-By: Claude <[email protected]>
…d path (#772) An `Eq`-constrained generic (`forall<T where Eq<T>>`) called with a value whose type is inferred from a constructor — `eq2(MkBox("a"), …)` — dropped the type argument: the monomorphizer resolves a `ConstructorCall` to the bare ADT name `Box` (correct for clone mangling, since `Box<T>` is a uniform pointer), leaving the `Eq` gate no `<String>` to inspect. Pre-#773 this false-accepted `Box<String>` and emitted an unsound equality; post-#773 (structural derivation) it flipped to an over-reject — a `Box<String>` that should derive `Eq` was refused E613, while the slot-ref call form was accepted and ran correctly. Fix scopes type-arg recovery to CONSTRAINED type vars: a `ConstructorCall` bound to a `@T` carrying an ability bound now keeps its type argument (`Box<String>`, via `_get_arg_type_info`) for the clone name, the substituted slot type in the clone body, and the E613 gate — so the constructor path derives/rejects exactly as the slot-ref path does. Unconstrained generics stay bare (no clone splitting). The parallel WASM call-site rewriter and return-type resolver apply the same recovery (new `_generic_constrained_vars` side table) so the mangled call matches the emitted clone, and the direct `==` path (`Some(1) == Some(1)`) recovers the operand type argument in `vera/wasm/operators.py`. Soundness held: `Box<Array>` / `Box<Map>` still reject with E613, no E699 leak, and the checker<->codegen structural-Eq differential (#732/#773) stays symmetric. Mutation-validated both halves; full existing Eq suites pass unchanged. New conformance program `ch09_ctor_inferred_eq` (run level) exercises the path. Co-Authored-By: Claude <[email protected]>
…rence (#878) A generic whose type argument must be inferred from a user-fn call's return type in argument position monomorphized to the Bool phantom-var default: on a check-green program `vera run` emitted a call to a `$Bool` clone Pass 1.5 never produced, so the caller (often `main`) was skipped and dropped from the exports ("No exported functions to call"), or a private helper left a dangling `call` that failed WAT validation. Root cause is the lossy WAT collapse `i32 -> "Bool"` (a Decimal handle, an ADT, and an Option/Result pointer are all i32 — the same value as the phantom default) applied to user-fn returns on both the discovery and the body-emission sides: - Discovery (vera/codegen/monomorphize.py): `_build_mono_context` seeded the shared `fn_ret_types` from WAT signatures, so a generic bound solely by a user-fn return was discovered as `$Bool` while the #732 verifier discovered the precise type — a differential desync masked only by identity clone bodies. Now seeds from each fn's declared return TypeExpr, exactly as the verifier does, with the WAT collapse kept only as a fallback. - Body-emission (vera/wasm/inference.py): `_get_arg_type_info_wasm` had no FnCall branch, so a parameterized return in `Option<VeraT>` position bound nothing; and a user-fn call in bare `@VeraT` position collapsed i32 to Bool. It now mirrors discovery's FnCall branch (shared _BUILTIN_PARAMETERIZED_RETURNS plus a non-generic-user-fn parameterized case) and consults the declared return TypeExpr for the ambiguous i32 case. GENERIC calls are excluded from the user-fn branch (their declared return is over the callee's own type vars, not concrete types) so they fall through to generic-return resolution exactly as discovery does — keeping the two consultors in lockstep. Test values are deliberately Decimal returns (`0.3333...`) that cannot coincide with the Bool default (1/0), the exact Bool-coincidence trap CLAUDE.md warns about. New run-level conformance program ch09_generic_infer_user_fn_return (suite 123) plus three regression tests (crash-via-IO, WAT-mangle discriminator, discovery-vs-verifier differential); each change is independently mutation- validated. Does not weaken #888's cross-module/closure discovery. Co-Authored-By: Claude <[email protected]>
…uracy
`Eq` auto-derivation over a sparse multi-type-parameter ADT
(`data Res<A, B> { MkOk(A), MkErr(B) }`) on the constructor-inferred path used
to over-reject. Two fixes close it fully.
TASK 1 — cross-argument type-argument merge (the real over-reject).
`eq2(MkErr(5), MkOk("x"))` is a fully-determined `Res<String, Int>` (arg 0
fixes B, arg 1 fixes A) but first-argument-wins binding rejected it with E202.
The checker's inference (`_infer_type_args`/`_unify_for_inference`,
vera/checker/resolution.py) now MERGES per-type-parameter information across the
arguments via a shared `merge_inferred_types` primitive (vera/types.py): a
fresh placeholder yields to the more-determined sibling, a nested per-position
disagreement is a genuine conflict reported as the new E205 ("Conflicting type
argument inference"), and a top-level scalar mismatch stays an ordinary E202
(so `set_add(@set<Nat>.0, "oops")` and the #747 `pick(@Nat.0, @Int.0)` Nat
narrowing are unchanged). The monomorphizer discovery
(`_infer_type_args_from_args`, shared by codegen AND the #732 verifier) and the
WASM call-rewrite (`_resolve_generic_call`) apply the same merge, so all three
consultors emit/reference the identical `Res<String, Int>` clone — the
type-checked call compiles, runs by value, and verifies Tier 1. A per-parameter
conflict is never silently merged (is_subtype backstop); a determined-non-Eq
cross-arg type (`Res<Int, Array<Int>>`) type-checks then rejects E613. A
genuinely under-determined case (`eq2(MkOk(3), MkOk(4))`, B free on both) stays
a correct E202 false-reject.
TASK 2 — E619 diagnostic accuracy.
`_eq_type_arg_under_determined` (vera/codegen/monomorphize.py) emits E619 only
when the type WOULD derive once its free params are Eq-bound (every recovered
component is Eq). A known non-Eq component, recovered (`Res<A, Array<Int>>`) or
structural (`W<A,B>{ K(Array<A>, B) }`), is the accurate E613 instead. A free
slot is materialised as a reserved `?` sentinel (`Res<?, Int>`, never emitted)
so the gate can probe derivability with each free slot provisionally Eq.
Round-3 review polish:
- E619 message no longer leaks the `?` sentinel and its fix is compilable Vera:
free slots render by their declared parameter name (`Res<A, Int>`) and the
fix suggests a concrete Eq annotation (`let @res<Int, Int> = ...;`).
- The #732 differential's inline and call-rewrite corpora gain the cross-arg
merge shape, so a future merge regression is caught by the soundness oracle
(symmetric collapse trips the vacuous-emission guard; asymmetric one-sided
merge surfaces as a dangling `eq2$Res` clone). Mutation-validated.
- E205 moved into numeric order in errors.py.
- E619/E613 still emit at line 0 (no call-site span reachable at the mono
discovery point; pre-existing for E613, not newly introduced).
Mutation-validated (each flips only its discriminating tests RED): neuter
merge_inferred_types; suppress its conflict flag (falls to E202 backstop);
collapse the mono merge; skip the gate's component-Eq probe; drop the WASM-side
merge; leak the E619 sentinel. #772 and #878 untouched; full suite green
(6260 passed). New checker tests, codegen round-2/round-3 tests, differential
corpus entries, and conformance `ch09_multiparam_ctor_eq`.
Co-Authored-By: Claude <[email protected]>
…ype (E135) instead of silently emitting invalid WASM (#945) An `Array<Unit>` / `[()]` literal compiled to invalid WASM (check + verify + compile green, run failed at wasmtime load). Now rejected at check with E135 at both the type-resolution and array-literal gates. Closes #945 on the final release/v0.1.0 → main merge. Co-Authored-By: Claude <[email protected]>
…rview table (91% combined) — CR #938 review TESTING.md's CI-threshold paragraph claimed "Current coverage is 96%", conflicting with the overview table's "95% Python, 61% JavaScript — 91% combined" (the single source of truth). Aligned the number and clarified that the Python (pytest --cov) and JavaScript (V8 / vera/browser/runtime.mjs) flags report to Codecov independently. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Good catch — fixed in f89843b. The stale "Current coverage is 96%" now reads 95% Python, 61% JavaScript (91% combined) to match the overview table (the single source of truth per this file's header), and the sentence now notes that the Python ( |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
echo "--- coverage mentions in TESTING.md ---"
rg -n "coverage" TESTING.md -i
echo "--- check for stale 96% ---"
rg -n "96%" TESTING.md || echo "no 96% occurrences found"Length of output: 2123
|
|
@coderabbitai review |
✅ Action performedReview finished.
|
…as a bug `vera serve` was missing from the `vera` USAGE help text emitted by cli.py — a real dispatched command left undiscoverable from the tool itself, now guarded by test_usage_lists_every_dispatched_command. That is a code defect, not a docs-only gap, so #942 is relabelled `bug`. Every one of #938's 35 closing refs is now a bug-labelled issue; correct the Stage 17 narrative count (31 → 35) to match. Co-Authored-By: Claude <[email protected]>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
vera/checker/expressions.py (1)
927-966: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winDuplicate E135 for an annotated zero-size array literal.
For
let@array<Unit> = [()];, bothresolution.py's newArray<T>gate (fired via_resolve_type(stmt.type_expr)) and this literal-level gate (fired via_synth_expr(stmt.value), which never receivesexpected) independently emit E135 — two diagnostics for one root cause. The function's own comment says it exists to catch "the un-annotated literal... that never flows through_resolve_type", implying the annotated case shouldn't double-fire here.🐛 Proposed fix: skip the literal-level error when the annotation already caught it
- if isinstance(expr, ast.ArrayLit): - return self._check_array_lit(expr) + if isinstance(expr, ast.ArrayLit): + return self._check_array_lit(expr, expected=expected)- def _check_array_lit(self, expr: ast.ArrayLit) -> Type | None: + def _check_array_lit(self, expr: ast.ArrayLit, *, + expected: Type | None = None) -> Type | None: ... if erases_to_unit(first): - self._error( - expr, - f"An array literal of a zero-size element type " - ... - ) + already_reported = ( + isinstance(expected, AdtType) and expected.name == "Array" + and expected.type_args + and erases_to_unit(expected.type_args[0]) + ) + if not already_reported: + self._error( + expr, + f"An array literal of a zero-size element type " + ... + )Not a correctness bug (the program is still rejected), but a JSON-diagnostics-consumer would see two entries for one mistake.
Also applies to: 239-240
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@vera/checker/expressions.py` around lines 927 - 966, The zero-size array literal check in _check_array_lit is duplicating E135 for annotated cases already handled by _resolve_type in resolution.py. Update _check_array_lit so it only reports the literal-level error for unannotated or otherwise unsolved array literals, using the surrounding type context/expected information to skip the diagnostic when Array<Unit> (or similar) has already been rejected during type resolution. Keep the existing guard for the un-annotated path mentioned in the comment, and ensure the error is emitted from just one place for a single root cause.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@vera/checker/expressions.py`:
- Around line 927-966: The zero-size array literal check in _check_array_lit is
duplicating E135 for annotated cases already handled by _resolve_type in
resolution.py. Update _check_array_lit so it only reports the literal-level
error for unannotated or otherwise unsolved array literals, using the
surrounding type context/expected information to skip the diagnostic when
Array<Unit> (or similar) has already been rejected during type resolution. Keep
the existing guard for the un-annotated path mentioned in the comment, and
ensure the error is emitted from just one place for a single root cause.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9d6e3caf-baca-46ec-a227-6a6f5cddeb92
⛔ Files ignored due to path filters (1)
docs/llms-full.txtis excluded by!docs/**
📒 Files selected for processing (12)
CHANGELOG.mdFAQ.mdHISTORY.mdREADME.mdROADMAP.mdTESTING.mdtests/test_checker_types.pytests/test_codegen_refinements.pyvera/checker/expressions.pyvera/checker/resolution.pyvera/codegen/contracts.pyvera/errors.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
…orts E135 (PR #938 review) An array literal that is the RHS of an annotated `Array<Unit>` binding/return was rejected by BOTH `_resolve_type` (the annotation) and `_check_array_lit` (the literal) — two E135s for one root cause. `_resolve_type` returns the `Array<Unit>` type intact after emitting E135 (resolution.py:178), so `_check_array_lit` now receives the `expected` type and defers when it is already a zero-size array; exactly one E135 fires. The un-annotated `[()]` literal (no `expected`) and the genuine `Array<Int> = [()]` element-type mismatch still report. The #945 regression tests now assert the exact E135 count — the gap that let the duplication hide. A separate, pre-existing signature-double-resolution (a function's param/return types are `_resolve_type`'d in both the registration and check passes, doubling any resolution diagnostic — e.g. an `Array<Unit>` param reports E135 twice) is out of scope here and tracked separately. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Verified and fixed in 4f6bc55. Confirmed the double-emission: an array literal that is the RHS of an annotated |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
echo "--- _check_array_lit / _synth_expr_impl expected threading ---"
rg -n "_check_array_lit|_synth_expr_impl" vera/checker/expressions.py -A5 -B5 | head -150
echo "--- resolution.py E135 emission ---"
rg -n "E135" vera/checker/resolution.py -A5 -B5
echo "--- `#945` tests E135 count assertions ---"
rg -n "E135" tests/test_checker_types.py -A3 -B3Length of output: 2300
Good catch on the separate pre-existing issue — a function's param/return types being |
…lved twice) — PR #938 review A function's param/return types are `_resolve_type`'d in BOTH the registration and the check pass, so any resolution-level diagnostic on a signature was emitted twice at one location — e.g. E135 on an `Array<Unit>` param. `_error` now dedups on (code, file, line, column, severity, message); an exact match is indistinguishable to the reader, so it collapses to one entry while keeping genuinely distinct diagnostics (a bad param AND a bad return still report once each). Closes the pre-existing signature-double-resolution the #945 E135 gate made visible — the item 4f6bc55 flagged as tracked separately. Full suite green (6,690 passed); doc counts reconciled to 6,779 (one new test: test_signature_diagnostics_deduplicated_per_location). Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Follow-up: the pre-existing signature-double-resolution I flagged in the previous comment is now also fixed, in 8824c7b. Rather than restructure the two-phase (registration + check) resolution, |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
echo "--- _error dedup logic in vera/checker/core.py ---"
rg -n "_error" vera/checker/core.py -A15 | head -60
echo "--- test_signature_diagnostics_deduplicated_per_location ---"
rg -n "test_signature_diagnostics_deduplicated_per_location" tests/test_checker_types.py -A30Length of output: 5673
|
…ersion-table row Two diagnostic-duplication bugs found in the #938 review are now filed and closed by #938 — #947 (annotated Array<zero-size> literal double-reported E135) and #948 (function signature types resolved twice, duplicating any resolution diagnostic) — so the burndown total is 37, not 35. Update the Stage 17 prose (35 -> 37) and the version-table row (31 -> 37), and condense that row to a single sentence per HISTORY style (detail lives in CHANGELOG). Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Confirming the |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
echo "--- expressions.py _check_array_lit expected param ---"
rg -n "_check_array_lit" vera/checker/expressions.py -A10 -B5 | head -60
echo "--- core.py dedup ---"
rg -n "_seen_diag_keys|def _error" vera/checker/core.py -A10 | head -60
echo "--- issues 947/948 ---"
gh issue view 947 --repo aallan/vera --json title,state,body 2>/dev/null || echo "no 947"
gh issue view 948 --repo aallan/vera --json title,state,body 2>/dev/null || echo "no 948"Length of output: 7098
Both concerns are fully resolved. Nice work tracking down the shared two-phase-resolution root cause for |
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
vera/checker/expressions.py (1)
946-966: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winPeel refinements before the zero-size-array check
expectedcan still be aRefinedTypehere, soisinstance(expected, AdtType)misses annotated shapes like{@array<Unit> | true }. That lets the literal-level E135 fire again alongside the annotation-level one. Strip refinements first, or reusebase_type(expected)in this guard.🐛 Proposed fix
expected_is_zero_size_array = ( - isinstance(expected, AdtType) - and expected.name == "Array" - and len(expected.type_args) == 1 - and erases_to_unit(expected.type_args[0]) + isinstance(base_type(expected), AdtType) + and base_type(expected).name == "Array" + and len(base_type(expected).type_args) == 1 + and erases_to_unit(base_type(expected).type_args[0]) )🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@vera/checker/expressions.py` around lines 946 - 966, The zero-size-array suppression in the literal check is too narrow because `expected` may still be a `RefinedType`, so `isinstance(expected, AdtType)` misses annotated forms and lets `E135` fire twice. Update the guard in `vera/checker/expressions.py` around the `expected_is_zero_size_array` logic to peel refinements first, or reuse `base_type(expected)` before checking for `Array<Unit>`, so annotated zero-size arrays are recognized consistently and only the annotation-level diagnostic remains.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Outside diff comments:
In `@vera/checker/expressions.py`:
- Around line 946-966: The zero-size-array suppression in the literal check is
too narrow because `expected` may still be a `RefinedType`, so
`isinstance(expected, AdtType)` misses annotated forms and lets `E135` fire
twice. Update the guard in `vera/checker/expressions.py` around the
`expected_is_zero_size_array` logic to peel refinements first, or reuse
`base_type(expected)` before checking for `Array<Unit>`, so annotated zero-size
arrays are recognized consistently and only the annotation-level diagnostic
remains.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 88288a2c-e442-42f4-8864-2bc91db1fb5c
⛔ Files ignored due to path filters (1)
docs/llms-full.txtis excluded by!docs/**
📒 Files selected for processing (9)
CHANGELOG.mdFAQ.mdHISTORY.mdREADME.mdROADMAP.mdTESTING.mdtests/test_checker_types.pyvera/checker/core.pyvera/checker/expressions.py
🔗 Linked repositories identified
CodeRabbit considers these linked repositories for cross-repo context during reviews:
aallan/vera-bench(manual)
…ession (PR #938 review) CodeRabbit's re-review caught a gap in the annotated-Array<zero-size> dedup (4f6bc55): the `expected`-type guard checked `isinstance(expected, AdtType)`, but a refined annotation `{ @array<Unit> | pred }` makes `expected` a `RefinedType`, so the suppression missed it and the literal-level E135 double-fired again. The guard now strips to `base_type(expected)` first (as `erases_to_unit` itself does), mirroring the existing pattern at expressions.py:153. Confirmed: `let @{ @array<Unit> | true } = [()]` was 2 E135, now 1. The #945 annotated-literal test gains the refined variant. Co-Authored-By: Claude <[email protected]>
|
@coderabbitai Good catch — fixed in 181cd53. You're right that |
|
@coderabbitai review |
✅ Action performedReview finished.
|
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
echo "--- expressions.py base_type stripping for expected ---"
rg -n "base_type" vera/checker/expressions.py -B5 -A5 | head -80
echo "--- E135 refined variant test ---"
rg -n "RefinedType|refined" tests/test_checker_types.py -A5 -B5 | grep -n "E135" -A5 -B5 || true
rg -n "test_array_literal_of_unit_rejected|refined" tests/test_checker_types.py -A15 | head -80Length of output: 2314
I attempted to re-verify directly against the branch this round, but hit a sandbox clone/auth error, so I can't re-confirm the diff myself this time. That said, the fix is a natural, narrow extension of the same |
v0.1.0 — the first minor release: zero known bugs
This branch lands v0.1.0. Over the bug burndown, every open
bug-labelled issue — 37 in all, surfaced by systematic hunts forcheck-green programs that fail at codegen or verify — was fixed on this singlerelease/v0.1.0integration 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
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.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.State<T>composite-Floatdefault fixed.Release
[0.1.0]; HISTORY Stage 17.release/v0.1.0do not trigger it. Each fix was validated by a local full gate + adversarial review before landing.Closes #772
Closes #774
Closes #867
Closes #869
Closes #873
Closes #874
Closes #878
Closes #882
Closes #884
Closes #890
Closes #891
Closes #898
Closes #900
Closes #902
Closes #904
Closes #905
Closes #908
Closes #911
Closes #912
Closes #913
Closes #914
Closes #918
Closes #920
Closes #921
Closes #922
Closes #923
Closes #924
Closes #927
Closes #928
Closes #932
Closes #934
Closes #939
Closes #942
Closes #943
Closes #945
Closes #947
Closes #948
🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
vera serve file.veraHTTP serving (default:8000), plus documented--host/--port.Bug Fixes
Eq/Ordsoundness: correct structural equality,compare/ordering (including lexicographic strings), and safer handling of undecidable contract preconditions.State<T>default initialisation in Node/browser and improved effect-handler composite argument handling.Documentation / Tests / Chores