Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,9 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) {
if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("use: " + token.text);
boolean isNoDeclaration = token.text.equals("no");

// Capture token index for caller() before consuming any tokens
int useTokenIndex = parser.tokenIndex;

TokenUtils.consume(parser); // "use"
token = TokenUtils.peek(parser);

Expand Down Expand Up @@ -627,11 +630,12 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) {
// execute the statement immediately, using:
// `require "fullName.pm"`

// Setup the caller stack
// Setup the caller stack - use getSourceLocationAccurate to honor #line directives
ErrorMessageUtil.SourceLocation loc = ctx.errorUtil.getSourceLocationAccurate(useTokenIndex);
CallerStack.push(
ctx.symbolTable.getCurrentPackage(),
ctx.compilerOptions.fileName,
ctx.errorUtil.getLineNumber(parser.tokenIndex));
loc.fileName(),
loc.lineNumber());
try {

if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use statement: " + fullName + " called from " + CallerStack.peek(0));
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/perlonjava/runtime/mro/DFS.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.perlonjava.runtime.mro;

import org.perlonjava.runtime.runtimetypes.GlobalVariable;
import org.perlonjava.runtime.runtimetypes.NameNormalizer;
import org.perlonjava.runtime.runtimetypes.PerlCompilerException;
import org.perlonjava.runtime.runtimetypes.RuntimeArray;
import org.perlonjava.runtime.runtimetypes.RuntimeBase;
Expand Down Expand Up @@ -91,6 +92,8 @@ private static void populateIsaMapWithCycleDetection(String className,
String parentName = entity.toString();
// FIXED: Skip empty or null parent names
if (parentName != null && !parentName.isEmpty()) {
// Normalize old-style ' separator to :: (e.g., Foo'Bar -> Foo::Bar)
parentName = NameNormalizer.normalizePackageName(parentName);
parents.add(parentName);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,8 @@ private static void populateIsaMapHelper(String className,
}
}
if (!parentName.isEmpty()) {
// Normalize old-style ' separator to :: (e.g., Foo'Bar -> Foo::Bar)
parentName = NameNormalizer.normalizePackageName(parentName);
parents.add(parentName);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private static RuntimeScalar callerWhere() {
}
String fileName = caller.elements.get(1).toString();
int line = ((RuntimeScalar) caller.elements.get(2)).getInt();
return new RuntimeScalar(" at " + fileName + " line " + line + "\n");
return new RuntimeScalar(" at " + fileName + " line " + line + ".\n");
}

private static String filehandleShortName(RuntimeScalar fileHandle) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -615,12 +615,22 @@ else if (code == null) {
FeatureFlags outerFeature = featureManager;
String savedPackage = InterpreterState.currentPackage.get().toString();

// Save and clear %^H (hints hash) to prevent hint leakage into required modules.
// In Perl >= 5.11 (which we emulate), hints don't leak into require'd files.
// The hints hash affects compile-time behavior (strict, warnings, features),
// and a required module should start with clean compile-time state.
RuntimeHash hintHash = GlobalVariable.getGlobalHash(GlobalContext.encodeSpecialVar("H"));
java.util.Map<String, RuntimeScalar> savedHintHash = new java.util.HashMap<>(hintHash.elements);

// Notify B::Hooks::EndOfScope that we're starting to load a file
// This enables on_scope_end callbacks to know which file they belong to
BHooksEndOfScope.beginFileLoad(parsedArgs.fileName);

try {
featureManager = new FeatureFlags();

// Clear the hints hash for a fresh compilation context
hintHash.elements.clear();

result = PerlLanguageProvider.executePerlCode(parsedArgs, false, ctx);

Expand All @@ -643,6 +653,10 @@ else if (code == null) {

featureManager = outerFeature;
InterpreterState.currentPackage.get().set(savedPackage);

// Restore the caller's hints hash
hintHash.elements.clear();
hintHash.elements.putAll(savedHintHash);
}

// Return result based on context
Expand Down Expand Up @@ -732,7 +746,8 @@ public static RuntimeScalar require(RuntimeScalar runtimeScalar) {
RuntimeScalar incEntry = incHash.elements.get(fileName);
if (!incEntry.defined().getBoolean()) {
// This was a compilation failure, throw the cached error
throw new PerlCompilerException("Compilation failed in require at " + fileName);
// Perl outputs: "Attempt to reload <file> aborted.\nCompilation failed in require at ..."
throw new PerlCompilerException("Attempt to reload " + fileName + " aborted.\nCompilation failed in require at " + fileName);
}
// module was already loaded successfully - always return exactly 1
return getScalarInt(1);
Expand Down Expand Up @@ -763,7 +778,22 @@ public static RuntimeScalar require(RuntimeScalar runtimeScalar) {
message = fileName + " did not return a true value";
throw new PerlCompilerException(message);
} else if (err.isEmpty()) {
message = "Can't locate " + fileName + " in @INC";
// Derive module name from filename for helpful error message
String moduleName = fileName;
if (moduleName.endsWith(".pm")) {
moduleName = moduleName.substring(0, moduleName.length() - 3);
}
moduleName = moduleName.replace("/", "::");

// Build @INC list for error message
RuntimeArray incArray = GlobalVariable.getGlobalArray("main::INC");
StringBuilder incList = new StringBuilder();
for (int i = 0; i < incArray.size(); i++) {
if (i > 0) incList.append(" ");
incList.append(incArray.get(i).toString());
}

message = "Can't locate " + fileName + " in @INC (you may need to install the " + moduleName + " module) (@INC entries checked: " + incList + ")";
// Don't set %INC for file not found errors
throw new PerlCompilerException(message);
} else {
Expand Down
39 changes: 26 additions & 13 deletions src/main/java/org/perlonjava/runtime/operators/Operator.java
Original file line number Diff line number Diff line change
Expand Up @@ -276,17 +276,18 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
int length = hasExplicitLength ? ((RuntimeScalar) args[2]).getInt() : strLength - offset;
String replacement = (size > 3) ? args[3].toString() : null;

// Store original offset and length for LValue creation
int originalOffset = offset;
int originalLength = length;

// Handle negative offsets (count from the end of the string)
if (offset < 0) {
offset = strLength + offset;
// When no explicit length is provided, Perl clips negative offsets to 0 (no warning)
// When explicit length IS provided, Perl warns and returns undef for too-negative offsets
// When computed offset goes negative (before string start):
// - Clip offset to 0
// - Reduce length by the overshoot amount
// Example: substr("a", -2, 2) -> offset=-1, clip to 0, length=2+(-1)=1, returns "a"
// But: substr("hello", -10, 1) -> offset=-5, length=1+(-5)=-4 → warn and return undef
if (offset < 0) {
if (hasExplicitLength) {
// Check if adjusted length would be non-positive (Perl warns in this case)
int adjustedLength = length + offset;
if (adjustedLength <= 0) {
// Warn and return undef (same as positive offset out of bounds)
if (warnEnabled) {
WarnDie.warn(new RuntimeScalar("substr outside of string"),
Expand All @@ -295,14 +296,14 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
if (replacement != null) {
return new RuntimeScalar();
}
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength);
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", 0, 0);
lvalue.type = RuntimeScalarType.UNDEF;
lvalue.value = null;
return lvalue;
} else {
// Clip to 0 without warning
offset = 0;
}
// Reduce length by the overshoot (negative offset value)
length = adjustedLength;
offset = 0;
}
}

Expand All @@ -315,7 +316,7 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
if (replacement != null) {
return new RuntimeScalar();
}
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength);
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, length);
lvalue.type = RuntimeScalarType.UNDEF;
lvalue.value = null;
return lvalue;
Expand All @@ -332,14 +333,26 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas
// Ensure length is non-negative and within bounds
length = Math.max(0, Math.min(length, strLength - offset));

// If length is zero or negative after all adjustments, return empty string
if (length <= 0) {
if (replacement != null) {
// With replacement, still need to handle the replacement at position 0
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, 0);
lvalue.set(replacement);
return new RuntimeScalar("");
}
return new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, 0);
}

// Extract the substring (offset/length are in Unicode code points)
int startIndex = str.offsetByCodePoints(0, offset);
int endIndex = str.offsetByCodePoints(startIndex, length);
String result = str.substring(startIndex, endIndex);

// Return an LValue "RuntimeSubstrLvalue" that can be used to assign to the original string
// This allows for in-place modification of the original string if needed
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], result, originalOffset, originalLength);
// Pass the adjusted offset and length, not the originals
var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], result, offset, length);

