Standard prelude: eliminate boilerplate data declarations (#289)#323
Conversation
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>
|
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:
📝 WalkthroughWalkthroughIntroduce a standard prelude that implicitly injects Changes
Sequence DiagramsequenceDiagram
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 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)
📝 Coding Plan
Comment |
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 Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
|
@coderabbitai Any comments? |
There was a problem hiding this comment.
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 | 🟠 MajorDo 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, orResultMapFncan survive whilearray_map,option_map, orresult_mapis 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
⛔ Files ignored due to path filters (18)
docs/index.htmlis excluded by!docs/**examples/base64.verais excluded by!**/*.veraexamples/closures.verais excluded by!**/*.veraexamples/effect_handler.verais excluded by!**/*.veraexamples/generics.verais excluded by!**/*.veraexamples/pattern_matching.verais excluded by!**/*.veraexamples/string_ops.verais excluded by!**/*.veraexamples/url_encoding.verais excluded by!**/*.veraexamples/url_parsing.verais excluded by!**/*.veratests/conformance/ch02_generics.verais excluded by!**/*.veratests/conformance/ch02_option_result.verais excluded by!**/*.veratests/conformance/ch04_match_nested.verais excluded by!**/*.veratests/conformance/ch04_parse_completeness.verais excluded by!**/*.veratests/conformance/ch09_base64.verais excluded by!**/*.veratests/conformance/ch09_option_result_combinators.verais excluded by!**/*.veratests/conformance/ch09_prelude.verais excluded by!**/*.veratests/conformance/ch09_url_encoding.verais excluded by!**/*.veratests/conformance/ch09_url_parsing.verais excluded by!**/*.vera
📒 Files selected for processing (18)
CHANGELOG.mdCLAUDE.mdCONTRIBUTING.mdREADME.mdROADMAP.mdSKILL.mdTESTING.mdpyproject.tomlscripts/check_skill_examples.pyscripts/check_spec_examples.pyspec/09-standard-library.mdtests/conformance/manifest.jsontests/test_prelude.pyvera/README.mdvera/__init__.pyvera/codegen/core.pyvera/codegen/registration.pyvera/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
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chainLet me pull up the changed lines in both files to assess the coverage gaps. 🏁 Script executed: Length of output: 10769 Let me check the actual line numbers to pinpoint the gaps precisely. 🏁 Script executed: Length of output: 7717 🏁 Script executed: Length of output: 806
|
…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>
|
Codecov coverage addressed in 79b4511:
|
|
@coderabbitai Regarding the atomic bundle injection concern (lines 337-357): The current design is intentional and has two layers of protection:
This is correct behavior: Making bundles fully atomic would be worse: if a user defines just |
|
@coderabbitai Regarding the atomic bundle injection concern (lines 337-357): The current design is intentional and has two layers of protection:
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. |
|
@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. |
|
Tip For best results, initiate chat on the files or code changes.
✏️ Learnings added
|
|
Tip For best results, initiate chat on the files or code changes.
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
|
|
Tip For best results, initiate chat on the files or code changes.
For the remaining gaps, the standard Python idiom for intentional defensive guards / unreachable-by-design branches is # 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
returnThis 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
|
There was a problem hiding this comment.
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 | 🟡 MinorFix 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
📒 Files selected for processing (5)
README.mdTESTING.mdspec/09-standard-library.mdtests/test_prelude.pyvera/prelude.py
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>
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
tests/test_prelude.py (1)
233-241: 🧹 Nitpick | 🔵 TrivialConsider tightening the assertion.
The assertion
len(user_fns) >= 1is intentionally permissive because when the user defines a standardOption, the prelude also injectsoption_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+ customoption_map: both exist (prelude + user).- If user defines non-standard
Option+ customoption_map: only user's exists.Consider adding a comment clarifying why
>= 1is 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 | 🟡 MinorUpdate version reference from v0.0.92 to v0.0.93.
Line 514 references
v0.0.92, butvera/__init__.pyandpyproject.tomlare both at0.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
📒 Files selected for processing (4)
README.mdTESTING.mdtests/test_prelude.pyvera/prelude.py
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>
|
@coderabbitai Both findings from review 3980380566 addressed in 4863250:
|
|
Tip For best results, initiate chat on the files or code changes.
Thanks for the thorough follow-through on both. 🧠 Learnings used |
Summary
Option<T>,Result<T, E>,Ordering, andUrlPartsare now always available without explicitdatadeclarations. 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.data Option/Result/UrlPartsdefinitions.ch09_prelude.verawith 17 test functions covering prelude types and combinators without local definitions. Conformance suite: 55 → 56 programs.datadefinitions 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
vera/prelude.pyvera/codegen/core.py,vera/codegen/registration.pytests/test_prelude.py.verafilesdatadeclarationsch09_prelude.vera,manifest.json.verafilesdatadeclarationsspec/09-standard-library.mdTest plan
pytest tests/ -q)check_conformance.py)check_examples.py)vera fmt --check)🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests
Documentation
Chores