Skip to content

[checking] Generate Traversable derived members - #230

Merged
purefunctor merged 1 commit into
mainfrom
traversable-deriving
Aug 1, 2026
Merged

[checking] Generate Traversable derived members#230
purefunctor merged 1 commit into
mainfrom
traversable-deriving

Conversation

@purefunctor

Copy link
Copy Markdown
Owner

Summary

Alexandrite already validates and registers derived Traversable and Bitraversable instance heads, but it does not generate their dictionary members. This change completes those instances by generating:

  • traverse and sequence for Traversable
  • bitraverse and bisequence for Bitraversable

Constructor fields are traversed in source order and reconstructed applicatively with pure, map, and apply. Records follow canonical row order, and recursive derived instances are resolved through the instance head registered before member generation.

Implementation

  • Add a dedicated effectful traversal generator while reusing the structural recipes produced by variance analysis.
  • Generalize known-instance generation to emit every class member in declaration order, atomically, rather than assuming a one-member dictionary.
  • Generate sequence as traverse with an identity lambda and bisequence as bitraverse with two identity lambdas.
  • Register the required Applicative, Apply, Traversable, and Bitraversable known terms.
  • Model nested unary and binary applications explicitly, including the binary class needed by Bitraversable.
  • Reject function-shaped occurrences such as Int -> a, which can be mapped but cannot be traversed, during variance checking.

This follows the canonical PureScript class signatures and keeps member generation all-or-nothing when a required known term or decoded member signature is unavailable.

Tests

Semantic fixtures cover:

  • identity, constant, optional, product, nested, recursive, and record-shaped Traversable types
  • left and both-argument binary applications
  • Bitraversable sums, products, unary and binary nesting, and records
  • rejection of function-shaped traversals

Validation completed:

  • just format
  • git diff --check
  • just t checking
  • just t semantic
  • cargo check -p checking --tests
  • cargo nextest run -p checking (30 tests passed)

Stack

This is the base PR for the deriving stack. The Foldable implementation follows in a separate PR based on traversable-deriving.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Summary

  • Add derived traverse and sequence members for Traversable.
  • Add derived bitraverse and bisequence members for Bitraversable.
  • Generate members from structural variance recipes.
  • Reconstruct constructors applicatively in source order.
  • Preserve canonical row order for records.
  • Resolve recursive instances through registered instance heads.
  • Generate all known class members atomically in declaration order.
  • Model unary and binary applications explicitly.
  • Reject function-shaped occurrences such as Int -> a during variance checking.
  • Add semantic and checking fixtures for identity, constant, optional, product, nested, recursive, record, unary, binary, and function-shaped types.
  • Register required Applicative, Apply, Traversable, and Bitraversable terms.
  • Validation includes formatting, diff checks, checking tests, semantic tests, cargo check, and 30 passing cargo nextest tests.

Confidence 5/5 - Broad coverage supports a safe merge

The implementation covers the requested derivation paths and includes targeted tests for nested, recursive, record, unary, binary, and rejected function-shaped cases. The reported validation checks pass, with 30 cargo nextest tests passing.

Walkthrough

Changes

The compiler now supports derived Traversable and Bitraversable instances. It resolves required class operations, generates traversal members for constructors and records, handles nested and binary parameters, rejects unsupported function traversal, and adds integration fixtures.

Traversable derivation

