Skip to content

v0.1.0 — zero known bugs (first minor release)#938

Merged
aallan merged 96 commits into
mainfrom
release/v0.1.0
Jul 4, 2026
Merged

v0.1.0 — zero known bugs (first minor release)#938
aallan merged 96 commits into
mainfrom
release/v0.1.0

Conversation

@aallan

@aallan aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner

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 for check-green programs that fail at codegen or verify — was fixed on this 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-Eq type (pointer identity, not value equality) are now rejected at check with E242 / E243 — the severest failure class, closed.
  • Structural show / hash / eq over composite, recursive, nested-generic, and generic-mutually-recursive ADTs; a polymorphically-recursive Box<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-Float default fixed.

Release

  • Version 0.1.0 (consistent across 6 files); CHANGELOG [0.1.0]; HISTORY Stage 17.
  • 6,778 tests, 143 conformance programs, 37 examples, 91% coverage.
  • The CI matrix runs on this PR — the first CI on the integration branch, since PRs into release/v0.1.0 do 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

    • Added vera serve file.vera HTTP serving (default :8000), plus documented --host/--port.
    • Expanded cross-module generic monomorphisation/verification for more complete lockstep behaviour.
  • Bug Fixes

    • Improved Eq/Ord soundness: correct structural equality, compare/ordering (including lexicographic strings), and safer handling of undecidable contract preconditions.
    • Fixed State<T> default initialisation in Node/browser and improved effect-handler composite argument handling.
  • Documentation / Tests / Chores

    • Expanded conformance suite (111 → 143), updated guidance/figures, and bumped version to v0.1.0.

T and others added 30 commits July 3, 2026 09:32
…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]>
…, #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]>
…at module (#890)

fix(resolver): transitive module imports compiled into the importer's flat module (#890)
… 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]>
…ono clone (#891)

fix(codegen): generic over a parameterized ADT no longer traps in its mono clone (#891)
…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]>
…rred path (#772)

fix(codegen): Eq auto-derivation is type-arg-aware on the constructor-inferred path (#772)
…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]>
…ry, verifier, and call-rewrite (#878)

fix(mono): recover user-fn return types in generic instantiation inference (#878)
The #878 merge resolved KNOWN_ISSUES to 'No known bugs.', but #898 (Eq
over-reject on sparse multi-type-param ADTs) is an open bug-labelled issue,
so the one-to-one Bugs invariant requires its row until it's fixed.

Skip-changelog: docs-only bookkeeping, no vera/ or spec/ change.
 (generic-over-Unit)

Maintainer decisions: fix the #898 checker cross-argument over-reject (not just
the E619 diagnostic), and fix #900 (generic-over-Unit codegen crash) for v0.1.0.
Both now bug-labelled and tracked one-to-one.

Skip-changelog: docs-only bookkeeping.
…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]>
…nation diagnostic (#898)

fix(checker,codegen): cross-argument type-arg merge + accurate Eq under-determination diagnostic (#898)
Found during #900; pre-existing, non-generic. Folding into the #900 PR per
maintainer plan (same Unit-as-degenerate-component design family).

Skip-changelog: docs-only bookkeeping.
T and others added 2 commits July 4, 2026 19:11
…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]>
@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@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 (pytest --cov) and JavaScript (V8 / vera/browser/runtime.mjs) flags report to Codecov independently. No other coverage figure in TESTING.md was stale.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

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


@aallan Thanks — that resolves the stale coverage figure cleanly. Aligning the prose with the overview table as the single source of truth, and calling out that the Python (pytest --cov) and JavaScript (V8 / vera/browser/runtime.mjs) flags report to Codecov independently, makes the discrepancy much less likely to recur. (Note: I couldn't re-verify this directly against the branch this round due to a sandbox clone/auth error, but the fix as described addresses the concern.)

@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…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]>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Duplicate E135 for an annotated zero-size array literal.

For let @array<Unit> = [()];, both resolution.py's new Array<T> gate (fired via _resolve_type(stmt.type_expr)) and this literal-level gate (fired via _synth_expr(stmt.value), which never receives expected) 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

📥 Commits

Reviewing files that changed from the base of the PR and between 3b66188 and f89843b.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (12)
  • CHANGELOG.md
  • FAQ.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_checker_types.py
  • tests/test_codegen_refinements.py
  • vera/checker/expressions.py
  • vera/checker/resolution.py
  • vera/codegen/contracts.py
  • vera/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]>
@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Verified and fixed in 4f6bc55. Confirmed the double-emission: an array literal that is the RHS of an annotated Array<zero-size> binding was rejected by both the type-resolution gate and _check_array_lit, emitting E135 twice for one root cause. _check_array_lit now receives the expected type (threaded through _synth_expr_impl) and defers when it is already a zero-size array — since _resolve_type returns the Array<Unit> type intact after emitting its E135 (resolution.py:178), the literal gate can distinguish "already flagged by the annotation" from "un-annotated." Exactly one E135 now fires; the bare [()] literal (no expected) and the Array<Int> = [()] element-type mismatch still report, and the #945 tests now assert the exact E135 count (the gap that let the duplication hide). Separately, while verifying this I found a pre-existing, broader issue you didn't flag: a function's param/return types are _resolve_type'd in both the registration and the check pass, so any resolution diagnostic on a signature doubles — e.g. an Array<Unit> param reports E135 twice at the same location. That predates #945 and is out of scope for this literal-level gate; it's tracked separately.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

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 -B3

Length of output: 2300


@aallan Thanks for the thorough fix and write-up. 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 explanation is coherent and consistent with the architecture described elsewhere in this PR (_resolve_type returning the Array<Unit> type intact post-E135 per resolution.py:178, and _check_array_lit gaining an expected parameter threaded through _synth_expr_impl to distinguish "already flagged by annotation" from "un-annotated bare literal"). Locking down the exact E135 count in the #945 tests is the right way to prevent this class of duplication from regressing silently.

Good catch on the separate pre-existing issue — a function's param/return types being _resolve_type'd in both the registration and check passes causing doubled diagnostics (e.g. Array<Unit> param reporting E135 twice at the same location) is a real, distinct bug from the literal-level gate here. Since you've noted it's out of scope for this PR and already tracked separately, I won't push for a fix here, but happy to open a follow-up issue that links back to this discussion if you'd like — just let me know.

…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]>
@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@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, _error (vera/checker/core.py) now dedups exact-duplicate diagnostics on (code, file, line, column, severity, message) — an exact match is indistinguishable to the reader — so an Array<Unit> param now reports E135 once, while a signature bad in both param and return still reports once per position (a new test_signature_diagnostics_deduplicated_per_location pins that it collapses to 2, not 4 un-deduped nor 1 over-collapsed). Full suite green (6,690 passed); doc counts reconciled to 6,779.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

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 -A30

