Tier A bug burn-down: #633 + #634 + #556 + #591#649
Conversation
Two small post-PR-#631 cleanups, both surfaced during that PR's review: - **#633** — `_resolve_base_type_name` in `vera/wasm/inference.py` now carries an explicit `_seen` cycle-detection accumulator, restoring consistency with the sibling `_canonical_named_type` walker that already had one. Defence-in-depth: cyclic type aliases are user errors that should be rejected upfront by the type checker (filed as #648 — the upstream `RecursionError` observed while validating #633's reachability), but a bug in the upstream rejection must not turn into a stack-blow inside codegen. New unit test exercises the guard directly via injected cyclic `_type_aliases` (two-, three-, and self-cycle cases). - **#634** — `SlotRef` and other AST nodes constructed inside `InterpolatedString.parts` now carry source spans in original-source coordinates instead of synthetic-wrapper coordinates. `_parse_interp_expr` previously wrapped each segment in a dummy `fn interpExpr() { <SEG> }` for parsing, placing the segment at wrapper line 3, col 3 — and Lark spans emitted inside the parsed body were never translated back, so diagnostics on interpolated expressions landed on line 3 of the user's file regardless of where the string literal actually was. `_split_interpolation` now records each segment's offset within the raw string; `string_lit` computes the segment's original line/column from the outer string-literal span; and `_parse_interp_expr` walks the parsed AST and remaps every `Span` via a new `_remap_spans_inplace` helper (uses `object.__setattr__` to bypass frozen-dataclass guard — safe because `Node.span` is declared with `compare=False` precisely to allow this kind of late correction). The softened assertion in `TestE615LoudInterpolationFallthrough630::test_e615_fires_on_adt_in_interpolation` is tightened to pin both line (8) **and** column (15) of the diagnostic at the SlotRef's position, and `test_multiple_e615_in_one_interpolation` now pins per-segment column fidelity — two SlotRefs in the same string get two distinct columns (15 and 37) matching their actual source positions. KNOWN_ISSUES.md: removed entries for #633 and #634; added entry for #648. Closes #633. Closes #634. 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:
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughAdds cycle detection to alias resolution, remaps interpolated-expression spans to original-source coordinates for correct diagnostic pinning, hardens UTF‑8 decoding in host/provider code paths, and updates tests, changelog and known-issues metadata. ChangesCycle-Detection Fix (Issue
Interpolation Span-Remapping Fix (Issue
UTF‑8 Hygiene in Codegen/API (Issue
Regression Tests & Documentation / Metadata
Sequence Diagram(s)(No sequence diagrams generated.) Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested labels
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #649 +/- ##
==========================================
- Coverage 90.97% 90.95% -0.02%
==========================================
Files 59 59
Lines 23137 23197 +60
Branches 259 259
==========================================
+ Hits 21048 21098 +50
- Misses 2082 2092 +10
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:
|
PR #649's CI lint step (`ruff check --select S vera/`) flagged two S101 violations on the new `assert isinstance(segment, ...)` calls in `string_lit`. S101 is the right call here — `assert` statements are stripped under `python -O`, so they shouldn't be load-bearing in production code, even when their primary purpose is mypy narrowing. Refactored to isinstance-based dispatch, which provides both the mypy narrowing and runtime safety without the assert. The alternating-layout contract of `_split_interpolation` is now enforced by the if/else structure rather than asserted, which is the more honest expression of the invariant. Co-Authored-By: Claude <noreply@anthropic.invalid>
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
vera/transform.py (1)
228-258:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSkip escaped pairs while scanning non-interpolation text.
_has_interpolation()skips escaped characters, but_split_interpolation()does not. That means a valid string like"...\\(literal) ... \(@Int.0)"is mis-split once a real interpolation exists later: the second backslash of\\(is reinterpreted as an opener, so the literal fragment becomes a bogus interpolation segment.Suggested fix
while i < len(raw): if raw[i] == "\\" and i + 1 < len(raw) and raw[i + 1] == "(": # Flush literal buffer parts.append("".join(buf)) buf = [] @@ parts.append((expr_text, i)) i = j else: - buf.append(raw[i]) - i += 1 + if raw[i] == "\\" and i + 1 < len(raw): + buf.append(raw[i]) + buf.append(raw[i + 1]) + i += 2 + else: + buf.append(raw[i]) + i += 1🤖 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/transform.py` around lines 228 - 258, _split_interpolation currently treats a single '\' as potential interpolation opener while scanning non-interpolation text, causing sequences like "\\(" to be mis-parsed; change the scanning loop in _split_interpolation to skip escaped pairs the same way _has_interpolation does: when encountering a backslash and there is a next character, append both characters to buf and advance i by 2 (treat as literal), only treating "\(" followed by "(" as the start of an expression; preserve existing flushing of buf into parts, error handling, and the logic that finds the matching ')' and empty-expression checks (use the same variables raw, i, buf, parts, expr_text, j, depth, meta, and error_code references).
🤖 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/inference.py`:
- Around line 1535-1545: The function _resolve_base_type_name currently returns
the revisited alias when a prefixed chain forms a cycle because it lumps "name
in _seen" with "name not in self._type_aliases"; change it to preserve and
return the original caller's alias by introducing a root_name parameter (e.g.,
def _resolve_base_type_name(self, name, _seen=frozenset(), root_name=None) and
set root_name = name on the first call), then split the guard into two checks:
if name in _seen: return root_name, and if name not in self._type_aliases:
return name; ensure all recursive calls (the calls inside the ast.RefinementType
and NamedType branches) pass the same root_name and _seen so cycles return the
original caller's name rather than the internal revisited node.
---
Outside diff comments:
In `@vera/transform.py`:
- Around line 228-258: _split_interpolation currently treats a single '\' as
potential interpolation opener while scanning non-interpolation text, causing
sequences like "\\(" to be mis-parsed; change the scanning loop in
_split_interpolation to skip escaped pairs the same way _has_interpolation does:
when encountering a backslash and there is a next character, append both
characters to buf and advance i by 2 (treat as literal), only treating "\("
followed by "(" as the start of an expression; preserve existing flushing of buf
into parts, error handling, and the logic that finds the matching ')' and
empty-expression checks (use the same variables raw, i, buf, parts, expr_text,
j, depth, meta, and error_code references).
🪄 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: 102c91fc-8acd-4d24-a687-8010a3efc2f3
📒 Files selected for processing (8)
CHANGELOG.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdtests/test_codegen.pytests/test_wasm_coverage.pyvera/transform.pyvera/wasm/inference.py
Two more Tier A items in today's bug burn-down (following #633 + #634 in the previous commits on this branch): - **#556** — calling a user-defined `@Unit`-returning function in statement position used to fail WASM validation (`type mismatch: expected a type but nothing on stack`). The bug class was already fixed by #584 in v0.0.135 (the `_is_void_expr` registry update in `vera/wasm/context.py`). The original #556 report's *exact* repro shape — a pure helper (no IO effect) followed by a unit-literal final expression — wasn't pinned by the existing conformance test `ch07_unit_fn_nontail.vera` (which covered IO-effect variants only). Added `TestUserUnitFnInStatementPosition556` with two regression cases (the pure-helper variant and the where-block variant from the follow-up comment). - **#591** — three network-response UTF-8 decode sites in `vera/codegen/api.py` no longer leak Python `UnicodeDecodeError` text into Vera-level `Result::Err` strings. Two strategies, chosen per-site by user intent: - **`Http.get` / `Http.post`** (lines 2826, 2858) — switched to `errors="replace"`. A remote server returning non-UTF-8 bytes surfaces as U+FFFD substitutions in the Ok-branch string, preserving the data. User intent for these calls is "fetch this URL"; preserving the body beats preserving the (already-rare) signal that bytes weren't cleanly UTF-8. - **`Inference.complete`** (line 756, in `_call_inference_provider`) — now catches `UnicodeDecodeError` explicitly and re-raises as `RuntimeError("Inference provider '<name>' returned a response body that is not valid UTF-8 (invalid byte at position N).")`. The `host_inference_complete` wrapper's existing `except Exception` catches this and writes the Vera-shaped message into the Err branch. Non-UTF-8 from an LLM API is genuinely broken; we want loud failure with a Vera-native message, not the `codec can't decode byte 0x...` Python form. Three structural assertions in `tests/test_runtime_traps.py::TestNetworkResponseUtf8Hygiene591`, mirroring the #589 coverage shape. KNOWN_ISSUES.md: removed entry for #591. TESTING.md, ROADMAP.md: test counts refreshed (3,797 → 3,802). Closes #556. Closes #591. Co-Authored-By: Claude <noreply@anthropic.invalid>
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 `@tests/test_runtime_traps.py`:
- Around line 2063-2079: The test's current string assertions in
test_http_post_uses_errors_replace (and the other affected assertions around the
2093-2107 range) can pass if the target text appears only in comments; change
them to assert on the actual decode-and-raise expression shape so comments can't
satisfy them. Concretely, update the checks to look for the decode call that
includes errors="replace" as part of the same expression (e.g. ensure the source
contains the substring pattern of resp.read().decode( ... , errors="replace") or
the decode call immediately followed by a raise (the specific test function
names to edit are test_http_post_uses_errors_replace and the analogous test
around the 2093-2107 block); make the assertion fail if that contiguous pattern
is not present.
🪄 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: 2b1f062c-1c8f-40a2-8136-168477568721
📒 Files selected for processing (7)
CHANGELOG.mdKNOWN_ISSUES.mdROADMAP.mdTESTING.mdtests/test_codegen.pytests/test_runtime_traps.pyvera/codegen/api.py
Two valid findings, both fixed with new regression coverage:
**Finding 1 — `_resolve_base_type_name` returned the revisited
cycle node, not the caller's original name, on prefix-chain
cycles.**
For aliases `{A: B, B: C, C: B}` (A leads INTO a B↔C cycle that
doesn't include A itself), calling with `A` previously returned
`B` (the revisited cycle node where `name in _seen` first hit),
not `A` (the caller's input). My initial test only covered FULL
cycles (`A→B→A`, `A→B→C→A`, `X→X`) where caller-name == revisited-
name by definition, hiding the bug.
Fix: thread a `_root_name` parameter through the recursion (set
to the original `name` on first call, preserved across recursion).
The cycle guard now returns `_root_name`, so the function's
identity-on-cycle contract is "return what you were asked about,
unchanged".
Added a regression test case covering the prefix-chain shape:
`_resolve_base_type_name("A")` with `{A: B, B: C, C: B}` must
return `"A"`, not `"B"`.
**Finding 2 — `_split_interpolation` mis-segmented escaped
backslashes.**
`_has_interpolation` correctly skipped escaped pairs (so a string
like `"\\("` was seen as having no interpolation at all), but
`_split_interpolation` rescanned the second character as a fresh
start and mis-parsed the `\\(` as the opener of an interpolation
segment. Result for `"a\\(b)c"`: a parse-time crash (no matching
`)`) where the user expected a literal `a\(b)c`.
Pre-existing bug (not introduced by this PR), but CodeRabbit
flagged it because PR #649 touches the function. Fixed
opportunistically since the scope is small and the
two-helpers-must-agree invariant is exactly what the file already
encodes.
Fix: aligned `_split_interpolation`'s escape-skipping with
`_has_interpolation`'s shape — when a `\` is followed by any
character other than `(`, both characters are appended to the
literal buffer and `i` advances by 2 so the escaped character is
not re-scanned as a potential interpolation opener.
Added `test_escaped_backslash_before_paren_is_literal` to
`TestStringInterpolation` exercising the `"a\\(b)c"` case
end-to-end.
Doc counts refreshed (3,802 → 3,803).
Co-Authored-By: Claude <noreply@anthropic.invalid>
|
@coderabbitai Both valid — fixed in b623aaf. Finding 1 ( Finding 2 ( |
|
Tip For best results, initiate chat on the files or code changes.
For Finding 1, threading For Finding 2, the two-helpers-must-agree invariant is the right way to think about it. Since |
CodeRabbit-flagged: the previous assertions in
`TestNetworkResponseUtf8Hygiene591::test_http_post_uses_errors_replace`
checked the bare substring `'errors="replace"'`, which also
appears in the explanatory comment block immediately above the
real decode call. A future regression that removed
`errors="replace"` from the actual decode while leaving the
comment intact would pass the test spuriously.
`test_inference_complete_catches_unicode_decode_error` wasn't
vulnerable in current code (the production `except` keyword does
not appear in nearby comments — they use backtick-quoted
`` `UnicodeDecodeError` ``) but a future comment containing the
literal substring `except UnicodeDecodeError` would break it.
Both tests now use `re.search` with patterns that bind the target
phrase to the surrounding code expression:
- **http_post**: the regex
`r'resp\.read\(\)\.decode\([^)]*errors="replace"'` requires
`errors="replace"` to be part of the same `resp.read().decode(...)`
call. `[^)]*` permits the multi-line form (with `"utf-8",`
argument on its own line) while forbidding any closing paren
between `decode(` and `errors="replace"` — so a comment placed
outside the call cannot satisfy the match.
- **inference**: two regexes, one for the contiguous
`except UnicodeDecodeError ... raise RuntimeError(` block
(up to 10 lines between to allow whitespace and the `from None`
trailer), one for the `not valid UTF-8` phrase inside the
`raise RuntimeError(...)` argument string. The DOTALL-flag
bounded `.{0,400}?` permits adjacent-literal f-string
concatenation across multiple lines (the production code
splits the message across three `f"..."` for readability).
Validated the regexes reject the comment-only-stub failure mode
with a direct counter-example test (substituted a body where
`errors="replace"` lives only in a comment and the decode is
strict-mode; regex correctly returned None).
Added `import re` to the test module's imports.
Co-Authored-By: Claude <noreply@anthropic.invalid>
|
@coderabbitai Valid finding on For Both tests now use |
|
Tip For best results, initiate chat on the files or code changes.
For For |
Recovery PR for the release-prep work that should have been part of #649 (Tier A bug burn-down) but was missed. Per the project's established pattern since v0.0.8: release prep belongs in the feature PR; post-merge is tag + release ONLY. I broke that rule on #649; this PR is the recovery. Release prep: - **Version bump** across the six tracked sites (0.0.143 → 0.0.144): `vera/__init__.py` (`__version__` + `version`), `pyproject.toml`, `README.md` (active-development line: 143 → 144 releases, 3,796 → 3,803 tests), `docs/index.html` (hero version-link span), `HISTORY.md` (new row + "58 active development days" + "144 tagged releases"), and `uv.lock`. - **CHANGELOG cut** — renamed `## [Unreleased]` to `## [0.0.144] - 2026-05-11`, added a fresh empty `## [Unreleased]`, and added the `[0.0.144]` link reference plus bumped the `[Unreleased]` link comparison base. Limitations sweep (KNOWN_ISSUES.md, vera/README.md, spec/*): - **KNOWN_ISSUES.md** — removed the `#595` Bug row (closed 2026-05-07; the runtime-workaround entry referencing it has been reworded to drop the now-redundant "closes #595" parenthetical, since #595 was closed independently of the workaround being removed). Removed the `#605` Documentation row (closed 2026-05-07; `decreases` doc gap fixed). - **spec/00-introduction.md** — removed the `#226 Typed holes` row from the "Future Features" table (closed; typed holes shipped). - **vera/README.md** — checked, no stale references. - **spec/06-contracts.md**, **spec/08-modules.md**, **spec/12-runtime.md** limitation tables — all entries verified open via `scripts/check_limitations_sync.py --check-states`. All consistency checks green: - `python scripts/check_version_sync.py` — 0.0.144 consistent across 6 files - `python scripts/check_doc_counts.py` — counts consistent - `python scripts/check_limitations_sync.py --check-states` — 34 in KNOWN_ISSUES.md, 18 in vera/README.md, 4 across spec chapters; all live-state-verified No code changes. Pure release-prep + limitations-table sweep. Co-Authored-By: Claude <noreply@anthropic.invalid>
v0.0.144 release prep (recovery — should have been in #649)
Summary
Tier A bug burn-down — four bug closures in one PR. All small, all independent, all pure-quality fixes (no breaking changes, no new user-facing behaviour beyond
[E615]diagnostic accuracy).Closes #633 — defensive cycle guard
_resolve_base_type_name(invera/wasm/inference.py) now carries an explicit_seencycle-detection accumulator, restoring consistency with the post-#630_canonical_named_typewalker that already had one. Defence-in-depth: cyclic type aliases are user errors that should be rejected upfront by the type checker (filed separately as #648 — the upstreamRecursionErrorobserved while validating #633's reachability), but a bug in the upstream rejection must not turn into a stack-blow inside codegen.Closes #634 — SlotRef span fidelity
SlotRefand other AST nodes constructed insideInterpolatedString.partsnow carry source spans in original-source coordinates instead of synthetic-wrapper coordinates._parse_interp_exprpreviously wrapped each segment in a dummyfn interpExpr() { <SEG> }for parsing, placing the segment at wrapper line 3, col 3 — and Lark-emitted spans inside the parsed body were never translated back. The result:[E615]diagnostics on interpolated expressions landed on line 3 of the user's file regardless of where the string literal actually was.Fix:
_split_interpolationnow records each segment's offset within the raw string;string_litcomputes the segment's original line/column from the outer string-literal span;_parse_interp_exprwalks the parsed AST and remaps everySpanvia a new_remap_spans_inplacehelper.Test assertions tightened:
test_e615_fires_on_adt_in_interpolationnow pins both line (8) and column (15) of the diagnostic at the SlotRef's position;test_multiple_e615_in_one_interpolationpins per-segment column fidelity (two SlotRefs in the same string get distinct columns 15 and 37 matching their actual source positions).Closes #556 — Unit-fn-in-statement-position regression pin
The user-visible bug class (calling a user-defined
@Unit-returning function in statement position trips WASM validation withtype mismatch: expected a type but nothing on stack) was already fixed by #584 in v0.0.135 (_is_void_exprinvera/wasm/context.pynow recognises user-defined@Unitfns via_fn_ret_types). The specific repro shape from the original #556 report — a pure helper (no IO effect) followed by a unit-literal final expression — wasn't pinned by the existing conformance testch07_unit_fn_nontail.vera(which covered IO-effect variants only). AddedTestUserUnitFnInStatementPosition556with two cases: the exact #556 repro plus the where-block variant from the follow-up comment.Closes #591 — HTTP / Inference UTF-8 hygiene
Three network-response decode sites in
vera/codegen/api.pyno longer leak PythonUnicodeDecodeErrortext into Vera-levelResult::Errstrings. Two strategies, chosen per-site by user intent:Http.get/Http.post—errors="replace". A remote server returning non-UTF-8 bytes surfaces as U+FFFD substitutions in the Ok-branch string, preserving the data. User intent for these calls is "fetch this URL"; preserving the body beats preserving the (already-rare) signal that bytes weren't cleanly UTF-8.Inference.complete—_call_inference_providernow catchesUnicodeDecodeErrorexplicitly and re-raises asRuntimeError("Inference provider '<name>' returned a response body that is not valid UTF-8 (invalid byte at position N)."). Non-UTF-8 from an LLM API is genuinely broken; we want loud failure with a Vera-native message, not thecodec can't decode byte 0x...Python form.Three structural assertions in
tests/test_runtime_traps.py::TestNetworkResponseUtf8Hygiene591, mirroring the #589 coverage shape.What I discovered along the way
While verifying #633's reachability claim, I confirmed that cyclic type aliases (
type A = B; type B = A;) currently passvera checkcleanly then crashvera compilewith aRecursionErrorfromvera/codegen/core.py:_type_expr_to_wasm_type. Filed as #648 — the proper fix is a post-registration cycle-detection pass invera/checker/registration.pyemitting[E132]with location and rationale. The #633 cycle guard is belt-and-braces for that family.Burn-down context
Today's bug-burn-down: started at 99 open issues, this PR closes 4, files 1 (#648), and the upcoming spec docs PR will close ~3 more. Net trajectory: 99 → ~93.
Test plan
pytest tests/— 3,802 passed, 14 skippedmypy vera/— Success: no issues found in 58 source filespython scripts/check_conformance.py— All 86 conformance programs passpython scripts/check_examples.py— All 34 examples pass (check + verify)python scripts/check_doc_counts.py— Documentation counts consistent (3802 tests)ruff check --select S vera/— All checks passedCloses #633.
Closes #634.
Closes #556.
Closes #591.
🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests
Documentation