if (replacement != null) {
// When replacement is provided, save the extracted substring before modifying
Expand Down
41 changes: 39 additions & 2 deletions src/main/java/org/perlonjava/runtime/perlmodule/Base.java
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) {
RuntimeList callerList = RuntimeCode.caller(new RuntimeList(), RuntimeContextType.SCALAR);
String inheritor = callerList.scalar().toString();

// Keep track of bases we're adding in this import call
java.util.List<String> basesToAdd = new java.util.ArrayList<>();

// Process each base class specified in the arguments
for (RuntimeScalar baseClass : args.elements) {
String baseClassName = baseClass.toString();
Expand All @@ -62,6 +65,35 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) {
continue;
}

// Check if inheritor or any base we're adding already isa this base class
// This matches Perl's base.pm line 92: next if grep $_->isa($base), ($inheritor, @bases);
boolean shouldSkip = false;

// Check if inheritor already isa baseClassName
RuntimeArray isaArgs = new RuntimeArray();
RuntimeArray.push(isaArgs, new RuntimeScalar(inheritor));
RuntimeArray.push(isaArgs, new RuntimeScalar(baseClassName));
if (Universal.isa(isaArgs, RuntimeContextType.SCALAR).getBoolean()) {
shouldSkip = true;
}

// Check if any of the bases we're adding already isa baseClassName
if (!shouldSkip) {
for (String addedBase : basesToAdd) {
RuntimeArray isaArgs2 = new RuntimeArray();
RuntimeArray.push(isaArgs2, new RuntimeScalar(addedBase));
RuntimeArray.push(isaArgs2, new RuntimeScalar(baseClassName));
if (Universal.isa(isaArgs2, RuntimeContextType.SCALAR).getBoolean()) {
shouldSkip = true;
break;
}
}
}

if (shouldSkip) {
continue;
}

if (!GlobalVariable.isPackageLoaded(baseClassName)) {
// Require the base class file
String filename = baseClassName.replace("::", "/").replace("'", "/") + ".pm";
Expand All @@ -77,8 +109,13 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) {
}
}

