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
27 changes: 22 additions & 5 deletions src/main/java/org/perlonjava/perlmodule/Exporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,11 @@ public static RuntimeList exportToLevel(RuntimeArray args, int ctx) {
// Determine the caller's namespace
RuntimeList callerList = RuntimeCode.caller(new RuntimeList(exportLevel), SCALAR);
String caller = callerList.scalar().toString();
if (caller == null || caller.isEmpty()) {
// In standard Perl, missing/empty caller context behaves like package 'main'.
// This is critical for `use Some::Module qw(...)` at top-level.
caller = "main";
}

// Retrieve the export lists and tags from the package
RuntimeArray export = GlobalVariable.getGlobalArray(packageName + "::EXPORT");
Expand Down Expand Up @@ -278,12 +283,24 @@ private static void importFunction(String packageName, String caller, String fun
if (exportSymbol.type == RuntimeScalarType.CODE) {
String fullName = caller + "::" + functionName;
RuntimeScalar importedRef = GlobalVariable.getGlobalCodeRef(fullName);
importedRef.set(exportSymbol);

// If the exported symbol is a forward declaration (undefined), annotate it with the source package
// so that AUTOLOAD can be resolved from the original package
if (exportSymbol.value instanceof RuntimeCode code && !code.defined()) {
code.sourcePackage = packageName;
if (exportSymbol.value instanceof RuntimeCode exportedCode) {
if (exportedCode.defined()) {
// Fully defined sub: import by aliasing the CODE ref.
importedRef.set(exportSymbol);
} else {
// Forward declaration: standard Perl semantics allow this to be called and
// resolved via AUTOLOAD in the defining package.
//
// The importedRef and exportSymbol point to DIFFERENT RuntimeCode objects
// (one for main::GetAllTags, one for Package::GetAllTags).
// We need to make the imported one resolve via the exporting package's AUTOLOAD.
if (importedRef.value instanceof RuntimeCode importedCode) {
importedCode.sourcePackage = packageName;
// Also copy the subName to ensure it's set correctly
importedCode.subName = functionName;
}
}
}

// If this function name is an overridable operator (like 'time'), mark it in isSubs
Expand Down
41 changes: 27 additions & 14 deletions src/main/java/org/perlonjava/runtime/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -651,17 +651,8 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int
// Try to find AUTOLOAD for this subroutine
String subroutineName = code.packageName + "::" + code.subName;
if (code.packageName != null && code.subName != null && !subroutineName.isEmpty()) {
// First check if AUTOLOAD exists in the current package
String autoloadString = code.packageName + "::AUTOLOAD";
RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString);
if (autoload.getDefinedBoolean()) {
// Set $AUTOLOAD name
getGlobalVariable(autoloadString).set(subroutineName);
// Call AUTOLOAD
return apply(autoload, a, callContext);
}

// If this is an imported forward declaration, check AUTOLOAD in the source package
// If this is an imported forward declaration, check AUTOLOAD in the source package FIRST
// This matches Perl semantics where imported subs resolve via the exporting package's AUTOLOAD
if (code.sourcePackage != null && !code.sourcePackage.equals(code.packageName)) {
String sourceAutoloadString = code.sourcePackage + "::AUTOLOAD";
RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString);
Expand All @@ -673,6 +664,16 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int
return apply(sourceAutoload, a, callContext);
}
}

// Then check if AUTOLOAD exists in the current package
String autoloadString = code.packageName + "::AUTOLOAD";
RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString);
if (autoload.getDefinedBoolean()) {
// Set $AUTOLOAD name
getGlobalVariable(autoloadString).set(subroutineName);
// Call AUTOLOAD
return apply(autoload, a, callContext);
}
}
throw new PerlCompilerException("Undefined subroutine &" + subroutineName + " called at ");
}
Expand Down Expand Up @@ -746,12 +747,24 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa
}

if (!fullSubName.isEmpty()) {
// Check if AUTOLOAD exists
// If this is an imported forward declaration, check AUTOLOAD in the source package FIRST
// This matches Perl semantics where imported subs resolve via the exporting package's AUTOLOAD
if (code.sourcePackage != null && !code.sourcePackage.isEmpty()) {
String sourceAutoloadString = code.sourcePackage + "::AUTOLOAD";
RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString);
if (sourceAutoload.getDefinedBoolean()) {
// Set $AUTOLOAD name to the original package function name
String sourceSubroutineName = code.sourcePackage + "::" + code.subName;
getGlobalVariable(sourceAutoloadString).set(sourceSubroutineName);
// Call AUTOLOAD from the source package
return apply(sourceAutoload, a, callContext);
}
}

// Then check if AUTOLOAD exists in the current package
String autoloadString = fullSubName.substring(0, fullSubName.lastIndexOf("::") + 2) + "AUTOLOAD";
// System.err.println("AUTOLOAD: " + fullName);
RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString);
if (autoload.getDefinedBoolean()) {
// System.err.println("AUTOLOAD exists: " + fullName);
// Set $AUTOLOAD name
getGlobalVariable(autoloadString).set(fullSubName);
// Call AUTOLOAD
Expand Down
40 changes: 40 additions & 0 deletions src/test/resources/unit/exiftool_exporter_autoload.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use strict;
use warnings;

our $autoload_seen;

{
package Exiftool::ExporterAutoload;

use Exporter qw(import);

our @EXPORT = qw(GetAllTags);

sub GetAllTags;

our $AUTOLOAD;

sub AUTOLOAD {
$main::autoload_seen = $AUTOLOAD;
return 1;
}
}

print "1..1\n";

Exiftool::ExporterAutoload->import();

my $ok = eval { GetAllTags(); 1 };
if (!$ok) {
my $err = $@;
$err =~ s/\s+\z//;
print "not ok 1 - GetAllTags() should trigger AUTOLOAD (got error: $err)\n";
exit;
}

if (defined $autoload_seen && $autoload_seen eq 'Exiftool::ExporterAutoload::GetAllTags') {
print "ok 1 - AUTOLOAD called for imported forward-declared sub\n";
} else {
my $seen = defined $autoload_seen ? $autoload_seen : '<undef>';
print "not ok 1 - AUTOLOAD not called correctly (saw: $seen)\n";
}