diff --git a/docs/about/changelog.md b/docs/about/changelog.md index fe85ab218..7cd081e54 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 652b445f7..40644a97b 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -857,19 +857,19 @@ 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. `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. 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); + // 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) { diff --git a/src/test/resources/unit/precedence.t b/src/test/resources/unit/precedence.t index caab10a5f..272baaf90 100644 --- a/src/test/resources/unit/precedence.t +++ b/src/test/resources/unit/precedence.t @@ -13,6 +13,19 @@ 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'); + +# 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 +');