// Add the base class to the @ISA array of the inheritor
RuntimeArray isa = getGlobalArray(inheritor + "::ISA");
// Add to our list of bases to add
basesToAdd.add(baseClassName);
}

// Add all the bases to @ISA at the end (like Perl's base.pm line 138)
RuntimeArray isa = getGlobalArray(inheritor + "::ISA");
for (String baseClassName : basesToAdd) {
RuntimeArray.push(isa, new RuntimeScalar(baseClassName));
}

Expand Down
26 changes: 24 additions & 2 deletions src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.perlonjava.runtime.runtimetypes.SystemUtils;

import java.io.File;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
Expand Down Expand Up @@ -425,7 +426,22 @@ public static RuntimeList abs2rel(RuntimeArray args, int ctx) {
}
String path = args.get(1).toString();
String base = args.size() == 3 ? args.get(2).toString() : System.getProperty("user.dir");
String relPath = Paths.get(base).relativize(Paths.get(path)).toString();

// Ensure both paths are absolute before relativizing (like Perl does)
// Note: We use user.dir explicitly because Java's Path.toAbsolutePath()
// doesn't respect System.setProperty("user.dir", ...) set by chdir()
Path pathObj = Paths.get(path);
Path baseObj = Paths.get(base);
String userDir = System.getProperty("user.dir");

if (!pathObj.isAbsolute()) {
pathObj = Paths.get(userDir).resolve(pathObj).normalize();
}
if (!baseObj.isAbsolute()) {
baseObj = Paths.get(userDir).resolve(baseObj).normalize();
}

String relPath = baseObj.relativize(pathObj).toString();
return new RuntimeScalar(relPath).getList();
}

Expand Down Expand Up @@ -454,8 +470,14 @@ public static RuntimeList rel2abs(RuntimeArray args, int ctx) {
return new RuntimeScalar(absPath).getList();
}

// If base is relative, resolve it against current working directory first
Path basePath = Paths.get(base);
if (!basePath.isAbsolute()) {
basePath = Paths.get(System.getProperty("user.dir")).resolve(basePath);
}

// For relative paths, resolve against the base directory
String absPath = Paths.get(base, path).toAbsolutePath().normalize().toString();
String absPath = basePath.resolve(path).normalize().toString();
return new RuntimeScalar(absPath).getList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -232,9 +232,11 @@ public static RuntimeList isa(RuntimeArray args, int ctx) {
// Get the linearized inheritance hierarchy using C3
List<String> linearizedClasses = InheritanceResolver.linearizeHierarchy(perlClassName);

// Normalize the argument: main::Foo -> Foo, ::Foo -> Foo
// Normalize the argument: main::Foo -> Foo, ::Foo -> Foo, Foo'Bar -> Foo::Bar
// This is needed because isa("main::Foo") should match a class blessed as "Foo"
String normalizedArg = argString;
// First normalize old-style ' separator to ::
normalizedArg = NameNormalizer.normalizePackageName(normalizedArg);
if (normalizedArg.startsWith("main::")) {
normalizedArg = normalizedArg.substring(6);
} else if (normalizedArg.startsWith("::")) {
Expand Down
Loading
Loading