Length of output: 5673


@aallan Confirmed on the branch. vera/checker/core.py now has _seen_diag_keys (Line 182) and _error (Line 262) builds a dedup key of (error_code, file, line, column, severity, description) (Lines 273–276), skipping exact repeats while preserving first-occurrence order — and the comment at Lines 174–181 correctly documents the two-phase registration/check root cause.

test_signature_diagnostics_deduplicated_per_location (Line 1985) pins the right invariant: a signature with Array<Unit> in both param and return position yields exactly 2 E135s, not 4 (un-deduped) and not 1 (over-collapsed) — proving the dedup keys on location, not just error code. This is a clean, general fix rather than a point patch, and it resolves the pre-existing issue correctly.

…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]>
@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Confirming the vera/checker/expressions.py _check_array_lit E135 double-emission finding (lines 927-966) is fixed in 4f6bc55 — the literal gate now receives the expected type and defers when it is already a zero-size Array, so the annotated case reports E135 exactly once (mechanics in my earlier reply). The sibling signature-level double (an Array<Unit> param reporting E135 twice) is fixed in 8824c7b via an exact-duplicate diagnostic dedup in vera/checker/core.py. Both filed as #947 / #948 and closed by this PR; both pinned by exact-count regression tests.

@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

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


@aallan Confirmed on the branch — both fixes check out:

  • Literal gate (#947): vera/checker/expressions.py Line 240 now calls self._check_array_lit(expr, expected=expected), and _check_array_lit (Line 927) accepts the keyword-only expected: Type | None = None. This is exactly the plumbing needed to distinguish "already flagged by the annotation gate" from "un-annotated bare literal."
  • Signature dedup (#948): vera/checker/core.py Line 182 initializes self._seen_diag_keys: set[tuple[str, ...]], and _error (Lines 262–289) builds the key (error_code, file, line, column, severity, description) at Lines 273–276 and short-circuits on an exact repeat (Lines 277–279) before appending the diagnostic. That's a sound, general fix — it collapses true duplicates without merging genuinely distinct diagnostics at different positions.
  • Both #947 and #948 are filed with "Closed by #938" keywords in their bodies, so they'll auto-close on merge — linkage is correct.

Both concerns are fully resolved. Nice work tracking down the shared two-phase-resolution root cause for #948 rather than just patching the symptom.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Peel refinements before the zero-size-array check

expected can still be a RefinedType here, so isinstance(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 reuse base_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

📥 Commits

Reviewing files that changed from the base of the PR and between f89843b and b6b49aa.

⛔ Files ignored due to path filters (1)
  • docs/llms-full.txt is excluded by !docs/**
📒 Files selected for processing (9)
  • CHANGELOG.md
  • FAQ.md
  • HISTORY.md
  • README.md
  • ROADMAP.md
  • TESTING.md
  • tests/test_checker_types.py
  • vera/checker/core.py
  • vera/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]>
@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Good catch — fixed in 181cd53. You're right that expected can be a RefinedType ({ @Array<Unit> | pred }), and the guard's isinstance(expected, AdtType) missed it, so the literal-level E135 double-fired alongside the annotation's. The guard in vera/checker/expressions.py now strips via base_type(expected) first — mirroring erases_to_unit's own base_type stripping and the existing byte-refinement pattern at expressions.py:153. Confirmed: let @{ @Array<Unit> | true } = [()] was 2 E135, now 1 (the E202 type-mismatch is separate); the #945 annotated-literal test gains the refined variant.

@aallan

aallan commented Jul 4, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown

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 -80

Length of output: 2314


@aallan Thanks for chasing this down. The fix makes sense: guarding on isinstance(expected, AdtType) alone would miss expected values wrapped in RefinedType (e.g., { @array<Unit> | pred }), since the array's actual shape is only visible after unwrapping the refinement. Stripping via base_type(expected) before the AdtType check aligns this gate with erases_to_unit's own stripping behavior and the existing refinement-handling pattern at expressions.py:153, so the literal-level E135 no longer double-fires alongside the annotation-level one for refined array-of-Unit types.

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 expected-threading mechanism already validated in #947, and adding the refined-type variant to the #945 annotated-literal test is the right way to pin this specific shape against regression.

@aallan aallan merged commit e87a996 into main Jul 4, 2026
28 checks passed
@aallan aallan deleted the release/v0.1.0 branch July 4, 2026 20:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant