Skip to content

Standard prelude: eliminate boilerplate data declarations (#289)#323

Merged
aallan merged 7 commits into
mainfrom
feat/standard-prelude
Mar 20, 2026
Merged

Standard prelude: eliminate boilerplate data declarations (#289)#323
aallan merged 7 commits into
mainfrom
feat/standard-prelude

Conversation

@aallan

@aallan aallan commented Mar 20, 2026

Copy link
Copy Markdown
Owner

Summary

  • Unconditional prelude injectionOption<T>, Result<T, E>, Ordering, and UrlParts are now always available without explicit data declarations. Combinators (option_map, option_unwrap_or, option_and_then, result_map, result_unwrap_or) and array operations (array_map, array_filter, array_fold) are also always injected.
  • Boilerplate removed from 8 conformance programs and 8 examples that previously required local data Option/Result/UrlParts definitions.
  • New conformance test ch09_prelude.vera with 17 test functions covering prelude types and combinators without local definitions. Conformance suite: 55 → 56 programs.
  • User shadowing preserved — local data definitions override prelude versions. Non-standard variants (e.g. data Option<T> { None, Just(T) }) correctly suppress combinator injection.

Closes #289. Version bump to v0.0.93.

Changed files

Area Files Change
Compiler vera/prelude.py Unconditional ADT + combinator injection
Compiler vera/codegen/core.py, vera/codegen/registration.py DataDecl registration in Pass 1.2, UrlParts pre-registration
Tests tests/test_prelude.py Rewritten: 12 → 19 tests for unconditional injection
Conformance 8 .vera files Removed boilerplate data declarations
Conformance ch09_prelude.vera, manifest.json New conformance test
Examples 8 .vera files Removed boilerplate data declarations
Spec spec/09-standard-library.md §9.1.2 Standard Prelude, updated §9.3.3 and §9.3.7
Docs SKILL.md, vera/README.md, CONTRIBUTING.md, TESTING.md Prelude documentation, updated counts
Release CHANGELOG.md, ROADMAP.md, version files, docs/index.html v0.0.93

Test plan

  • All 2,419 tests pass (pytest tests/ -q)
  • All 56 conformance programs pass (check_conformance.py)
  • All 25 examples pass (check_examples.py)
  • All doc validation scripts pass (spec, SKILL.md, README, FAQ, HTML examples)
  • mypy clean
  • All .vera files formatted (vera fmt --check)
  • Version sync verified
  • Doc counts verified
  • 18/18 pre-commit hooks pass

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Standard prelude now implicitly provides Option, Result<T, E>, Ordering and UrlParts, plus combinators and array/utility operations.
  • Tests

    • Conformance suite increased to 56 programs; added conformance manifest entry and new prelude end-to-end and unit tests; parametrised test counts adjusted.
  • Documentation

    • Changelog, README, roadmap, spec, testing guides and docs updated to describe prelude behaviour and revised test totals.
  • Chores

    • Package version bumped to 0.0.93.

Option<T>, Result<T, E>, Ordering, and UrlParts are now unconditionally
injected by the prelude — programs no longer need local `data`
declarations for these types. Combinators (option_map, result_unwrap_or,
array_fold, etc.) are also always available. User-defined data
declarations shadow the prelude; non-standard variants correctly
suppress combinator injection.

Boilerplate removed from 8 conformance programs and 8 examples.
New ch09_prelude conformance test (56 total). v0.0.93.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Note

Reviews paused

It 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 reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Introduce a standard prelude that implicitly injects Option<T>, Result<T, E>, Ordering, UrlParts and array combinators into every Vera program; user-defined types with the same names shadow the prelude. Bump version to 0.0.93, add one conformance program (total 56), and update tests, docs and allowlist line references.

Changes

Cohort / File(s) Summary
Docs & Changelog
CHANGELOG.md, README.md, ROADMAP.md, SKILL.md, TESTING.md, CONTRIBUTING.md, CLAUDE.md, vera/README.md
Documented the standard prelude, release 0.0.93, updated conformance/program & unit test counts and CI comments (conformance → 56).
Version Metadata
pyproject.toml, vera/__init__.py
Bumped package version from 0.0.920.0.93.
Specification
spec/09-standard-library.md
Added §9.1.2: standard prelude semantics, injected ADTs, combinators, array ops, and shadowing rules.
Prelude Injection Engine
vera/prelude.py
Always prepend prelude data decls (Option, Result, Ordering, UrlParts); combinators/array ops injected according to shadowing and strict structural checks; added helpers _is_type_param_ref, _user_defined_data_names and _PRELUDE_DATA.
Codegen Registration
vera/codegen/registration.py
Registered built-in UrlParts ADT layout (single constructor with five string-like fields and computed total size).
Codegen Core
vera/codegen/core.py
compile_program now snapshots existing ADTs and registers injected DataDecls to avoid duplicate ADT registrations (adds data registration alongside functions).
Tests & Conformance
tests/test_prelude.py, tests/conformance/manifest.json
Reworked prelude tests (ADT/combinator/alias/end-to-end) to reflect unconditional ADT injection and shadowing rules; added ch09_prelude manifest entry and new conformance program.
Scripts (allowlists)
scripts/check_skill_examples.py, scripts/check_spec_examples.py
Updated many allowlist line-number mappings to match shifted docs; no parsing or behaviour changes.
Other internal docs
vera/README.md
Added prelude behaviour notes; aligned various internal docs to new prelude and test counts.

Sequence Diagram

sequenceDiagram
    participant Program as Program AST
    participant Prelude as Prelude (inject_prelude)
    participant Codegen as Codegen (compile_program)
    participant Reg as RegistrationMixin (_register_builtin_adts)

    Program->>Prelude: provide AST with user declarations
    activate Prelude
    Note over Prelude: compute user_defined_data_names<br/>verify _has_standard_option/_has_standard_result
    Prelude->>Prelude: prepend ADT decls (Option, Result, Ordering, UrlParts)
    Prelude->>Prelude: inject combinator & array-op functions unless shadowed/non-standard
    Prelude->>Program: return augmented AST
    deactivate Prelude

    Program->>Codegen: compile augmented AST
    activate Codegen
    Codegen->>Codegen: snapshot existing_adts/existing_fns
    Codegen->>Reg: request builtin ADT registration (includes UrlParts)
    activate Reg
    Reg->>Reg: add UrlParts layout entry
    deactivate Reg
    Codegen->>Codegen: register injected DataDecls/FnDecls/TypeAliasDecls (skip duplicates)
    deactivate Codegen
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested labels

compiler, tests, spec, ci, docs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: implementing a standard prelude that eliminates boilerplate data declarations for Option, Result, Ordering, and UrlParts.
Linked Issues check ✅ Passed All coding requirements from issue #289 are met: unconditional prelude injection for Option/Result/Ordering/UrlParts, combinator injection, user shadowing semantics, UrlParts pre-registration, tests expanded to 56 programs, and version bumped to 0.0.93.
Out of Scope Changes check ✅ Passed All changes are directly scoped to issue #289: prelude injection implementation, conformance suite expansion, documentation updates, version bumps, and related test infrastructure. No unrelated modifications detected.
Docstring Coverage ✅ Passed Docstring coverage is 86.21% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/standard-prelude
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Three spec allowlist entries existed at both old (shifted) and new
(manually added) line numbers. Remove the stale duplicates.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@codecov-commenter

codecov-commenter commented Mar 20, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.95%. Comparing base (d7fec62) to head (4863250).
⚠️ Report is 8 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #323      +/-   ##
==========================================
+ Coverage   90.91%   90.95%   +0.03%     
==========================================
  Files          45       45              
  Lines       15394    15416      +22     
==========================================
+ Hits        13996    14021      +25     
+ Misses       1398     1395       -3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@aallan

aallan commented Mar 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Any comments?

@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.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
vera/prelude.py (1)

337-357: ⚠️ Potential issue | 🟠 Major

Do not partially inject wrappers against user-shadowed helper names.

This logic filters prelude declarations one name at a time, so a user-defined support symbol like array_map_go, OptionMapFn, or ResultMapFn can survive while array_map, option_map, or result_map is still injected. The injected wrapper will then resolve against the user symbol rather than the matching prelude helper, which can change behaviour or fail type checking. Please either reserve internal prelude names or inject each wrapper/helper/alias bundle atomically.

Also applies to: 362-374

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vera/prelude.py` around lines 337 - 357, The current per-name checks can
partially inject bundles and let user-defined helpers shadow internal names;
instead, treat each bundle atomically: for option combinators use a single check
that ensures all names in {_OPTION_TYPE_ALIASES names + _OPTION_COMBINATORS
names} are absent from user_names before appending either aliases or combinators
(use option_fn_names ∪ option_alias_names or explicit bundle name sets), and
similarly for result (use result_fn_names ∪ result_alias_names and _RESULT_*
bundles) and array (array_fn_names ∪ array_alias_names and _ARRAY_* bundles);
change the logic so you either inject the entire alias+combinator bundle or skip
it, guarding these branches behind inject_option_combinators /
inject_result_combinators and apply the same atomic check at the other
occurrence mentioned (lines ~362-374).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@spec/09-standard-library.md`:
- Line 202: Update the sentence that claims the combinator functions are “always
available” to qualify it: state they are provided by the standard prelude (see
§9.1.2) but may be suppressed for non-standard Option<T>/Result<T, E>
declarations and are omitted when the prelude is shadowed (e.g., vera/prelude.py
skips combinators if the user defines a function with the same name); reference
§9.3.7 for the shadowing behavior so the paragraph remains accurate when the
prelude is shadowed or custom types are used.

In `@tests/test_prelude.py`:
- Around line 135-154: Add two more regression cases alongside
test_non_standard_option_skips_combinators: one that defines a non-standard
Result type (e.g. public data Result<T,E> { OkValue(T), ErrValue(E) } or renamed
constructors) and asserts that Result combinators from inject_prelude are not
injected (use _RESULT_FN_NAMES), and one that defines a standard-named type with
the wrong shape (e.g. Option with an extra constructor or wrong arity) and
asserts Option combinators are not injected (use _OPTION_FN_NAMES) while other
prelude items like _ARRAY_FN_NAMES still are; create these programs via
_make_program, call inject_prelude(prog), gather names with _fn_names(prog), and
assert the appropriate sets are/are not subsets/intersections as in the existing
test.

In `@vera/prelude.py`:
- Around line 310-320: The current gating uses
_has_standard_option/_has_standard_result which only checks constructor names;
change the logic so the gates require an exact ADT shape (constructor names and
arities) before enabling combinators: update or replace
_has_standard_option/_has_standard_result to validate that the type named
"Option" (and "Result") in program has exactly the expected constructors and
payload arities (e.g., Option has exactly None() and Some(T) with arity 0 and 1;
Result has exactly Ok(T) and Err(E) with arities 1 and 1), or introduce new
helpers like _is_exact_standard_option(_program) /
_is_exact_standard_result(_program) and use those in computing
inject_option_combinators and inject_result_combinators instead of the current
helpers; ensure you still treat the absence of the type
(user_has_option/user_has_result) as before.

---

Outside diff comments:
In `@vera/prelude.py`:
- Around line 337-357: The current per-name checks can partially inject bundles
and let user-defined helpers shadow internal names; instead, treat each bundle
atomically: for option combinators use a single check that ensures all names in
{_OPTION_TYPE_ALIASES names + _OPTION_COMBINATORS names} are absent from
user_names before appending either aliases or combinators (use option_fn_names ∪
option_alias_names or explicit bundle name sets), and similarly for result (use
result_fn_names ∪ result_alias_names and _RESULT_* bundles) and array
(array_fn_names ∪ array_alias_names and _ARRAY_* bundles); change the logic so
you either inject the entire alias+combinator bundle or skip it, guarding these
branches behind inject_option_combinators / inject_result_combinators and apply
the same atomic check at the other occurrence mentioned (lines ~362-374).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 6e489aea-322a-4526-9121-af2d3ed0ff8e

📥 Commits

Reviewing files that changed from the base of the PR and between d7fec62 and 26f08c0.

⛔ Files ignored due to path filters (18)
  • docs/index.html is excluded by !docs/**
  • examples/base64.vera is excluded by !**/*.vera
  • examples/closures.vera is excluded by !**/*.vera
  • examples/effect_handler.vera is excluded by !**/*.vera
  • examples/generics.vera is excluded by !**/*.vera
  • examples/pattern_matching.vera is excluded by !**/*.vera
  • examples/string_ops.vera is excluded by !**/*.vera
  • examples/url_encoding.vera is excluded by !**/*.vera
  • examples/url_parsing.vera is excluded by !**/*.vera
  • tests/conformance/ch02_generics.vera is excluded by !**/*.vera
  • tests/conformance/ch02_option_result.vera is excluded by !**/*.vera
  • tests/conformance/ch04_match_nested.vera is excluded by !**/*.vera
  • tests/conformance/ch04_parse_completeness.vera is excluded by !**/*.vera
  • tests/conformance/ch09_base64.vera is excluded by !**/*.vera
  • tests/conformance/ch09_option_result_combinators.vera is excluded by !**/*.vera
  • tests/conformance/ch09_prelude.vera is excluded by !**/*.vera
  • tests/conformance/ch09_url_encoding.vera is excluded by !**/*.vera
  • tests/conformance/ch09_url_parsing.vera is excluded by !**/*.vera
📒 Files selected for processing (18)
  • CHANGELOG.md
  • CLAUDE.md
  • CONTRIBUTING.md
  • README.md
  • ROADMAP.md
  • SKILL.md
  • TESTING.md
  • pyproject.toml
  • scripts/check_skill_examples.py
  • scripts/check_spec_examples.py
  • spec/09-standard-library.md
  • tests/conformance/manifest.json
  • tests/test_prelude.py
  • vera/README.md
  • vera/__init__.py
  • vera/codegen/core.py
  • vera/codegen/registration.py
  • vera/prelude.py
👮 Files not reviewed due to content moderation or server errors (7)
  • vera/codegen/core.py
  • scripts/check_spec_examples.py
  • CHANGELOG.md
  • SKILL.md
  • TESTING.md
  • ROADMAP.md
  • scripts/check_skill_examples.py

Comment thread spec/09-standard-library.md Outdated
Comment thread tests/test_prelude.py
Comment thread vera/prelude.py
@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me pull up the changed lines in both files to assess the coverage gaps.


🏁 Script executed:

#!/bin/bash
# Show the diff for vera/prelude.py and vera/codegen/core.py on this PR branch
git diff origin/main...HEAD -- vera/prelude.py vera/codegen/core.py

Length of output: 10769


Let me check the actual line numbers to pinpoint the gaps precisely.


🏁 Script executed:

#!/bin/bash
# Show prelude.py with line numbers around the new additions
cat -n vera/prelude.py | sed -n '237,290p'
echo "---"
cat -n vera/prelude.py | sed -n '290,380p'
echo "---"
# Show core.py with line numbers around the new block
cat -n vera/codegen/core.py | sed -n '155,175p'

Length of output: 7717


🏁 Script executed:

#!/bin/bash
# Check if _has_nonstandard_data is called anywhere
rg "_has_nonstandard_data" -n

echo "==="
# Check the guard in core.py - what's in existing_adts before inject_prelude runs?
# Look at _register_data and _adt_layouts to understand when it would be pre-populated
rg "_adt_layouts" -n -A2 -B2 vera/codegen/core.py | head -60

echo "==="
# Check _register_all to understand what gets registered in Pass 1
rg "_register_all\|_register_data" -n vera/codegen/core.py | head -30

Length of output: 806


@aallan: I've dug into both files to trace the specific gaps. Here's the breakdown:


vera/prelude.py — 6 missing lines

Primary issue: _has_nonstandard_data is dead code.

# vera/prelude.py lines 240–247
def _has_nonstandard_data(program: ast.Program, name: str) -> bool:
    """Check if the program defines a data type with the given name
    that does NOT match the standard prelude shape."""
    for tld in program.declarations:
        decl = tld.decl
        if isinstance(decl, ast.DataDecl) and decl.name == name:
            return True
    return False

A rg search confirms this function is defined but never called anywhere in the codebase. The def line itself runs at import time and counts as covered, but the body lines are marked uncovered if the function is never called. That accounts for 5 of the 6 missing lines (lines 243–247). The function looks like a leftover from a refactor — the actual injection logic pivots on _has_standard_option / _has_standard_result and _user_defined_data_names instead.

The 6th missing line is most likely the continue # User's data type shadows the prelude's branch at line 368 of inject_prelude (the case where a user-defined DataDecl name collides with a prelude ADT name) not being exercised through a code path that Codecov measures.

Recommended fix:

  • Either remove _has_nonstandard_data (it duplicates what _user_defined_data_names already provides), or wire it up if it was intended to replace one of the isinstance checks.
  • Add a test_prelude.py case that verifies a program defining e.g. data Option<T> { None, Just(T) } (non-standard shape) doesn't get a shadowed combinator injected, exercised through inject_prelude directly.

vera/codegen/core.py — 1 missing line

# core.py lines 162–164
elif isinstance(decl, ast.DataDecl):
    if decl.name not in existing_adts:
        self._register_data(decl)  # ← line 164, likely uncovered branch

existing_adts is captured from self._adt_layouts before inject_prelude runs, so the prelude DataDecls (Option, Result, Ordering, UrlParts) are always new and always hit line 164. The uncovered path is the False branch of the guard — i.e., a user-defined ADT that was already registered in Pass 1 is encountered again in the post-injection loop. There's no unit test that drives CodeGenerator through the full compile path with a user-defined data declaration that shadows a prelude type (e.g. data Option<T> { None, Just(T) } compiled end-to-end).

Recommended fix: Add a codegen-level test (or conformance test) that compiles a program containing a user-defined ADT with a prelude-conflicting name.


🧠 Learnings used
Learnt from: CR
Repo: aallan/vera PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-20T07:33:16.947Z
Learning: When replying to CodeRabbit review comments, prefix replies with `coderabbitai` so the bot registers the interaction

Learnt from: CR
Repo: aallan/vera PR: 0
File: SKILL.md:0-0
Timestamp: 2026-03-20T07:33:01.566Z
Learning: Applies to **/*.vera : When calling option and result combinators, use typed bindings when the type variable appears only in the return position (e.g., use `let Option<Int> = None;` before calling `option_map`)

…ase tests

- Tighten _has_standard_option/_has_standard_result to require exact
  constructor count and arities (not just name presence). Prevents
  combinators from being injected for types like Option<T> { None,
  Some(T), Extra } where the prelude exhaustive match would fail.
- Remove unused _has_nonstandard_data helper (dead code, hurt coverage).
- Add 3 new tests: non-standard Result, extra-constructor Option,
  extra-constructor Result.
- Fix spec 9.3.7 wording: qualify availability with suppression conditions.

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Mar 20, 2026

Copy link
Copy Markdown
Owner Author

Codecov coverage addressed in 79b4511:

  • Removed unused _has_nonstandard_data helper (was dead code hurting patch coverage)
  • Added 3 new tests exercising the non-standard Result and extra-constructor branches in _has_standard_option/_has_standard_result
  • Patch coverage for vera/prelude.py now at 97% (3 remaining uncovered lines are edge cases: user-defined type alias shadowing and early return when everything is shadowed)

@aallan

aallan commented Mar 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Regarding the atomic bundle injection concern (lines 337-357):

The current design is intentional and has two layers of protection:

  1. Bundle-level gate (lines 347-367): The issubset check skips the entire bundle only when the user has defined ALL functions in the group. This prevents partial injection of the source text.

  2. Declaration-level filter (lines 374-384): After parsing, individual declarations are filtered out if the user shadows them by name. So if a user defines array_map_go but not array_map, the prelude injects array_map (which calls array_map_go), but the prelude's array_map_go is skipped — the user's version is used.

This is correct behavior: array_map calling the user's array_map_go will fail at type-check time if signatures don't match, which is the right outcome. The internal helpers (array_map_go, OptionMapFn) are private implementation details — a user defining them is deliberately shadowing and should get type errors if incompatible.

Making bundles fully atomic would be worse: if a user defines just array_map (a reasonable name), we'd skip ALL array operations including array_filter and array_fold, which would be surprising.

@aallan

aallan commented Mar 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Regarding the atomic bundle injection concern (lines 337-357):

The current design is intentional and has two layers of protection:

  1. Bundle-level gate (lines 347-367): The issubset check skips the entire bundle only when the user has defined ALL functions in the group.

  2. Declaration-level filter (lines 374-384): After parsing, individual declarations are filtered out if the user shadows them by name. So if a user defines array_map_go but not array_map, the prelude injects array_map (which calls array_map_go), but the prelude array_map_go is skipped and the user version is used.

This is correct: array_map calling a user array_map_go will fail at type-check time if signatures mismatch, which is the right outcome. Making bundles fully atomic would be worse: if a user defines just array_map (a reasonable name), we would skip ALL array operations including array_filter and array_fold.

@aallan

aallan commented Mar 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Regarding core.py line 164 coverage gap:

Investigated — this line (self._register_data(decl)) is architecturally unreachable in the current codebase. All four prelude ADTs (Option, Result, Ordering, UrlParts) are already pre-registered by _register_builtin_adts() in Pass 1, so when the Pass 1.2 loop encounters them, decl.name is always in existing_adts and the registration is skipped. The guard exists as a safety net for future prelude ADTs that might not be pre-registered.

The remaining uncovered lines in prelude.py (268, 384, 392) are similarly edge cases: user-defined type alias shadowing and the early return when every prelude declaration is shadowed. These are correct but rare paths that would require contrived test programs to exercise.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: That's a clear and well-reasoned design. The two-layer separation makes sense — bundle-level gating on the full set prevents injecting source text with broken internal references, while declaration-level filtering handles partial shadowing at parse time without over-suppressing unrelated operations. The type-checker as a correctness backstop for mismatched array_map_go signatures is the right contract boundary. I'm satisfied with the explanation.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/323

Timestamp: 2026-03-20T09:04:03.159Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (lines ~347–367): the `issubset` check skips parsing/injecting an entire source bundle only when the user has defined ALL public functions in that group, preventing partial source-text injection with broken internal references.
2. **Declaration-level filter** (lines ~374–384): after parsing, individual declarations are skipped if the user has defined a name that shadows them. This means a user defining only `array_map_go` (an internal helper) will get prelude `array_map` injected (which calls their `array_map_go`), and a type error fires if signatures are incompatible.
Making bundles fully atomic would be incorrect: a user defining `array_map` alone would suppress `array_filter` and `array_fold`, which is surprising and undesirable.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: That design rationale is clear and well-reasoned. The two-layer approach is the correct trade-off here:

  • Bundle gate as a coarse filter: skipping only when all members are user-defined avoids silently suppressing unrelated operations (e.g. not killing array_filter/array_fold just because the user happened to name something array_map).
  • Per-declaration filter as the precise override: lets individual internal helpers (array_map_go, OptionMapFn, etc.) be shadowed without pulling down the whole bundle, with the type-checker acting as the correctness backstop if signatures mismatch.

The type-error-on-mismatch safety net is exactly the right contract for a language with mypy-clean codegen. No changes needed here.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/323

Timestamp: 2026-03-20T09:04:13.416Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (around lines 347-367): an `issubset` check skips an entire bundle (e.g. all array ops) only when the user has defined ALL functions in the group — prevents partial injection of the bundle's source text.
2. **Declaration-level filter** (around lines 374-384): after parsing the bundle source, individual declarations are filtered out if the user already defines a declaration with that name — so partial shadowing of internal helpers (e.g. `array_map_go`) is supported. The prelude's public function (e.g. `array_map`) is still injected and will call the user's version; a signature mismatch is caught at type-check time.
Making bundles fully atomic (skip all if any member is shadowed) is intentionally avoided because a user defining `array_map` should not suppress `array_filter`/`array_fold`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: That's a clean architectural explanation — thanks for tracing it through. The invariant makes sense: since _register_builtin_adts() runs in Pass 1 and covers all current prelude ADTs, the not in existing_adts branch on line 164 is genuinely unreachable today and the guard is purely forward-compatibility scaffolding.

For the remaining gaps, the standard Python idiom for intentional defensive guards / unreachable-by-design branches is # pragma: no cover with an inline rationale comment, which keeps the coverage signal clean without forcing contrived test programs:

# vera/codegen/core.py
elif isinstance(decl, ast.DataDecl):
    if decl.name not in existing_adts:  # pragma: no cover — safety net for future prelude ADTs not pre-registered by _register_builtin_adts
        self._register_data(decl)
# vera/prelude.py  (lines 268, 384, 392 — type-alias shadowing and all-shadowed early return)
if ...:  # pragma: no cover — reached only when user shadows every prelude declaration
    return

This is purely cosmetic/metrics hygiene — no obligation to change it if you'd rather leave the guards uncommented and accept the small coverage delta.


✏️ Learnings added
Learnt from: aallan
URL: https://github.com/aallan/vera/pull/323

Timestamp: 2026-03-20T09:04:28.773Z
Learning: In vera/codegen/core.py, the `self._register_data(decl)` branch inside the post-prelude-injection loop (guarded by `decl.name not in existing_adts`) is architecturally unreachable: all four current prelude ADTs (Option, Result, Ordering, UrlParts) are already pre-registered by `_register_builtin_adts()` in Pass 1, so the guard always evaluates to False. The guard is a forward-compatibility safety net for future prelude ADTs that might not be pre-registered.

Learnt from: aallan
URL: https://github.com/aallan/vera/pull/323

Timestamp: 2026-03-20T09:04:28.773Z
Learning: In vera/prelude.py, the uncovered lines (user-defined type alias shadowing path and the early-return-when-every-prelude-declaration-is-shadowed path) are correct but rare edge cases that would require contrived test programs to exercise. They are intentional defensive guards, not missing test coverage.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

@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.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
spec/09-standard-library.md (1)

202-219: ⚠️ Potential issue | 🟡 Minor

Fix the section reference for domain_verb.

Line 219 points to §5, but the naming-convention rule is introduced in §9.1.1. As written, this sends readers to the functions chapter instead of the rule being cited.

As per coding guidelines, spec/**/*.md: Language specification. Review for factual accuracy and broken code examples.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spec/09-standard-library.md` around lines 202 - 219, The paragraph
incorrectly cites the naming-convention rule as "§5"; update the reference to
point to the correct section "§9.1.1" where the `domain_verb` convention is
defined (and scan the same paragraph for any other occurrences of `domain_verb`
to ensure they also reference §9.1.1); replace the `§5` citation with `§9.1.1`
and, if the repo uses internal anchors/links, update the link target to the
`9.1.1` heading so readers are directed to the naming-convention rule.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@TESTING.md`:
- Around line 303-304: Update the markdown table row for check_examples.py so
the example count matches the rest of the file: change the text "All 23 `.vera`
examples" to "All 25 `.vera` examples" in TESTING.md (the table row referencing
check_examples.py), and quickly scan other occurrences of `check_examples.py` or
the example-count text to ensure consistency.

In `@vera/prelude.py`:
- Around line 216-236: The helper _has_standard_option currently only checks
arity; update it to also verify constructor field types match the declared type
parameter: locate _has_standard_option and, when decl.name == "Option" and
len(type_params)==1, ensure the "Some" constructor has exactly one field whose
type is the same type parameter (e.g., a type variable/reference matching
decl.type_params[0]) and "None" has zero fields before returning True; apply the
same pattern for the analogous Result checker (the code around lines ~239-257)
to ensure Ok and Err field types match the two declared type parameters, and add
a regression in tests/test_prelude.py asserting that mismatched payload types
(e.g., Some(Int) when T is generic) do not count as standard.

---

Outside diff comments:
In `@spec/09-standard-library.md`:
- Around line 202-219: The paragraph incorrectly cites the naming-convention
rule as "§5"; update the reference to point to the correct section "§9.1.1"
where the `domain_verb` convention is defined (and scan the same paragraph for
any other occurrences of `domain_verb` to ensure they also reference §9.1.1);
replace the `§5` citation with `§9.1.1` and, if the repo uses internal
anchors/links, update the link target to the `9.1.1` heading so readers are
directed to the naming-convention rule.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: d99ece5e-f012-4514-96cb-e9d27b02264d

📥 Commits

Reviewing files that changed from the base of the PR and between 69ac26e and 79b4511.

📒 Files selected for processing (5)
  • README.md
  • TESTING.md
  • spec/09-standard-library.md
  • tests/test_prelude.py
  • vera/prelude.py

Comment thread TESTING.md Outdated
Comment thread vera/prelude.py Outdated
aallan and others added 2 commits March 20, 2026 09:08
Per CodeRabbit suggestion: mark defensive guards that are correct but
unreachable in the current architecture. All four prelude ADTs are
pre-registered by _register_builtin_adts(), making the codegen guard
always false. Type alias shadowing and all-shadowed early return in
prelude.py require contrived programs to exercise.

Co-Authored-By: Claude <noreply@anthropic.invalid>
_has_standard_option and _has_standard_result now verify that constructor
fields reference the declared type parameters, not concrete types. This
prevents combinators from being injected for types like
data Option<T> { None, Some(Int) } where the generic combinators would
fail to type-check against the monomorphic variant.

Added _is_type_param_ref helper and 2 regression tests for concrete
field types (Option with Some(Int), Result with Ok(Int)/Err(String)).

Also fixes TESTING.md example count (23 -> 25).

Co-Authored-By: Claude <noreply@anthropic.invalid>

@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 (2)
tests/test_prelude.py (1)

233-241: 🧹 Nitpick | 🔵 Trivial

Consider tightening the assertion.

The assertion len(user_fns) >= 1 is intentionally permissive because when the user defines a standard Option, the prelude also injects option_map. However, the comment on line 240–241 checks that "The user-defined one has no forall_vars", which correctly distinguishes the user's version. The logic is sound but could be clearer:

  • If user defines standard Option + custom option_map: both exist (prelude + user).
  • If user defines non-standard Option + custom option_map: only user's exists.

Consider adding a comment clarifying why >= 1 is used, or restructuring to explicitly count prelude vs user versions.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_prelude.py` around lines 233 - 241, The check using len(user_fns)
>= 1 is too permissive; tighten it by explicitly asserting that at least one
function exists and that exactly one of them is the user-defined variant (the
one with forall_vars is None). In practice: ensure user_fns is non-empty, assert
any(fn.forall_vars is None for fn in user_fns) (already present), and replace or
augment the len check with an assertion that counts the user-defined entry
(e.g., sum(1 for fn in user_fns if fn.forall_vars is None) == 1) and optionally
assert that any other entries have forall_vars != None to represent the
prelude-injected version; reference user_fns, prog.declarations, ast.FnDecl,
option_map, and forall_vars to locate the code to change.
README.md (1)

514-514: ⚠️ Potential issue | 🟡 Minor

Update version reference from v0.0.92 to v0.0.93.

Line 514 references v0.0.92, but vera/__init__.py and pyproject.toml are both at 0.0.93. Update the README to maintain consistency.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 514, The README currently states the project is at
"v0.0.92" while vera/__init__.py and pyproject.toml use "0.0.93"; update the
version text in the README (replace the literal "v0.0.92" used in the
descriptive paragraph) to "v0.0.93" so the human-facing release line matches the
package version references in vera/__init__.py and pyproject.toml.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@README.md`:
- Line 514: The README currently states the project is at "v0.0.92" while
vera/__init__.py and pyproject.toml use "0.0.93"; update the version text in the
README (replace the literal "v0.0.92" used in the descriptive paragraph) to
"v0.0.93" so the human-facing release line matches the package version
references in vera/__init__.py and pyproject.toml.

In `@tests/test_prelude.py`:
- Around line 233-241: The check using len(user_fns) >= 1 is too permissive;
tighten it by explicitly asserting that at least one function exists and that
exactly one of them is the user-defined variant (the one with forall_vars is
None). In practice: ensure user_fns is non-empty, assert any(fn.forall_vars is
None for fn in user_fns) (already present), and replace or augment the len check
with an assertion that counts the user-defined entry (e.g., sum(1 for fn in
user_fns if fn.forall_vars is None) == 1) and optionally assert that any other
entries have forall_vars != None to represent the prelude-injected version;
reference user_fns, prog.declarations, ast.FnDecl, option_map, and forall_vars
to locate the code to change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: bdff1ea3-e1bb-4d58-bda5-ed1e74d638f6

📥 Commits

Reviewing files that changed from the base of the PR and between 1f86f52 and 392e133.

📒 Files selected for processing (4)
  • README.md
  • TESTING.md
  • tests/test_prelude.py
  • vera/prelude.py

aallan and others added 2 commits March 20, 2026 09:23
Structurally unreachable after the constructor-count gate passes:
name-not-in-ctors (impossible with exactly 2 distinct constructors),
nullary-with-fields (None(Int)), and the final return-False fallthrough
(only called when user_has_option/result is True).

Co-Authored-By: Claude <noreply@anthropic.invalid>
… test

- README.md: update v0.0.92 -> v0.0.93 in active development paragraph
- test_prelude.py: assert exactly one user-defined option_map (no
  forall_vars) and verify prelude versions have forall_vars set
- TESTING.md: update test_prelude.py line count 403 -> 406

Co-Authored-By: Claude <noreply@anthropic.invalid>
@aallan

aallan commented Mar 20, 2026

Copy link
Copy Markdown
Owner Author

@coderabbitai Both findings from review 3980380566 addressed in 4863250:

  1. README.md: v0.0.92 -> v0.0.93 in the active development paragraph
  2. test_prelude.py: tightened shadowing test — now asserts exactly one user-defined option_map (forall_vars is None) and verifies prelude versions have forall_vars set

@coderabbitai

coderabbitai Bot commented Mar 20, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

@aallan: Confirmed — both fixes look correct:

  1. README.md — version string aligned with pyproject.toml and vera/__init__.py.
  2. test_prelude.py — the tightened shadowing assertion is a meaningful improvement: distinguishing user-defined option_map (no forall_vars) from prelude-injected versions (with forall_vars set) gives the test real structural teeth rather than just a name-presence check.

Thanks for the thorough follow-through on both.


🧠 Learnings used
Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:28.773Z
Learning: In vera/prelude.py, the uncovered lines (user-defined type alias shadowing path and the early-return-when-every-prelude-declaration-is-shadowed path) are correct but rare edge cases that would require contrived test programs to exercise. They are intentional defensive guards, not missing test coverage.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:03.159Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (lines ~347–367): the `issubset` check skips parsing/injecting an entire source bundle only when the user has defined ALL public functions in that group, preventing partial source-text injection with broken internal references.
2. **Declaration-level filter** (lines ~374–384): after parsing, individual declarations are skipped if the user has defined a name that shadows them. This means a user defining only `array_map_go` (an internal helper) will get prelude `array_map` injected (which calls their `array_map_go`), and a type error fires if signatures are incompatible.
Making bundles fully atomic would be incorrect: a user defining `array_map` alone would suppress `array_filter` and `array_fold`, which is surprising and undesirable.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:13.416Z
Learning: In `vera/prelude.py`, the prelude bundle injection uses a deliberate two-layer design:
1. **Bundle-level gate** (around lines 347-367): an `issubset` check skips an entire bundle (e.g. all array ops) only when the user has defined ALL functions in the group — prevents partial injection of the bundle's source text.
2. **Declaration-level filter** (around lines 374-384): after parsing the bundle source, individual declarations are filtered out if the user already defines a declaration with that name — so partial shadowing of internal helpers (e.g. `array_map_go`) is supported. The prelude's public function (e.g. `array_map`) is still injected and will call the user's version; a signature mismatch is caught at type-check time.
Making bundles fully atomic (skip all if any member is shadowed) is intentionally avoided because a user defining `array_map` should not suppress `array_filter`/`array_fold`.

Learnt from: aallan
Repo: aallan/vera PR: 0
File: :0-0
Timestamp: 2026-03-20T09:04:28.773Z
Learning: In vera/codegen/core.py, the `self._register_data(decl)` branch inside the post-prelude-injection loop (guarded by `decl.name not in existing_adts`) is architecturally unreachable: all four current prelude ADTs (Option, Result, Ordering, UrlParts) are already pre-registered by `_register_builtin_adts()` in Pass 1, so the guard always evaluates to False. The guard is a forward-compatibility safety net for future prelude ADTs that might not be pre-registered.

Learnt from: CR
Repo: aallan/vera PR: 0
File: SKILL.md:0-0
Timestamp: 2026-03-20T07:33:01.566Z
Learning: Applies to **/*.vera : When calling option and result combinators, use typed bindings when the type variable appears only in the return position (e.g., use `let Option<Int> = None;` before calling `option_map`)

@aallan aallan merged commit 38db3e1 into main Mar 20, 2026
12 checks passed
@aallan aallan deleted the feat/standard-prelude branch March 20, 2026 09:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standard prelude for Result, Option, and common ADT constructors

2 participants