Skip to content

FilterFilter et al: inline predicates, skip multi-statement bodies#16

Open
JesseHerrick wants to merge 2 commits intomainfrom
filter-reject-inline-predicates
Open

FilterFilter et al: inline predicates, skip multi-statement bodies#16
JesseHerrick wants to merge 2 commits intomainfrom
filter-reject-inline-predicates

Conversation

@JesseHerrick
Copy link
Copy Markdown
Member

@JesseHerrick JesseHerrick commented May 7, 2026

The four pipe-collapse rules (FilterFilter, RejectReject, FilterReject, RejectFilter) used to wrap each predicate as f.(item) no matter what, producing nasty output like (&(&1 in list)).(item) && (fn x -> ... end).(item) whenever the source used a capture or anonymous fn.

Now they reuse the MapMap rule's inlining machinery to substitute the iteration variable directly into the predicate body, and skip the rewrite entirely when either side has a multi-statement fn body — combining those under &&/|| would change short-circuit semantics and read worse than the original two-pipe chain.


Note

Medium Risk
Changes AST rewrite behavior for collapsing Enum.filter/Enum.reject pipes, which could affect generated code semantics if edge cases are missed despite added safety checks.

Overview
Improves the FilterFilter/RejectReject/FilterReject/RejectFilter pipe-collapsing rewrites to inline simple capture/fn predicate bodies (instead of always emitting f.(item) calls), producing cleaner merged predicates.

Adds guardrails so the collapse is skipped when predicates aren’t safely composable (e.g., multi-statement fn bodies, non-inlineable forms) or when the chosen parameter name would shadow a free variable in either predicate; tests are expanded to cover the new inlining behavior and the new skip cases.

Reviewed by Cursor Bugbot for commit ac3c752. Bugbot is set up for automated code reviews on this repo. Configure here.

The four pipe-collapse rules (FilterFilter, RejectReject, FilterReject,
RejectFilter) used to wrap each predicate as `f.(item)` no matter what,
producing nasty output like `(&(&1 in list)).(item) && (fn x -> ... end).(item)`
whenever the source used a capture or anonymous fn.

Now they reuse the MapMap rule's inlining machinery to substitute the iteration
variable directly into the predicate body, and skip the rewrite entirely when
either side has a multi-statement `fn` body — combining those under `&&`/`||`
would change short-circuit semantics and read worse than the original two-pipe
chain.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit efc71c9. Configure here.

Comment thread lib/style/pipes.ex
bugbot caught that combined_predicate hardcoded `:item` as the merged lambda's
parameter without checking whether either predicate closed over a variable
named `item` — the new lambda would silently shadow it.

Apply the same machinery MapMap already uses: prefer the source's own
iteration name (from a `fn x -> ...` predicate) when present, and check
shadows_free_var? before committing. If the chosen name would shadow a free
var on either side, bail out and leave the chain untouched. Also extend
free_var_in? to recognize bare-reference predicates (e.g. `Enum.filter(list, item)`)
as shadow risks — MapMap already filters these out before reaching the check,
so its behavior is unchanged.
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