Layer 3 of #626: CodegenSkip infrastructure + convert 104 SILENT_SKIPs#658
Conversation
…Phase 1)
Layer 3 of the silent-translator-skip campaign converts the
implicit `return None` propagation chain across codegen into
explicit, source-located diagnostics. This commit introduces
the infrastructure — exception classes and catch handlers — and
makes no behavioural change: no translator raises CodegenSkip
yet, so the try/except wrappers are inert.
What changed
- New `vera/codegen/skip.py` with two exception classes:
- `CodegenSkip(node, reason)` — raised by a translator when an
AST node shape isn't yet supported by the WASM backend.
Caught at the `_compile_fn` / `_compile_lifted_closure`
boundary and converted to a structured [E602] diagnostic
with the unsupported-node's source span (rather than the
enclosing-function's, which is all the legacy `return None`
path can give us).
- `CodegenInvariantError(msg, node=None)` — raised when codegen
sees a state that type-check should have rejected. Surfaced
as [E699] "Internal compiler error" with a "please file a
bug" rationale.
- `vera/codegen/functions.py::_compile_fn` — wraps
`ctx.translate_block(decl.body, env)` in a try/except for both
exception types. Legacy `body_instrs is None` branch stays as
the catch-all until Phase 3 converts the remaining call sites.
- `vera/codegen/closures.py::_compile_lifted_closure` — same
pattern around the closure-body translate call. The
enclosing-fn drop is handled by Layer 2 (#636); these handlers
just emit the closure-level [E602] before returning None.
- `vera/errors.py` — adds the `E699` code.
Why these aren't in `vera/errors.py`
Every `VeraError` subclass carries a fully-formed `Diagnostic`
at construction time. The codegen-skip path needs to defer
diagnostic construction: the translator that detects an
unsupported shape doesn't know the enclosing function name or
its declaration span (both needed for the [E602] message). The
catch handler at the `_compile_fn` boundary has that context,
so these exceptions carry raw `(node, reason)` and let the
catch handler build the structured `Diagnostic`.
What's next
Phase 2 (audit): walk the ~367 `return None` sites across
`vera/codegen/` and `vera/wasm/`, classify each into one of
SILENT_SKIP / OPTIONAL_RETURN / INVARIANT_DEFENSIVE / KNOWN_GAP.
Phase 3 (convert): rewrite the ~30 SILENT_SKIP sites identified
by the audit to `raise CodegenSkip(node, reason)`.
Phase 4 (follow-up): the OPTIONAL_RETURN / KNOWN_GAP buckets
remain in a new follow-up issue with the audit table attached.
Verification
- `mypy vera/` clean (59 source files)
- `pytest tests/ -q` passes (3789 passed, 14 skipped) — confirms
the infrastructure addition is behaviourally inert
Refs #626
Co-Authored-By: Claude <noreply@anthropic.invalid>
…se 3a) 55 silent-skip sites in `vera/wasm/calls_arrays.py` converted to `raise CodegenSkip(node, reason)`. Each now surfaces as a source-located [E602] pointing at the unsupported AST node, rather than the generic enclosing-function-level [E602] the legacy `return None` propagation produced. What changed - New helper `_array_elem_triad_or_skip(arr_arg, *, role)` on `CallsArraysMixin`: centralises the `(_infer_concat_elem_type, _element_mem_size, _element_wasm_type)` triad that every array combinator runs upfront. Pre-conversion this was 9 lines × 14 combinators of identical bail-out shape. Post-conversion it's one helper call per combinator + per-leg CodegenSkip raises in the helper. All 6 `t_type = _infer_concat_elem_type(arr_arg); if t_type is None: return None; t_size = _element_mem_size(...); if t_size is None: return None; ...` blocks (filter, fold, reverse, any_all, find, sort_by) collapse to one line. - `_translate_array_map` / `_translate_array_mapi` use the helper for the input element triad; the closure-return triad (b_type) still has its own raises since it uses a different inference helper (`_infer_closure_return_vera_type`). - `_translate_array_append` raises on element-type / size / store-op failures. Doesn't use the shared helper since its arg is the element value, not the array container. - `_translate_array_concat` and `_translate_array_slice` retain their "provably-empty literal" fallback paths; the non-fallback branches raise CodegenSkip. - `_translate_array_flatten` raises on the two AST-walk failures (input isn't Array<Array<T>>; inner type couldn't be recovered). - Body-internal `_element_load_op` / `_element_store_op` checks inside every combinator's loop body raise CodegenSkip with a per-combinator role label. - `_translate_array_sort_by`'s inner-loop `t_load2 / t_store2` defensive check raises on real failure (the `not t_is_pair` guard is preserved — pair-typed elements legitimately have no load_op). Module path move `vera/codegen/skip.py` → `vera/skip.py`. The previous location triggered a circular import: `vera.wasm.calls_arrays` imports from `vera.codegen.skip`, but `vera.codegen.__init__` eagerly imports `vera.codegen.core` which imports from `vera.wasm`. Moving skip out of both packages (to top-level `vera/`) breaks the cycle — both `vera/codegen/` and `vera/wasm/` can now import it freely. Import-statement updates in `vera/codegen/functions.py` and `vera/codegen/closures.py`. Verification - mypy clean (59 source files) - pytest 3789 passed, 14 skipped — confirms behaviour unchanged on every well-typed input the test suite covers (no test currently exercises an unsupported-element-type path that would now raise; those paths exist as code, not as tests, and the [E602] warning emission is the observable change there). Refs #626 Co-Authored-By: Claude <noreply@anthropic.invalid>
24 silent-skip sites in `vera/wasm/data.py` converted to `raise CodegenSkip(node, reason)`. Sites by translator - `_translate_nullary_constructor`: unknown ctor lookup. - `_translate_constructor_call`: unknown ctor lookup + per-arg inference failure (now raises with the offending arg's span, not the whole ctor call's). - `_translate_let_destruct`: type-slot resolution + WASM-type resolution per binding. - `_translate_match`: scrutinee inference failure + empty-arms guard. The empty-arms case is audit-classed as borderline (type-check rejects empty match, but a cross-module import could still hand us one); converted with a top-level guard in `_translate_match` that runs before `_compile_match_arms` so the diagnostic carries the MatchExpr's span. - `_translate_match_condition`: nullary/constructor pattern ctor lookup, plus nested-tag-check propagation. - `_setup_match_arm_env`: BindingPattern slot-name failure + ConstructorPattern ctor lookup + unsupported pattern fallthrough. - `_extract_constructor_fields`: BindingPattern slot-name + field WASM-type + nested ctor lookup + unknown sub-pattern fallthrough. - `_sub_pattern_wasm_type`: BindingPattern slot-name (the only branch that's a silent skip; wildcard / no-field branches stay as `return None` since callers handle the legitimate "no type here" case). - `_collect_nested_tag_checks`: per-field WASM-type failure + nested ctor lookup. - `_translate_array_lit`: element-type inference + memory size + store-op. - `_translate_index_expr`: index element-type inference + memory size + load-op. Verification - mypy clean - pytest 3789 passed, 14 skipped Refs #626 Co-Authored-By: Claude <noreply@anthropic.invalid>
… Phase 3c) 11 silent-skip sites in `vera/wasm/calls_containers.py` converted to `raise CodegenSkip(call, reason)`. All 11 sites follow the same pattern: the translator calls `_map_wasm_tag(type)` on a key/value/element vera_type and bails when the helper returns None (which is the case for Array-typed K, V, or element — #475 finding 5 area). Pre-conversion every site was a one-line `return None` with a `# Map<K, Array<T>> not supported` marker comment; post-conversion each raises CodegenSkip anchored to the relevant FnCall so the [E602] diagnostic points at the call expression rather than the enclosing function. Sites covered (one per Map/Set operation that touches K/V/elem): - `_translate_map_insert` — key + value type tagging - `_translate_map_get` — key type tagging (×2: K and V independently) - `_translate_map_contains` — key type tagging - `_translate_map_remove` — key type tagging - `_translate_map_keys` — key type tagging - `_translate_map_values` — value type tagging - `_translate_set_add` — element type tagging - `_translate_set_contains` — element type tagging - `_translate_set_remove` — element type tagging - `_translate_set_to_array` — element type tagging The helper `_map_wasm_tag` (line 380) continues to return None for the Array case; the audit classified it as OPTIONAL_RETURN since its callers handle None and the empty-collection paths still need the helper's None-allowing branch (uninferred-type fallthrough to the ``"b"`` tag is the documented behaviour for `set_new()` / `map_keys(map_new())` round-trips). Verification - mypy clean - pytest 3789 passed, 14 skipped Refs #626 Co-Authored-By: Claude <noreply@anthropic.invalid>
…hase 3d) 9 silent-skip sites in `vera/wasm/calls_handlers.py` converted to `raise CodegenSkip(node, reason)`. Sites by translator - `_translate_show`: arg-type inference failure + unsupported show target (vera_type not in _SHOW_DISPATCH and not String/ Unit/Decimal). - `_translate_hash`: arg-type inference failure + unsupported hash target. - `_translate_handle_expr`: effect isn't an EffectRef + handler type not State/Exn. - `_translate_handle_state`: State<T> type_arg isn't a NamedType. - `_translate_handle_exn`: Exn<E> type_arg isn't a NamedType + zero-clauses guard. The "arg-instrs is None" propagate at line 87 (now post-conversion) is preserved as `return None` since it's pure forwarding. Same for the post-translate-block None checks in `_translate_handle_state` and `_translate_handle_exn`. Verification - mypy clean - pytest 3789 passed, 14 skipped Refs #626 Co-Authored-By: Claude <noreply@anthropic.invalid>
… 3 Phase 3e+3f) 5 silent-skip sites converted across the two central dispatcher files in vera/wasm/. `vera/wasm/context.py` (4 sites) - `translate_expr` — dispatch fallthrough at the end of the ``isinstance(...)`` chain. Pre-conversion an unknown AST node (e.g. a new expr kind added to grammar but not wired up here) silently fell through to `return None`. Now raises CodegenSkip with the expression's source span. - `translate_block` LetStmt — `_type_expr_to_slot_name` returned None for the binding's declared type. - `translate_block` LetStmt — `_slot_name_to_wasm_type` returned None for the resolved slot name. - `translate_block` — unknown statement type at the end of the ``isinstance(stmt, ...)`` chain. `vera/wasm/calls.py` (1 site) - `_translate_call` — the "call target not in module" guard rail. Reachable in cross-module / prelude-mangling edge cases (e.g. the #604 option_map mono-suffix mismatch where mono rewrites the callee name but the rewritten name isn't in `_known_fns`). Borderline site NOT converted: `vera/codegen/functions.py::_compile_fn` line 29 (`_is_compilable(decl)` rejection). The audit flagged this as borderline; on inspection `_is_compilable` itself calls `self._warning(...)` in every False-returning branch, so the diagnostic is already emitted upstream. This is PROPAGATE — the caller's `return None` after a False is pure forwarding. No conversion needed. Total SILENT_SKIPs converted so far: 104 (55 + 24 + 11 + 9 + 5). 105 was the audit estimate; the borderline functions.py site was re-classified post-audit as PROPAGATE, leaving 104. All SILENT_SKIPs identified by the audit are now loud. Verification - mypy clean - pytest 3789 passed, 14 skipped Refs #626 Co-Authored-By: Claude <noreply@anthropic.invalid>
CHANGELOG: add an Unreleased entry for the Layer 3 work (infrastructure + 104 SILENT_SKIP conversions, with the follow-up #657 reference for the remaining INVARIANT_DEFENSIVE and PROPAGATE buckets). ROADMAP: delete the row for #626 from the stabilisation tier since Layer 3 closes it. Renumber the remaining stabilisation rows (#596 → 2, #597 → 3, etc.) and the agent-integration tier rows (#222 → 8, #523 → 9, #370 → 10). #597's "Pairs with #626" note adjusted to past tense since #626 has closed. Refs #626 #657 Co-Authored-By: Claude <noreply@anthropic.invalid>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughAdds ChangesSilent-Skip Exception Infrastructure and Top-Level Integration
Sequence DiagramsequenceDiagram
participant Boundary as _compile_fn/_compile_lifted_closure
participant Translator as ctx.translate_block (wasm translators)
participant Skip as CodegenSkip
participant Invariant as CodegenInvariantError
participant Diagnostic as emit (CodeGenerator._warning/_error)
Boundary->>Translator: request translation of body
alt translator cannot translate
Translator->>Skip: raise CodegenSkip(node, reason)
Skip->>Boundary: exception
Boundary->>Diagnostic: emit E602 with node.span
Boundary-->>Boundary: return None
else translator hits invariant
Translator->>Invariant: raise CodegenInvariantError(msg, node?)
Invariant->>Boundary: exception
Boundary->>Diagnostic: emit E699 with inv.node.span via _error()
Boundary-->>Boundary: return None
else success
Translator-->>Boundary: return body_instrs
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #658 +/- ##
==========================================
+ Coverage 90.95% 91.03% +0.08%
==========================================
Files 59 60 +1
Lines 23197 23134 -63
Branches 259 259
==========================================
- Hits 21098 21060 -38
+ Misses 2092 2067 -25
Partials 7 7
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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.
Inline comments:
In `@vera/codegen/functions.py`:
- Around line 228-243: The CodegenInvariantError except-block is missing the
interpolation-failure harvest that the CodegenSkip handler performs; before
calling self._warning in the CodegenInvariantError handler, invoke
self._harvest_interp_inference_failures(ctx) (using the same ctx variable used
in the CodegenSkip path) so any prior interpolation inference failures are
collected, then proceed to call self._warning with the same parameters
(inv.node/decl, message, rationale, error_code="E699") and return None as
before.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: e509ffb7-69b5-440e-b9f3-0a72260d3463
⛔ Files ignored due to path filters (1)
docs/llms-full.txtis excluded by!docs/**
📒 Files selected for processing (12)
CHANGELOG.mdROADMAP.mdvera/codegen/closures.pyvera/codegen/functions.pyvera/errors.pyvera/skip.pyvera/wasm/calls.pyvera/wasm/calls_arrays.pyvera/wasm/calls_containers.pyvera/wasm/calls_handlers.pyvera/wasm/context.pyvera/wasm/data.py
CodeRabbit symmetry catch: the `CodegenInvariantError` catch-block in `_compile_fn` emits the `[E699]` warning without first calling `self._harvest_interp_inference_failures(ctx)`, whereas the parallel `CodegenSkip` catch-block does call it before the `[E602]` warning. Empirically invariant violations fire early — before any interpolation translation has populated `ctx._interp_inference_failures` — so this is mostly insurance. But keeping the two handlers structurally identical is the right principle: any time we add a new failure side-channel above either path, we want both to drain it. Added the harvest call plus a comment explaining the symmetry reasoning. Refs #626 #658 Co-Authored-By: Claude <noreply@anthropic.invalid>
The Layer 3 audit (`#626`) classified `if helper_returned_None: raise CodegenSkip` checks as SILENT_SKIP without verifying that the helpers can actually return None for the codepaths reaching them. On inspection a number of these defensive guards are unreachable: `_element_mem_size` / `_element_wasm_type` / `_element_load_op` / `_element_store_op` all fall back to a non-None default for non-pair types, and every call site is inside an `else:` branch of `if X_is_pair:` (so the helper's only None-returning input — pair types — never reaches it). The raises themselves remain in place — they correctly become defensive guards that fire only if a helper signature is ever loosened. But they shouldn't count against patch coverage, so each gets `# pragma: no cover` with an inline rationale pointing at the specific helper's fallback behaviour. Pragmas added (all in `vera/wasm/calls_arrays.py`): - 6 sites: `if X_size is None:` after `_element_mem_size(...)` — the helper has a `return 4` final fallback for any non-primitive non-pair type, so this branch is unreachable. - 3 sites: `if X_wasm is None:` after `_element_wasm_type(...)` — the helper has a `return "i32"` final fallback, same shape. - 9 sites: `if X_load is None:` / `if X_store is None:` inside `else:` branches of `if X_is_pair:` — load/store helpers return None only for pair types, guarded above. - 4 sites: `if X_load is None or X_store is None:` combined checks with the same guarding. - 2 sites inside `_array_elem_triad_or_skip` helper itself: legs 2 (size) and 3 (wasm-type) are dead by the same reasoning. Leg 1 (`_infer_concat_elem_type` returns None) remains coverable. Total: 24 pragma annotations. No code paths removed — all raises remain as defensive guards. `calls_arrays.py` per-file coverage: 93% → 94%. Discovery noted in the follow-up `#657` audit table as Track 1 input: these dead guards become candidates for removal once `_element_mem_size` / `_element_wasm_type` signatures tighten from `int | None` / `str | None` to `int` / `str` (which would accurately reflect their actual return contracts). Refs #626 #658 #657 Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@vera/wasm/calls_arrays.py`:
- Around line 1686-1688: The skip-reason currently uses the internal role string
"array_any_all" so diagnostics show helper names instead of source-level
builtins; modify the callers (array_any and array_all) to pass their actual
names (e.g., role="array_any" or role="array_all") into
_array_elem_triad_or_skip and ensure the helper uses that role value when
composing skip reasons (and similarly for the shared common helper path
currently using "array_any_all_common") so [E602] messages mention array_any or
array_all rather than the internal helper name.
- Around line 504-507: The CodegenSkip raised for arr_arg in the array_slice
path currently tells users a non-empty array literal is required; change the
diagnostic text to a neutral inference-failure message so it covers both literal
and non-literal cases (this is the branch that can fail when
_infer_concat_elem_type() cannot recover the element type). Update the raise
CodegenSkip(...) message for array_slice to something like "could not infer
array_slice element type (provide an explicit element type or ensure elements
are recoverable)" so it no longer implies only literals are accepted.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: df54f2fe-5083-4295-960f-8df2c72a18bc
📒 Files selected for processing (1)
vera/wasm/calls_arrays.py
Both findings are reason-string quality on user-facing [E602] diagnostics — the same lens the comment-analyzer flagged in the PR review, refined to specific sites. CR-2 (line 1688): `_translate_array_any_all_common` is a shared implementation helper for `array_any` and `array_all`. It was passing `role="array_any_all"` into `_array_elem_triad_or_skip` and emitting `"no load op for array_any_all_common element"` in its body load-op raise. Both strings leaked the internal helper name into user-visible diagnostics. Fix: thread a `name: str` kwarg through the helper from each caller (`name="array_any"` / `name="array_all"`), use it for the triad helper's `role=` and the body load-op raise. Diagnostics now mention `array_any` or `array_all` — the source-level builtin the user wrote — rather than the internal dispatch helper. CR-3 (lines 504-507): `_translate_array_slice`'s inference-failure raise used to read "could not infer array_slice element type (non-empty array literal required)". The parenthetical implied the fix was "use a literal", but the branch fires whenever `_infer_concat_elem_type()` can't recover the element type — that includes non-literal expressions that happen to be inference-opaque, not just non-empty literals. Fix: rephrase to "(provide an explicit element type or ensure elements are recoverable from the expression)". Neutral on the literal/non-literal distinction; covers both reachable paths. Validation - mypy clean - pytest 3789 passed, 14 skipped Refs #626 #658 Co-Authored-By: Claude <noreply@anthropic.invalid>
Multi-agent review summaryRan the TL;DR
Findings already addressed in this PR
Findings outstanding — awaiting disposition
Type-design suggestions — deferred to polish follow-upThese are valid but not blocking; rolling them into the #657 Track 1 follow-up would be coherent:
Strengths flagged
Next stepAwaiting direction on whether to proceed with F1, F2, F3, F5, F6 + S1 as a single follow-up commit (all small, total ~80 lines including the 2 tests), and how to handle F4 (E699 severity). F8 will be noted in #657 regardless. |
Multi-agent PR review (code-reviewer + comment-analyzer + silent-failure-hunter + type-design-analyzer + pr-test-analyzer) surfaced 9 important findings. Three were already addressed in prior commits (CR-1 in f1765bb, CR-2/CR-3 in 09bb9e4). Eight addressed here; one (F8) noted on #657 as pre-existing. F1 (code-reviewer + comment-analyzer) Stale module path comment in `vera/codegen/functions.py:206`. The module lives at `vera/skip.py` (top-level, deliberately, to break a circular import — see #658 description). Comment now points at the correct path. Also tightened the Phase 3 reference to name #657 instead of the vaguer "audit-and-convert pass". F2 (code-reviewer) `_compile_lifted_closure`'s `CodegenInvariantError` handler in `vera/codegen/closures.py:397-406` was missing the `_harvest_interp_inference_failures(ctx)` call that f1765bb added to the parallel `_compile_fn` handler. Symmetry restored — both boundaries now drain `ctx._interp_inference_failures` before the warning/error emission so no E615s get silently dropped if a future invariant fires mid-interp-translation. F3 (code-reviewer) `CodegenInvariantError` is defined and caught but never raised in this PR — the catch blocks are uncovered code that confuses future readers about whether the handlers are reachable. Added `# pragma: no cover` with an inline rationale to both catch blocks (`functions.py` + `closures.py`) noting that no production code raises yet and the handler exists as the catch-side contract for the Track 2 work in #657. F4 (silent-failure-hunter) — ESCALATED The `[E699]` handlers were emitting at `severity="warning"`, which is wrong for a compiler-bug diagnostic: `vera compile` would exit 0 with a soft message that CI logs can mask. Added a new `_error()` method on `vera/codegen/core.py::CodeGenerator` (parallel to the existing `_warning()`, same signature, hardcodes `severity="error"`). Both `_compile_fn` and `_compile_lifted_closure` now route `CodegenInvariantError` through `_error` for [E699]. The skip.py docstring's "never caught, always crash" claim is now backed by an error-severity diagnostic, not a warning. This is a user-visible CLI contract change for any program that ever triggers an `[E699]` (none currently exist in production — no `raise CodegenInvariantError` site has shipped — but the catch handler is now contract-correct for when Track 2 wires them). CHANGELOG entry updated. F5 (comment-analyzer) `vera/skip.py` docstring said "the follow-up issue (filed when #626 closes)" — #657 is now filed, named directly. F6 (comment-analyzer) `vera/skip.py` docstring claimed "~30 silent-skip sites out of ~367 return None". Stale pre-audit estimate. Replaced with the audit's actual aggregate counts (372 total: 105 SILENT_SKIP, 154 PROPAGATE, 74 OPTIONAL_RETURN, 39 INVARIANT_DEFENSIVE — bucket status per cleanup track). F8 (silent-failure-hunter) `vera/codegen/compilability.py:60-63` has two pre-existing silent `return False` paths that don't call `self._warning(...)`. Audit classification of `functions.py:29` (PROPAGATE) assumed `_is_compilable` always emits a diagnostic on failure; those two branches violate that assumption. Noted in #657 as Phase 4 cleanup — not introduced by this PR, so not in scope here. S1 (pr-test-analyzer) Added new test class `TestE602NodeLevelReasons626Layer3` in `tests/test_codegen.py` with two tests pinning the user-visible Layer 3 contract: * `test_e602_description_contains_node_specific_reason` — asserts the `[E602]` description carries the CodegenSkip's `reason` text and the AST-node-type label (`FnCall`), not the legacy generic "function body contains unsupported expressions". * `test_e602_location_points_at_offending_call_not_fn_header` — asserts the diagnostic's `location.line` points at the offending call (line 5 in the test fixture) rather than the enclosing function declaration (line 2). Both tests use the existing `_compile()` helper + the existing `Map<Nat, Array<Nat>>` fixture (same as `TestMapArrayValueRejected475`). The fixture passes type-check, fails at codegen, and the assertions verify the post-#658 diagnostic shape. Validation - mypy clean (59 source files) - pytest 3791 passed, 14 skipped (was 3789 + 2 new tests) - The 2 new tests fail against the pre-Layer-3 codepath, which is the locking-in property — any regression that drops the per-node span or generic-reason text will break them. Refs #626 #657 #658 Co-Authored-By: Claude <noreply@anthropic.invalid>
Update: all review findings addressed in
|
| # | Finding | Disposition | Where |
|---|---|---|---|
| F1 | Stale vera/codegen/skip.py → vera/skip.py path comment |
Fixed | 0205f9c vera/codegen/functions.py:206 |
| F2 | _compile_lifted_closure's CodegenInvariantError handler missing harvest call |
Fixed (parallel to f1765bb on the fn-boundary) | 0205f9c vera/codegen/closures.py:397-417 |
| F3 | CodegenInvariantError never raised; uncovered catch blocks |
Fixed — # pragma: no cover + inline rationale on both catches |
0205f9c vera/codegen/functions.py:228, vera/codegen/closures.py:397 |
| F4 | E699 emitted at severity="warning" — wrong for a compiler bug | Escalated to severity="error". New _error() method on CodeGenerator (parallel to _warning); both E699 catch handlers route through it. vera compile now exits non-zero on CodegenInvariantError instead of emitting a maskable warning. No production code raises CodegenInvariantError yet, so no existing programs see new errors — but the catch contract is now correct for when Track 2 of #657 wires raises. |
0205f9c vera/codegen/core.py:233-263 (new _error), vera/codegen/functions.py:242, vera/codegen/closures.py:407 |
| F5 | vera/skip.py "filed when #626 closes" → "#657" |
Fixed (named explicitly) | 0205f9c vera/skip.py:46 |
| F6 | vera/skip.py "~30/~367" stale pre-audit counts |
Fixed — replaced with the audit aggregate (372 total: 105 SILENT_SKIP / 154 PROPAGATE / 74 OPTIONAL_RETURN / 39 INVARIANT_DEFENSIVE) + per-bucket status | 0205f9c vera/skip.py:31-50 |
| F8 | _is_compilable (compilability.py:60-63) has 2 pre-existing silent return False paths that contradict the audit's PROPAGATE reclassification |
Noted on #657 as Phase 4 cleanup — pre-existing behaviour, audit-classification fix. Comment thread | (no code change in this PR) |
| S1 | Add tests for the user-visible E602 improvement | Added new TestE602NodeLevelReasons626Layer3 class with 2 tests: one for reason-text + AST-node-type label in the description, one for span-attachment. Both pass; both would fail against the pre-Layer-3 codepath, locking in the regression-detection contract. |
0205f9c tests/test_codegen.py:15835-15931 |
Validation
- mypy clean (59 source files)
- pytest 3791 passed, 14 skipped (was 3789 + 2 new)
- doc-count hook satisfied (TESTING.md / ROADMAP.md updated for the +2 tests)
Type-design suggestions S2-S9
Still deferred to a polish follow-up per the earlier framing — these don't surface correctness issues and 0205f9c is the natural stopping point for this PR. Happy to take direction if you'd rather fold them in.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 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.
Inline comments:
In `@CHANGELOG.md`:
- Line 13: Update the CHANGELOG bullet to remove the contradictory "No existing
call sites change" by clarifying that the new _error() API
(vera/codegen/core.py) is used by the internal handlers _compile_fn and
_compile_lifted_closure (the [E699] catch paths) so internal diagnostics now
emit severity="error", and note explicitly that no external/user-facing call
sites changed (if that is true) or else state that only those internal handlers
were updated; reference _error(), _warning(), _compile_fn,
_compile_lifted_closure and [E699] in the revised sentence.
In `@tests/test_codegen.py`:
- Around line 15934-15939: The assertion for the E602 diagnostic span is too
loose (assert loc_line > 2) and should be tightened to assert the exact
offending line; update the test that checks loc_line (the E602 assertion in the
test around the `main` fixture that triggers `map_insert(...)`) to assert
loc_line == 5 and update the failure message accordingly so the test pins the
diagnostic to the `map_insert(...)` call rather than any later statement.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: fe1438fc-951e-4b14-891f-ac3d6d054369
📒 Files selected for processing (8)
CHANGELOG.mdROADMAP.mdTESTING.mdtests/test_codegen.pyvera/codegen/closures.pyvera/codegen/core.pyvera/codegen/functions.pyvera/skip.py
Both findings are precision-tightening on artefacts added in `0205f9c`. CR-4 (CHANGELOG line 13) The new `_error()` bullet ended with "No existing call sites change", which contradicted the rest of the bullet — the [E699] catch handlers in `_compile_fn` and `_compile_lifted_closure` both DO change (route through `_error()` instead of `_warning()`). The intended meaning was "no user-facing API surface changes", but the loose shorthand was misleading. Fix: rewrite to be explicit about what changed (the two named internal handlers, both routed through the new `_error()` method), and qualify the "no other call sites" claim to "no user-facing API surface or other `_warning()` call sites". CR-5 (test_codegen.py line 15939) The span assertion in `test_e602_location_points_at_offending_call_not_fn_header` used `assert loc_line > 2` — too loose. A regression that drifted the catch-handler's span onto `IO.print(...)` (line 6) instead of `map_insert(...)` (line 5) would slip past the test silently. Fix: tighten to `assert loc_line == 5`. Verified by running the fixture through `_compile()` and reading `d.location.line` directly (5, column 31 — the start of the `map_insert(...)` call). Updated the assertion failure message to call out both regression paths the loose check allowed: dropping back to the legacy enclosing-fn span (line 2) AND drifting to any later call. Validation - pytest tests/test_codegen.py::TestE602NodeLevelReasons626Layer3 -v → 2 passed - The full suite was last verified green in 0205f9c; this commit only touches comment text + an assertion bound, no production code Refs #626 #658 Co-Authored-By: Claude <noreply@anthropic.invalid>
Summary
CodegenSkip/CodegenInvariantErrorinfrastructure invera/skip.pywith catch handlers at the_compile_fn/_compile_lifted_closureboundaries.return Nonesites invera/codegen/**andvera/wasm/**and classified each into SILENT_SKIP / PROPAGATE / OPTIONAL_RETURN / INVARIANT_DEFENSIVE buckets.raise CodegenSkip(node, reason). Each now surfaces as a source-located[E602]pointing at the unsupported AST node, rather than the generic enclosing-function-level[E602]the legacy propagate-None path produced.Closes #626.
What's in this PR
Built up across 7 commits, each independently green so
git bisectworks:bf30a0f— Phase 1: infrastructure (vera/skip.py, catch handlers in_compile_fn+_compile_lifted_closure, newE699error code). Behaviourally inert: no translator raises yet.41c5c7c— Phase 3a: convert 55 SILENT_SKIPs incalls_arrays.py. Introduces a shared_array_elem_triad_or_skip(arr_arg, *, role)helper that centralises the(_infer_concat_elem_type, _element_mem_size, _element_wasm_type)triad every array combinator runs upfront. Also fixes a circular import discovered during this commit —vera/codegen/skip.py→vera/skip.py.a10d202— Phase 3b: convert 24 SILENT_SKIPs indata.py(constructors / patterns / let-destruct / match arms / array literals / index expressions).1290e35— Phase 3c: convert 11 SILENT_SKIPs incalls_containers.py(all Map<K, Array> / Set<Array> rejection points — the# Map<K, Array<T>> not supported (#475 finding 5)markers).5523db4— Phase 3d: convert 9 SILENT_SKIPs incalls_handlers.py(show/hash dispatch + State/Exn handle expressions).f9da64b— Phase 3e+3f: convert 5 SILENT_SKIPs incontext.py+calls.py(central dispatcher fallthroughs + call-target guard rail). Also documents one borderline site (functions.py L29) reclassified as PROPAGATE since_is_compilablealready emits its own diagnostics.dffbc3e— Phase 4 docs: CHANGELOG entry, ROADMAP cleanup (delete Cross-cutting: convert 'translate returns None → silent skip' failures into loud diagnostics #626 row, renumber).The 39 INVARIANT_DEFENSIVE sites and the 154 PROPAGATE sites that may now be unreachable are tracked in #657.
Test plan
vera checkandvera verify[E602]/[E604]allowlist entries needed (the existing 11 entries from PR Layer 1 (+ Layer 2 status note) of #626: pre-commit gate against unexpected [E602]/[E604] silent skips #656 are unchanged — none of the SILENT_SKIP conversions newly fire for any tested input)Design notes
Why exceptions, not return-value Union types. The catch handler at
_compile_fnneeds to attach enclosing-function context (name, declaration span) to the diagnostic, but the translator that detects the unsupported shape doesn't know that context. Exceptions let the translator carry just the local information (node+reason) and let the catch handler enrich it. AResult[list[str], CodegenSkip]return type would force every translator-call site to either propagate or explicitly handle the skip, which is what the legacyif x is None: return Nonepropagation chain already does and the bug class #626 is about.Why
vera/skip.pyand notvera/errors.py. EveryVeraErrorsubclass carries a fully-formedDiagnosticat construction time. Codegen skips need to defer diagnostic construction until the catch handler can attach enclosing-function context. Different lifecycle, different module. Also avoids a circular import:vera.wasm.calls_arraysraisesCodegenSkip, andvera.codegen.__init__eagerly importsvera.codegen.corewhich imports fromvera.wasm. Top-levelvera/skip.pybreaks the cycle cleanly.Why not just unify with
_warning(...)calls. Half the legacy "silent skip" sites are several translator-call layers deep inside the WASM mixin chain. Calling_warning(...)and thenreturn Noneworks at the leaf, but the warning has the wrong span (the leaf doesn't know the enclosing-function context). The exception mechanism lets the leaf say "I couldn't translate this specific AST node, here's why" and the catch handler builds the structured diagnostic from both pieces of information.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
New Features
Tests
Documentation