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
4 changes: 2 additions & 2 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "5f0ebe111";
public static final String gitCommitId = "3fb27ed18";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand All @@ -48,7 +48,7 @@ public final class Configuration {
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String buildTimestamp = "Apr 29 2026 10:39:59";
public static final String buildTimestamp = "Apr 29 2026 10:51:13";

// Prevent instantiation
private Configuration() {
Expand Down
20 changes: 14 additions & 6 deletions src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -383,10 +383,9 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr
variableName.append("::");
parser.tokenIndex++;

// Skip whitespace after '
parser.tokenIndex = Whitespace.skipWhitespace(parser, parser.tokenIndex, parser.tokens);

// Update token references
// Update token references. Do NOT skip whitespace here:
// in real perl, qualified names cannot contain whitespace
// around the separator. "$Foo' bar" is not "$Foo::bar".
token = parser.tokens.get(parser.tokenIndex);
nextToken = parser.tokens.get(parser.tokenIndex + 1);

Expand All @@ -404,15 +403,24 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr
variableName.append(token.text);
parser.tokenIndex++;

// Skip whitespace after ::
parser.tokenIndex = Whitespace.skipWhitespace(parser, parser.tokenIndex, parser.tokens);
// Do NOT skip whitespace after ::. In real perl, "$Foo:: bar"
// is parsed as the stash glob "$Foo::" followed by the bareword
// "bar"; whitespace breaks the qualified name. Specifically,
// "%Foo:: and 2" must tokenize as the stash hash %Foo:: followed
// by the low-precedence operator `and`, not as %Foo::and.
// Previously we skipped whitespace here and accidentally pulled
// the next keyword (and / or / not / xor / cmp / eq / ...) into
// the identifier, which broke e.g. the bundled Dumpvalue.pm
// (`and %overload:: and defined ...`) and any code path that
// required loading it (notably CPAN.pm's error reporter).

// Check what follows ::
token = parser.tokens.get(parser.tokenIndex);
nextToken = parser.tokens.get(parser.tokenIndex + 1);

// After ::, only identifiers or another :: are allowed (or ' as package separator)
// Note: Keywords CAN be valid identifier parts after :: (e.g., $Foo::and, &UNIVERSAL::isa)
// — but only when they are flush against ::, with no intervening whitespace.
if (token.type != LexerTokenType.IDENTIFIER && !token.text.equals("::") && !token.text.equals("'")) {
// Nothing valid follows ::, so return what we have
return variableName.toString();
Expand Down
77 changes: 77 additions & 0 deletions src/test/resources/unit/stash_var_keyword_op.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use strict;
use warnings;
use Test::More tests => 12;

# Regression: parsing of a package-stash variable like %Foo:: must NOT
# consume a following whitespace-separated keyword (and / or / not / xor /
# cmp / eq / ne / lt / gt / le / ge / x) as part of the identifier.
#
# Real perl tokenizes "%Foo:: and 2" as the stash hash %Foo:: followed by
# the low-precedence operator "and"; only "%Foo::and" (no space) is the
# hash named "and" in package Foo. PerlOnJava previously skipped whitespace
# after :: and accidentally produced %Foo::and, causing a syntax error and
# (because the bundled Dumpvalue.pm uses `and %overload:: and ...`) breaking
# any code path through CPAN.pm's error reporter.
# See https://github.com/fglock/PerlOnJava/issues for the cpan -t JSON::Literal
# repro that surfaced this.

package Foo;
our $touched = 0;

package main;

# Make sure the stash exists before we read it.
$Foo::dummy = 1;

# 1: bare stash hash followed by `and` operator
my $r1 = (%Foo:: and 1);
is($r1, 1, '%Foo:: and 1 (whitespace before "and" must keep "and" as operator)');

# 2: bare stash hash followed by `or` operator
my $r2 = (%Foo:: or 'fallback');
ok($r2, '%Foo:: or ... (whitespace before "or" must keep "or" as operator)');

# 3: bare stash hash followed by `not` is just illegal-as-statement in perl,
# but `! %Foo:: and 1` and `not %Foo:: and 1` parse fine.
my $r3 = (not %Foo::) ? 0 : 1;
is($r3, 1, 'not %Foo:: (whitespace after :: before nothing parses)');

# 4..7: same with the other low-precedence keyword operators
my $r4 = (%Foo:: xor 0);
ok($r4, '%Foo:: xor 0');

my $r5 = (1 and %Foo:: and 2);
is($r5, 2, '1 and %Foo:: and 2');

my $r6 = (1 and %Foo::);
ok($r6, '1 and %Foo:: (trailing stash with no following operator)');

my $r7 = (%Foo:: && 1);
is($r7, 1, '%Foo:: && 1 (high-precedence form, regression check)');

# 8: comparison operators must also stay as operators
my @keys = sort keys %Foo::;
ok(@keys, 'keys %Foo:: returns something');
ok((scalar(@keys) cmp 0) >= 0, 'scalar(keys %Foo::) cmp 0');

# 9: %Foo::and (no space) should still be the hash named "and" in Foo
%Foo::and = (a => 1);
is(scalar(keys %Foo::and), 1, '%Foo::and (no space) is still hash named "and" in Foo');

# 10: $Foo::or (no space) is the scalar named "or" in Foo
$Foo::or = 'value';
is($Foo::or, 'value', '$Foo::or (no space) is still scalar named "or" in Foo');

# 11: control case — Dumpvalue's exact pattern from line 110 must compile
eval q{
my $self = { bareStringify => 1 };
my $val = "x";
no strict 'refs';
$val = &{'overload::StrVal'}($val)
if $self->{bareStringify} and ref \$val
and %overload:: and defined &{'overload::StrVal'};
1;
} or do {
fail("Dumpvalue.pm pattern compiles: $@");
};
pass("Dumpvalue.pm pattern (and %overload:: and defined ...) compiles cleanly");
Loading