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 = "7d7723dfb";
public static final String gitCommitId = "e3750ee00";

/**
* 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 28 2026 12:16:39";
public static final String buildTimestamp = "Apr 28 2026 13:11:57";

// Prevent instantiation
private Configuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -564,6 +564,29 @@ public static Node parseGivenStatement(Parser parser) {
return givenBlock;
}

/**
* Returns true if the given AST node is a syntactically empty list — that is,
* a ListNode whose elements are themselves all syntactically empty lists
* (or which has no elements at all). Examples that match: `()`, `qw()`,
* `((), qw())`. Examples that do not match: `@list`, `(1)`, `qw(a)`.
*
* Used by `use` parsing to detect `use Foo qw()` and similar forms, which
* Perl treats as "skip import" — distinct from `use Foo` (no list at all,
* imports defaults) and `use Foo @empty` (calls import even if @empty is
* runtime-empty).
*/
private static boolean isStaticallyEmptyList(Node node) {
if (!(node instanceof ListNode listNode)) {
return false;
}
for (Node child : listNode.elements) {
if (!isStaticallyEmptyList(child)) {
return false;
}
}
return true;
}

/**
* Parses a use or no declaration.
*
Expand Down Expand Up @@ -681,7 +704,19 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) {

// Parse the parameter list
boolean hasParentheses = TokenUtils.peek(parser).text.equals("(");
int listStartIndex = parser.tokenIndex;
Node list = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false);
// Detect a syntactically empty list expression after the module name
// (e.g. `use Foo qw()` — `use Foo ()` is already covered by hasParentheses).
// Perl treats this as "skip import", distinct from `use Foo` (no list at all)
// which calls import() with no arguments and triggers default exports.
// We require both: (a) the parser actually consumed tokens for a list
// expression (so this isn't `use Foo;`) and (b) the resulting AST is
// statically empty (so this isn't `use Foo @list` where @list happens
// to be empty at runtime — real Perl still calls import() in that case).
boolean hasEmptyLiteralList = !hasParentheses
&& parser.tokenIndex > listStartIndex
&& isStaticallyEmptyList(list);
if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use statement list hasParentheses:" + hasParentheses + " ast:" + list);

StatementResolver.parseStatementTerminator(parser);
Expand Down Expand Up @@ -772,7 +807,7 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) {
RuntimeList args = runSpecialBlock(parser, "BEGIN", list, RuntimeContextType.LIST);

if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use statement list: " + args);
if (hasParentheses && args.isEmpty()) {
if ((hasParentheses || hasEmptyLiteralList) && args.isEmpty()) {
// do not import
} else {
// fetch the method using `can` operator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,13 @@ public HttpTiny() {
public static void initialize() {
HttpTiny httpTiny = new HttpTiny();
httpTiny.initializeExporter();
httpTiny.defineExport("EXPORT", "new", "get", "post", "request");
// HTTP::Tiny does not export anything in real Perl. `new`, `get`, `post`,
// and `request` are object methods, not importable subs. Adding them to
// @EXPORT polluted callers (`use HTTP::Tiny;` in `package Foo;` would
// alias `Foo::new` to `HTTP::Tiny::new`), which broke any Moo-based
// class that did `use HTTP::Tiny;` — Moo's assert_constructor saw a
// pre-existing `new` and bailed with "Unknown constructor for Foo
// already exists" before it could install its own constructor.
try {
httpTiny.registerMethod("request", null);
httpTiny.registerMethod("mirror", null);
Expand Down
20 changes: 20 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/Internals.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,12 @@ public static void initialize() {
try {
internals.registerMethod("SvREADONLY", "svReadonly", "\\[$@%];$");
internals.registerMethod("SvREFCNT", "svRefcount", "$;$");
// Clear placeholder slots in a restricted hash. PerlOnJava doesn't
// implement Perl's restricted-hash placeholder machinery (used by
// Hash::Util / fields), so this is a safe no-op. Modules like
// Const::Fast call it when sealing a hash readonly; failing here
// breaks loading the whole module.
internals.registerMethod("hv_clear_placeholders", "hvClearPlaceholders", "\\%");
// Phase 0 diagnostic: expose PerlOnJava-internal refcount state
// (refCount, flags, tracking mode) for differential testing
// against native Perl. See dev/design/refcount_alignment_plan.md.
Expand Down Expand Up @@ -72,6 +78,20 @@ public static RuntimeList stack_refcounted(RuntimeArray args, int ctx) {
return new RuntimeScalar(1).getList();
}

/**
* Clear placeholder slots in a restricted hash.
*
* PerlOnJava does not implement Perl's restricted-hash placeholder
* machinery (used by {@code Hash::Util} / pseudo-hashes / fields). The
* actual op only matters for hashes that have had keys "locked" via
* {@code lock_keys}, where calling {@code keys %h} can leave behind
* placeholder slots. We don't have those slots, so there is nothing
* to do — returning an empty list matches the behavior callers expect.
*/
public static RuntimeList hvClearPlaceholders(RuntimeArray args, int ctx) {
return new RuntimeList();
}

/**
* No-op, returns false.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,13 @@ public RuntimeScalarReadOnly(double d) {
*/
@Override
void vivify() {
throw new RuntimeException("Modification of a read-only value attempted");
// Use PerlCompilerException so stringifyException() short-circuits and
// appends "at FILE line N." to the message. Throwing a plain
// RuntimeException produces a multi-line stack-trace style message
// (e.g. for `\do{45}; $$r = 99`) that doesn't match Perl's canonical
// "Modification of a read-only value attempted at FILE line N." form
// and breaks Const::Fast / Test::Fatal-style regex matches.
throw new PerlCompilerException("Modification of a read-only value attempted");
}

/**
Expand Down
Loading