Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions src/main/java/org/perlonjava/frontend/parser/OperatorParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
13 changes: 13 additions & 0 deletions src/test/resources/unit/precedence.t
Original file line number Diff line number Diff line change
Expand Up @@ -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 +');
Expand Down
Loading