Layer / File(s) Summary
Variance and recipe contracts
compiler-core/checking/src/source/derive/variance.rs, compiler-core/checking/src/source/derive/{functor,foldable,contravariant,traversable,member}.rs, compiler-core/checking/src/source/derive/generate/functor.rs
Variance configurations now include function policies and binary-class metadata. Traversal operation names now identify unary and binary applications.
Known terms and generation wiring
compiler-core/checking/src/context.rs, compiler-core/checking/src/source/derive/generate.rs
KnownTermsCore stores references to applicative and traversal operations. Known derivation dispatch generates collections of members and resolves supplied class members.
Traversal member generation
compiler-core/checking/src/source/derive/generate/traversable.rs
Traversable and Bitraversable generation decodes signatures, traverses constructors and records, propagates evidence, and rebuilds results with applicative operations.
Derivation integration fixtures
tests-integration/fixtures/checking/*, tests-integration/fixtures/semantic/*traversable*, tests-integration/fixtures/semantic/*bitraversable*
Fixtures cover simple, nested, recursive, record, binary, fixed-field, and function-containing types.

Sequence Diagram(s)

sequenceDiagram
  participant DeriveDispatch
  participant VarianceRecipes
  participant TraversableGenerator
  participant KnownTermsCore
  DeriveDispatch->>VarianceRecipes: build traversal recipes
  VarianceRecipes->>TraversableGenerator: provide traversal metadata
  TraversableGenerator->>KnownTermsCore: resolve traversal operations
  KnownTermsCore-->>TraversableGenerator: return known term references
  TraversableGenerator-->>DeriveDispatch: generate instance members
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description directly matches the changes, including derived Traversable and Bitraversable members, traversal generation, function rejection, and tests.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 78.99408% with 142 lines in your changes missing coverage. Please review.
✅ Project coverage is 83.78%. Comparing base (644eca9) to head (3bd35e0).

Files with missing lines Patch % Lines
...checking/src/source/derive/generate/traversable.rs 77.17% 70 Missing and 56 partials ⚠️
...mpiler-core/checking/src/source/derive/generate.rs 84.31% 8 Missing ⚠️
compiler-core/checking/src/context.rs 50.00% 0 Missing and 6 partials ⚠️
...mpiler-core/checking/src/source/derive/variance.rs 92.85% 1 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #230      +/-   ##
==========================================
- Coverage   83.87%   83.78%   -0.09%     
==========================================
  Files         157      158       +1     
  Lines       34050    34700     +650     
  Branches    34050    34700     +650     
==========================================
+ Hits        28560    29075     +515     
- Misses       2963     3036      +73     
- Partials     2527     2589      +62     

☔ View full report in Codecov by Harness.
📢 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.

@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: 1

🧹 Nitpick comments (2)
compiler-core/checking/src/source/derive/generate/traversable.rs (1)

1042-1046: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Resolve known_terms.apply before the loop.

The loop re-checks self.builder.context.known_terms.apply on every effect. If apply is absent, the generator has already emitted the map application, and it then discards the work. Hoisting the check above the loop fails earlier and removes the repeated lookup.

♻️ Proposed refactor
+        let Some(apply) = self.builder.context.known_terms.apply else {
+            return Ok(None);
+        };
         for effect in remaining_effects {
-            let Some(apply) = self.builder.context.known_terms.apply else {
-                return Ok(None);
-            };
             let apply = self.builder.term_reference(apply)?;
🤖 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 `@compiler-core/checking/src/source/derive/generate/traversable.rs` around
lines 1042 - 1046, Resolve and convert self.builder.context.known_terms.apply to
the term reference before entering the remaining_effects loop. If it is absent,
return Ok(None) before any map application is emitted; inside the loop, reuse
the hoisted apply value and remove the repeated lookup and conversion.
compiler-core/checking/src/source/derive/generate.rs (1)

149-194: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bind the generated member before collecting it.

The four single-member arms chain .into_iter().collect() directly onto the call result. The coding guidelines require an iterator expression to be bound to a local variable before collecting it. Binding the optional member first also removes the nested chain and makes each arm read as one step.

♻️ Proposed refactor for the single-member arms
         let members = match dispatch {
             DeriveDispatch::Eq => {
-                eq_ord::generate_eq_member(state, context, result, &freshened.arguments)?
-                    .into_iter()
-                    .collect()
+                let member =
+                    eq_ord::generate_eq_member(state, context, result, &freshened.arguments)?;
+                let members = member.into_iter();
+                members.collect()
             }
-            DeriveDispatch::Eq1 => generate_delegated_member(
-                state,
-                context,
-                result,
-                &freshened.arguments,
-                context.known_terms.eq,
-            )?
-            .into_iter()
-            .collect(),
+            DeriveDispatch::Eq1 => {
+                let member = generate_delegated_member(
+                    state,
+                    context,
+                    result,
+                    &freshened.arguments,
+                    context.known_terms.eq,
+                )?;
+                let members = member.into_iter();
+                members.collect()
+            }
             DeriveDispatch::Ord => {
-                eq_ord::generate_ord_member(state, context, result, &freshened.arguments)?
-                    .into_iter()
-                    .collect()
+                let member =
+                    eq_ord::generate_ord_member(state, context, result, &freshened.arguments)?;
+                let members = member.into_iter();
+                members.collect()
             }
-            DeriveDispatch::Ord1 => generate_delegated_member(
-                state,
-                context,
-                result,
-                &freshened.arguments,
-                context.known_terms.compare,
-            )?
-            .into_iter()
-            .collect(),
+            DeriveDispatch::Ord1 => {
+                let member = generate_delegated_member(
+                    state,
+                    context,
+                    result,
+                    &freshened.arguments,
+                    context.known_terms.compare,
+                )?;
+                let members = member.into_iter();
+                members.collect()
+            }

Based on the coding guideline "Always bind an iterator expression to a local variable before collecting or folding it."

🤖 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 `@compiler-core/checking/src/source/derive/generate.rs` around lines 149 - 194,
Update the four single-member dispatch arms in the members match—Eq, Eq1, Ord,
and Ord1—to bind each generated member result to a local variable before calling
into_iter().collect(). Preserve the existing optional-member behavior and
collected result while removing the direct chained collection from
generate_eq_member, generate_ord_member, and generate_delegated_member calls.

Source: Coding guidelines

🤖 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 `@compiler-core/checking/src/source/derive/foldable.rs`:
- Around line 44-45: Change the FunctionPolicy setting in both Foldable and
Bifoldable derivation paths from Allow to Reject, so function types containing a
derived parameter are rejected while fixed function fields remain valid. Add
checking fixtures covering unary and binary function-shaped fold derives.

---

Nitpick comments:
In `@compiler-core/checking/src/source/derive/generate.rs`:
- Around line 149-194: Update the four single-member dispatch arms in the
members match—Eq, Eq1, Ord, and Ord1—to bind each generated member result to a
local variable before calling into_iter().collect(). Preserve the existing
optional-member behavior and collected result while removing the direct chained
collection from generate_eq_member, generate_ord_member, and
generate_delegated_member calls.

In `@compiler-core/checking/src/source/derive/generate/traversable.rs`:
- Around line 1042-1046: Resolve and convert
self.builder.context.known_terms.apply to the term reference before entering the
remaining_effects loop. If it is absent, return Ok(None) before any map
application is emitted; inside the loop, reuse the hoisted apply value and
remove the repeated lookup and conversion.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b2514d86-4da7-4a0f-93b8-31e131e9afae

📥 Commits

Reviewing files that changed from the base of the PR and between 644eca9 and 3bd35e0.

⛔ Files ignored due to path filters (15)
  • tests-integration/fixtures/checking/1785484200_derive_traversable_function_rejection/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_either_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_nested_binary_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_nested_unary_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_pair_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_record_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_binary_both_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_binary_left_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_const_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_identity_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_maybe_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_nested_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_product_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_record_body/Main.snap is excluded by !**/*.snap
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_recursive_body/Main.snap is excluded by !**/*.snap
📒 Files selected for processing (25)
  • compiler-core/checking/src/context.rs
  • compiler-core/checking/src/source/derive/contravariant.rs
  • compiler-core/checking/src/source/derive/foldable.rs
  • compiler-core/checking/src/source/derive/functor.rs
  • compiler-core/checking/src/source/derive/generate.rs
  • compiler-core/checking/src/source/derive/generate/functor.rs
  • compiler-core/checking/src/source/derive/generate/traversable.rs
  • compiler-core/checking/src/source/derive/member.rs
  • compiler-core/checking/src/source/derive/traversable.rs
  • compiler-core/checking/src/source/derive/variance.rs
  • tests-integration/fixtures/checking/1785484200_derive_traversable_function_rejection/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_either_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_nested_binary_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_nested_unary_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_pair_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_bitraversable_record_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_binary_both_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_binary_left_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_const_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_identity_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_maybe_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_nested_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_product_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_record_body/Main.purs
  • tests-integration/fixtures/semantic/1785484440_derive_traversable_recursive_body/Main.purs

Comment on lines +44 to 45
function_policy: FunctionPolicy::Allow,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Find relevant files"
git ls-files | rg '(^|/)foldable\.rs$|FunctionPolicy|Foldable|Bifoldable|derive.*foldable|derive.*bifold' || true

echo
echo "Inspect foldable.rs outline"
if [ -f compiler-core/checking/src/source/derive/foldable.rs ]; then
  wc -l compiler-core/checking/src/source/derive/foldable.rs
  ast-grep outline compiler-core/checking/src/source/derive/foldable.rs --view expanded || true
  echo
  sed -n '1,140p' compiler-core/checking/src/source/derive/foldable.rs | cat -n
fi

echo
echo "Search FunctionPolicy usages"
rg -n "FunctionPolicy|foldable|bifoldable|Foldable|Bifoldable" compiler-core/checking/src -S || true

Repository: purefunctor/purescript-alexandrite

Length of output: 8411


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Inspect variance implementation"
sed -n '1,340p' compiler-core/checking/src/source/derive/variance.rs | cat -n

echo
echo "Inspect traversable derive for comparison"
sed -n '1,120p' compiler-core/checking/src/source/derive/traversable.rs | cat -n

echo
echo "Find current fixtures related to function fields in Foldable/Bifoldable"
rg -n "fn ->|-> .*Foldable|Foldable|Bifoldable|Bifunctor|traversable|Traversable" tests-integration/fixtures/checking -S || true

echo
echo "Read relevant fixture/snap files"
for d in \
  tests-integration/fixtures/checking/1772565960_derive_foldable_simple \
  tests-integration/fixtures/checking/1772566020_derive_foldable_higher_kinded \
  tests-integration/fixtures/checking/1772566200_derive_bifoldable_simple \
  tests-integration/fixtures/checking/1772566260_derive_bifoldable_higher_kinded
do
  echo "--- $d ---"
  find "$d" -maxdepth 1 -type f -print | sort | xargs -r cat -n
done

Repository: purefunctor/purescript-alexandrite

Length of output: 50390


🌐 Web query:

PureScript Foldable f => Foldable (Reader r) foldM foldMap Reader function field

💡 Result:

In PureScript, the Reader monad is defined as type Reader r = ReaderT r Identity [1][2][3]. It does not have a Foldable instance because it is a functional wrapper (a computation that depends on an environment) rather than a container of values that can be folded [4][5][3]. The Foldable type class is intended for data structures that contain elements, such as Array, Maybe, or Either [4][5]. Because Reader r a represents a function r -> a, it does not store multiple elements in a structure that foldMap or foldM can traverse [4][1][2]. If you are looking for foldMap or foldM functionality related to Reader, you may be confusing it with other concepts: 1. Monad instances: Reader r is a Monad, which allows for chaining computations (bind), but this is distinct from folding over a structure [3][6]. 2. Applicative instances: Reader r is an Applicative functor [6]. 3. Mapping: You can modify the result of a Reader computation using mapReader (which is effectively map from the Functor instance) or withReader to modify the environment r [1][2][3]. If you need to perform a fold, the structure being folded must implement the Foldable type class [4]. If your goal is to fold values within a Reader context (i.e., folding while having access to an environment), you would typically fold the underlying container and perform the monadic operations within the Reader monad using standard traverse or foldM functions adapted for monads [4][7].

Citations:


Reject function-shaped occurrences in Foldable and Bifoldable derives.

FunctionPolicy::Allow permits fields such as Int -> a, but foldMap/foldM must consume the a values in f a; a function field does not provide those values to the fold callback. Keep fixed function fields valid, but reject function types that contain a derived parameter. Apply FunctionPolicy::Reject at both derivation points and add checking fixtures for unary and binary function-shaped folds.

🤖 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 `@compiler-core/checking/src/source/derive/foldable.rs` around lines 44 - 45,
Change the FunctionPolicy setting in both Foldable and Bifoldable derivation
paths from Allow to Reject, so function types containing a derived parameter are
rejected while fixed function fields remain valid. Add checking fixtures
covering unary and binary function-shaped fold derives.

Source: MCP tools

@purefunctor
purefunctor merged commit 7849883 into main Aug 1, 2026
13 of 15 checks passed
@purefunctor
purefunctor deleted the traversable-deriving branch August 1, 2026 08:04
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.

1 participant