From 0b05a1fd5c78c5ed09c6c92a699525917bc56f89 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 8 Sep 2026 16:47:33 +0200 Subject: [PATCH 1/2] fix: correct named unary arithmetic precedence Parse the operand of scalar, values, keys, and each at the named-unary boundary so arithmetic binds inside it. This restores the expected grouping of ! scalar @array % 2 used by Class::MakeMethods. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- docs/about/changelog.md | 3 +++ .../frontend/parser/OperatorParser.java | 19 +++++++------------ src/test/resources/unit/precedence.t | 8 ++++++++ 3 files changed, 18 insertions(+), 12 deletions(-) diff --git a/docs/about/changelog.md b/docs/about/changelog.md index fe85ab2187..7cd081e54b 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,6 +6,9 @@ priorities and future plans. ## Work in progress +- Correct named-unary operand precedence, so `! scalar @array % 2` evaluates + the modulo operation before its logical negation. + - Preserve tied-scalar magic through `utf8::encode` and `utf8::decode`. - Preserve IO::Async thread callback results and accepted listener sockets on diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 652b445f7b..33a3fb1f5c 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -857,20 +857,15 @@ public static OperatorNode parseSelect(Parser parser, LexerToken token, int curr static OperatorNode parseKeys(Parser parser, LexerToken token, int currentIndex) { String operator = token.text; Node operand; - // Handle operators with a single operand - // For scalar, values, keys, each: parse with precedence that includes postfix operators ([], {}, ->) - // Named unary operators have precedence between 20 and 21 in Perl - // This allows expressions like: values $hashref->%* or keys $hashref->%* or scalar((nil) x 3, 1) + // Handle operators with a single operand. Named unary operators bind + // less tightly than arithmetic operators, so their operand includes + // expressions such as `scalar @array % 2`. if (operator.equals("scalar") || operator.equals("values") || operator.equals("keys") || operator.equals("each")) { // parseExpression stops before an operator whose precedence is - // equal to the supplied floor. Named unary scalar binds across a - // following =~ / !~ (`scalar $s =~ /(...)/`) and must force that - // match into scalar context rather than letting an enclosing print - // put it in list context. The other named unary operators retain - // their existing match-level boundary. - int operandPrecedence = parser.getPrecedence("=~") - - (operator.equals("scalar") ? 1 : 0); - operand = parser.parseExpression(operandPrecedence); + // equal to the supplied floor. Keep the named-unary boundary above + // relational operators while admitting arithmetic and binding + // operators such as %, +, and =~. + operand = parser.parseExpression(parser.getPrecedence("isa") + 1); // Check if operand is null (no argument provided) if (operand == null) { throw new PerlCompilerException(currentIndex, "Not enough arguments for " + operator, parser.ctx.errorUtil); diff --git a/src/test/resources/unit/precedence.t b/src/test/resources/unit/precedence.t index caab10a5f6..f32d6d87c0 100644 --- a/src/test/resources/unit/precedence.t +++ b/src/test/resources/unit/precedence.t @@ -13,6 +13,14 @@ ok(!($flush_mro->{a} != 3), '++ vs ->'); my $result = -2**2; ok(!($result != -4), '** vs unary -'); +# Prefix ! must let a following named unary operator consume multiplicative +# expressions. This is the form used by Class::MakeMethods to test whether +# a constructor returned an even-sized method/name list. +my @even_results = ('new', sub {}); +my @odd_results = ('new', sub {}, 'extra'); +ok(! scalar @even_results % (1 + 1), '! scalar @array % divisor groups modulo with scalar'); +ok(!(! scalar @odd_results % (1 + 1)), '! scalar @array % divisor preserves the odd case'); + # Test precedence of * vs + $result = 2 + 3 * 4; ok(!($result != 14), '* vs +'); From 49e8abb00c2dfa5941190ca8e93538d15dbbfec1 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 8 Sep 2026 19:07:35 +0200 Subject: [PATCH 2/2] fix: retain keys operand precedence Restrict scalar's relaxed arithmetic operand boundary to scalar. keys, values, and each keep their aggregate operand boundary, restoring expressions such as keys(%hash) * 4 used by perf/benchmarks.t. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- .../frontend/parser/OperatorParser.java | 19 ++++++++++++------- src/test/resources/unit/precedence.t | 5 +++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 33a3fb1f5c..40644a97b5 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -857,15 +857,20 @@ public static OperatorNode parseSelect(Parser parser, LexerToken token, int curr static OperatorNode parseKeys(Parser parser, LexerToken token, int currentIndex) { String operator = token.text; Node operand; - // Handle operators with a single operand. Named unary operators bind - // less tightly than arithmetic operators, so their operand includes - // expressions such as `scalar @array % 2`. + // Handle operators with a single operand. `scalar` binds less tightly + // than arithmetic operators, so its operand includes expressions such + // as `scalar @array % 2`. keys/values/each retain their conventional + // single-aggregate operand boundary (`keys(%hash) * 4`). if (operator.equals("scalar") || operator.equals("values") || operator.equals("keys") || operator.equals("each")) { // parseExpression stops before an operator whose precedence is - // equal to the supplied floor. Keep the named-unary boundary above - // relational operators while admitting arithmetic and binding - // operators such as %, +, and =~. - operand = parser.parseExpression(parser.getPrecedence("isa") + 1); + // equal to the supplied floor. scalar needs the named-unary + // boundary above relational operators while admitting arithmetic + // and binding operators such as %, +, and =~. The aggregate + // operators must stop before those arithmetic operations. + int operandPrecedence = operator.equals("scalar") + ? parser.getPrecedence("isa") + 1 + : parser.getPrecedence("=~"); + operand = parser.parseExpression(operandPrecedence); // Check if operand is null (no argument provided) if (operand == null) { throw new PerlCompilerException(currentIndex, "Not enough arguments for " + operator, parser.ctx.errorUtil); diff --git a/src/test/resources/unit/precedence.t b/src/test/resources/unit/precedence.t index f32d6d87c0..272baaf908 100644 --- a/src/test/resources/unit/precedence.t +++ b/src/test/resources/unit/precedence.t @@ -21,6 +21,11 @@ my @odd_results = ('new', sub {}, 'extra'); ok(! scalar @even_results % (1 + 1), '! scalar @array % divisor groups modulo with scalar'); ok(!(! scalar @odd_results % (1 + 1)), '! scalar @array % divisor preserves the odd case'); +# keys retains its single-aggregate operand boundary: the multiplication is +# applied to the key count, not to the hash operand. +my %precedence_keys = (first => 1, second => 2); +is(keys(%precedence_keys) * 4, 8, 'keys %hash stops before multiplication'); + # Test precedence of * vs + $result = 2 + 3 * 4; ok(!($result != 14), '* vs +');