From 2eeb21369e648e9293b91c4f967b449b1f3dbc8e Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Tue, 4 Aug 2026 07:20:18 +0000 Subject: [PATCH 1/4] fix: don't panic on `std.not` with multiple arguments --- prqlc/prqlc/src/semantic/resolver/expr.rs | 4 ++++ .../prqlc/tests/integration/error_messages.rs | 24 +++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/prqlc/prqlc/src/semantic/resolver/expr.rs b/prqlc/prqlc/src/semantic/resolver/expr.rs index ceb68e402b6f..d92b349c48bc 100644 --- a/prqlc/prqlc/src/semantic/resolver/expr.rs +++ b/prqlc/prqlc/src/semantic/resolver/expr.rs @@ -160,8 +160,12 @@ impl pl::PlFold for Resolver<'_> { } } + // `select !{a, b}` — column exclusion. Only a single tuple argument + // is exclusion syntax; anything else is an ordinary `std.not` call + // and falls through to the arity checks below. pl::ExprKind::FuncCall(pl::FuncCall { name, args, .. }) if (name.kind.as_ident()).is_some_and(|i| i.to_string() == "std.not") + && args.len() == 1 && matches!(args[0].kind, pl::ExprKind::Tuple(_)) => { let arg = args.into_iter().exactly_one().unwrap(); diff --git a/prqlc/prqlc/tests/integration/error_messages.rs b/prqlc/prqlc/tests/integration/error_messages.rs index fb14604e0103..4e43493761d6 100644 --- a/prqlc/prqlc/tests/integration/error_messages.rs +++ b/prqlc/prqlc/tests/integration/error_messages.rs @@ -554,3 +554,27 @@ fn append_by_name_unnamed() { ───╯ "); } + +#[test] +fn not_with_extra_args() { + // A tuple argument to `std.not` is column-exclusion syntax, but only when + // it's the sole argument — extra args used to panic on `exactly_one`. + assert_snapshot!(compile(r"from x | select (std.not {a} b)").unwrap_err(), @" + Error: + ╭─[ :1:18 ] + │ + 1 │ from x | select (std.not {a} b) + │ ──────┬────── + │ ╰──────── Too many arguments to function `not` + ───╯ + "); + assert_snapshot!(compile(r"from x | select (std.not {a} {b})").unwrap_err(), @" + Error: + ╭─[ :1:18 ] + │ + 1 │ from x | select (std.not {a} {b}) + │ ───────┬─────── + │ ╰───────── Too many arguments to function `not` + ───╯ + "); +} From dd10bcf25f9849b97c7b478bc8b2b10a8bccad8e Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Tue, 4 Aug 2026 07:29:04 +0000 Subject: [PATCH 2/4] fix: don't drop named args to `std.not` The exclusion-syntax guard ignored `named_args`, so `std.not {a} b:1` took the column-exclusion branch and the named argument vanished. Requiring `named_args.is_empty()` sends it down the ordinary call path, which reports it. --- prqlc/prqlc/src/semantic/resolver/expr.rs | 16 ++++++++++------ prqlc/prqlc/tests/integration/error_messages.rs | 3 +++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/prqlc/prqlc/src/semantic/resolver/expr.rs b/prqlc/prqlc/src/semantic/resolver/expr.rs index d92b349c48bc..692ba7623a33 100644 --- a/prqlc/prqlc/src/semantic/resolver/expr.rs +++ b/prqlc/prqlc/src/semantic/resolver/expr.rs @@ -161,12 +161,16 @@ impl pl::PlFold for Resolver<'_> { } // `select !{a, b}` — column exclusion. Only a single tuple argument - // is exclusion syntax; anything else is an ordinary `std.not` call - // and falls through to the arity checks below. - pl::ExprKind::FuncCall(pl::FuncCall { name, args, .. }) - if (name.kind.as_ident()).is_some_and(|i| i.to_string() == "std.not") - && args.len() == 1 - && matches!(args[0].kind, pl::ExprKind::Tuple(_)) => + // and no named args is exclusion syntax; anything else is an ordinary + // `std.not` call and falls through to the argument checks below. + pl::ExprKind::FuncCall(pl::FuncCall { + name, + args, + named_args, + }) if (name.kind.as_ident()).is_some_and(|i| i.to_string() == "std.not") + && args.len() == 1 + && named_args.is_empty() + && matches!(args[0].kind, pl::ExprKind::Tuple(_)) => { let arg = args.into_iter().exactly_one().unwrap(); self.resolve_column_exclusion(arg)? diff --git a/prqlc/prqlc/tests/integration/error_messages.rs b/prqlc/prqlc/tests/integration/error_messages.rs index 4e43493761d6..7f6944171e22 100644 --- a/prqlc/prqlc/tests/integration/error_messages.rs +++ b/prqlc/prqlc/tests/integration/error_messages.rs @@ -577,4 +577,7 @@ fn not_with_extra_args() { │ ╰───────── Too many arguments to function `not` ───╯ "); + // A named argument isn't exclusion syntax either; it used to be dropped + // silently, compiling as though it weren't there. + assert_snapshot!(compile(r"from x | select (std.not {a} b:1)").unwrap_err(), @r#"Error: unknown named argument `b` to closure Some(["std", "not"])"#); } From e80e187098081a904bf14f7d5f042eafac61e853 Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Tue, 4 Aug 2026 07:39:38 +0000 Subject: [PATCH 3/4] test: cover `std.not` with only a named argument --- prqlc/prqlc/tests/integration/error_messages.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/prqlc/prqlc/tests/integration/error_messages.rs b/prqlc/prqlc/tests/integration/error_messages.rs index 7f6944171e22..c2c6f07e1807 100644 --- a/prqlc/prqlc/tests/integration/error_messages.rs +++ b/prqlc/prqlc/tests/integration/error_messages.rs @@ -580,4 +580,7 @@ fn not_with_extra_args() { // A named argument isn't exclusion syntax either; it used to be dropped // silently, compiling as though it weren't there. assert_snapshot!(compile(r"from x | select (std.not {a} b:1)").unwrap_err(), @r#"Error: unknown named argument `b` to closure Some(["std", "not"])"#); + // With no positional args at all, the guard used to index `args[0]` and + // panic before it got as far as `exactly_one`. + assert_snapshot!(compile(r"from x | select (std.not b:1)").unwrap_err(), @r#"Error: unknown named argument `b` to closure Some(["std", "not"])"#); } From 9264f0033a0a14a4242331977170400d0bc0b5d7 Mon Sep 17 00:00:00 2001 From: prql-bot <107324867+prql-bot@users.noreply.github.com> Date: Tue, 4 Aug 2026 07:41:18 +0000 Subject: [PATCH 4/4] test: move `std.not` named-arg snapshots to bad_error_messages --- prqlc/prqlc/tests/integration/bad_error_messages.rs | 10 ++++++++++ prqlc/prqlc/tests/integration/error_messages.rs | 10 +++------- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/prqlc/prqlc/tests/integration/bad_error_messages.rs b/prqlc/prqlc/tests/integration/bad_error_messages.rs index 2883a620d1a8..6d76259f6432 100644 --- a/prqlc/prqlc/tests/integration/bad_error_messages.rs +++ b/prqlc/prqlc/tests/integration/bad_error_messages.rs @@ -209,6 +209,16 @@ fn nested_groups() { "); } +#[test] +fn not_with_named_arg() { + // A named argument to `std.not` isn't column-exclusion syntax. Both of these + // used to panic — the first on `exactly_one`, the second on indexing + // `args[0]` with no positional args at all. They're errors now, but the + // message has no span and `Debug`-prints the ident. + assert_snapshot!(compile(r"from x | select (std.not {a} b:1)").unwrap_err(), @r#"Error: unknown named argument `b` to closure Some(["std", "not"])"#); + assert_snapshot!(compile(r"from x | select (std.not b:1)").unwrap_err(), @r#"Error: unknown named argument `b` to closure Some(["std", "not"])"#); +} + #[test] fn just_std() { assert_snapshot!(compile(r###" diff --git a/prqlc/prqlc/tests/integration/error_messages.rs b/prqlc/prqlc/tests/integration/error_messages.rs index c2c6f07e1807..493c72c98f70 100644 --- a/prqlc/prqlc/tests/integration/error_messages.rs +++ b/prqlc/prqlc/tests/integration/error_messages.rs @@ -558,7 +558,9 @@ fn append_by_name_unnamed() { #[test] fn not_with_extra_args() { // A tuple argument to `std.not` is column-exclusion syntax, but only when - // it's the sole argument — extra args used to panic on `exactly_one`. + // it's the sole argument — extra args used to panic on `exactly_one`. The + // named-argument forms are in `bad_error_messages::not_with_named_arg`, + // since the message they produce still needs improving. assert_snapshot!(compile(r"from x | select (std.not {a} b)").unwrap_err(), @" Error: ╭─[ :1:18 ] @@ -577,10 +579,4 @@ fn not_with_extra_args() { │ ╰───────── Too many arguments to function `not` ───╯ "); - // A named argument isn't exclusion syntax either; it used to be dropped - // silently, compiling as though it weren't there. - assert_snapshot!(compile(r"from x | select (std.not {a} b:1)").unwrap_err(), @r#"Error: unknown named argument `b` to closure Some(["std", "not"])"#); - // With no positional args at all, the guard used to index `args[0]` and - // panic before it got as far as `exactly_one`. - assert_snapshot!(compile(r"from x | select (std.not b:1)").unwrap_err(), @r#"Error: unknown named argument `b` to closure Some(["std", "not"])"#); }