diff --git a/prqlc/prqlc/src/semantic/resolver/functions.rs b/prqlc/prqlc/src/semantic/resolver/functions.rs index e00dceb021b1..f6cdd2d08342 100644 --- a/prqlc/prqlc/src/semantic/resolver/functions.rs +++ b/prqlc/prqlc/src/semantic/resolver/functions.rs @@ -123,10 +123,13 @@ impl Resolver<'_> { // Get the missing params (params that don't have args yet) let missing = inner_closure.params[inner_closure.args.len()..].to_vec(); - // Create wrapper params and add references to them as args to the inner closure + // Create wrapper params and add references to them as args to the inner closure. + // The param names must be globally unique: nested partial applications share the + // NS_PARAM namespace, so a per-materialization index would collide and resolve to + // the wrong binding (issue #5978). let mut wrapper_params = Vec::with_capacity(missing.len()); - for (i, param) in missing.iter().enumerate() { - let param_name = format!("_partial_{i}"); + for param in missing.iter() { + let param_name = format!("_partial_{}", self.id.gen()); let substitute_arg = Expr::new(Ident::from_path(vec![ NS_PARAM.to_string(), param_name.clone(), diff --git a/prqlc/prqlc/tests/integration/sql.rs b/prqlc/prqlc/tests/integration/sql.rs index 1f911b42efdf..31dc3744f78a 100644 --- a/prqlc/prqlc/tests/integration/sql.rs +++ b/prqlc/prqlc/tests/integration/sql.rs @@ -7385,6 +7385,33 @@ fn test_partial_application_of_transform() { "); } +/// Regression test for issue #5978: currying a function through more than two +/// `let`-bound wrappers produced the wrong result, because the wrapper params +/// synthesized during partial-application materialization were named by a +/// per-materialization index (`_partial_0`, `_partial_1`, …) and collided +/// across nested partial applications sharing the `NS_PARAM` namespace. +#[test] +fn test_nested_currying() { + // `y` (curried one arg at a time) must match `z` (curried directly). + assert_snapshot!(compile(r#" + 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), + } + "#).unwrap(), @" + SELECT + *, + 100 + 20 + 3 AS y, + 100 + 20 + 3 AS z + FROM + t + "); +} + #[test] fn test_tuple_map() { assert_snapshot!(compile(r###"