Skip to content

fix: unique wrapper param names for nested currying#5979

Open
prql-bot wants to merge 1 commit into
mainfrom
fix/issue-5978
Open

fix: unique wrapper param names for nested currying#5979
prql-bot wants to merge 1 commit into
mainfrom
fix/issue-5978

Conversation

@prql-bot

@prql-bot prql-bot commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Problem

Currying a function through more than two let-bound wrappers produced the wrong result. From #5978:

let f1 = func a b c -> a + b + c
let f2 = func a b -> f1 a b
let f3 = func a -> f2 a

from t | derive {
  y = (((f3 100) 20) 3),
  z = ((f2 100 20) 3),
}

y and z should be equivalent, but y compiled to 100 + 20 + 20 instead of 100 + 20 + 3c picked up the value of b.

Root cause: when a function returns a partially-applied function, materialize_function synthesizes a wrapper whose params were named by a per-materialization index (_partial_0, _partial_1, …). Nested partial applications share the NS_PARAM namespace, so these names collided across levels and a reference resolved to the wrong binding. (The reporter's analysis pinpointed this exactly, including why the 4-param case happened to work — the colliding param landed on _partial_1 there, not _partial_0.)

Solution

Name the wrapper params with the resolver's global id generator (self.id.gen()) so they are unique across materializations and can't collide in the shared namespace. This mirrors how extract_partial_application already derives its _partial_ names from a unique arg.id.

Testing

Added test_nested_currying in integration/sql.rs, which fails before the fix (100 + 20 + 20 AS y) and passes after. Verified both the 3-param and 4-param reproductions from the issue now compile correctly, and task prqlc:pull-request passes with no regressions.


Closes #5978 — automated triage

When a function returns a partially-applied function, the resolver
materializes a wrapper whose synthesized params were named by a
per-materialization index (_partial_0, _partial_1, ...). Nested partial
applications share the NS_PARAM namespace, so these names collided across
levels and a reference resolved to the wrong binding — currying through
more than two let-bound wrappers produced the wrong result.

Name the wrapper params with the resolver's global id generator so they
are unique across materializations.

Closes #5978

Co-Authored-By: Claude <noreply@anthropic.com>
@max-sixty

Copy link
Copy Markdown
Member

@tantmd lmk if you could have a look, thank you!

@tantmd

tantmd commented Jun 5, 2026

Copy link
Copy Markdown

Ehh, this will fix the immediate issue, and the code look okay. But I'm not sure about the deeper implication here. E.g. this won't fix the issue of partial param leaking into function scope (less of an issue now as id is way higher)
CleanShot 2026-06-05 at 14 37 07@2x

A proper fix would need to look into the wrapper mechanism or func.env (?), which I do not have enough infos to give input.

@prql-bot

prql-bot commented Jun 5, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks @tantmd — that's a fair distinction, and you're right that this PR doesn't change the underlying design.

To be explicit about the scope: this is a minimal regression fix. The synthesized wrapper params still live in the shared NS_PARAM namespace alongside user params — moving from a per-materialization index to self.id.gen() only makes collisions improbable (and keeps it consistent with how extract_partial_application already mints _partial_{arg.id}); it doesn't prevent them. There's no reservation on the _partial_ prefix, so a user-defined param literally named _partial_<n> could in principle still collide. So I'd frame this as "fixes the reported regression and removes the systematic collision," not "closes the structural issue."

A proper fix would isolate the wrapper params from user scope — e.g. a dedicated namespace for synthesized partials, or threading them through func.env rather than NS_PARAM — but as you note that needs a clearer picture of the wrapper mechanism than I can confidently claim here, and it's a larger change than a triage fix should bundle in.

@max-sixty — happy to either land this as the targeted fix and open a follow-up issue tracking the namespace-isolation work, or to dig into the deeper refactor if you'd prefer that here. Your call.

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.

Weird currying behavior when returning a partially-applied function from another function

3 participants