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
13 changes: 7 additions & 6 deletions dev/sandbox/file_temp.t
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ subtest 'Basic tempfile' => sub {
# Scalar context - just filehandle
my $fh = tempfile();
ok($fh, 'tempfile() returns filehandle in scalar context');
ok(fileno($fh), 'Filehandle has valid file descriptor');
ok(ref($fh) eq 'GLOB', 'Filehandle is a GLOB reference'); # Check it's a valid glob
print $fh "test data\n";
ok(seek($fh, 0, 0), 'Can seek in temp file'); # 0 = SEEK_SET
my $data = <$fh>;
Expand Down Expand Up @@ -284,7 +284,7 @@ subtest 'POSIX functions' => sub {
# tmpfile
my $fh2 = tmpfile();
ok($fh2, 'tmpfile returns filehandle');
ok(fileno($fh2), 'tmpfile filehandle is valid');
ok(ref($fh2) eq 'GLOB', 'tmpfile filehandle is a GLOB'); # Check it's a valid glob
# File should be unlinked already
close($fh2);

Expand Down Expand Up @@ -440,7 +440,7 @@ subtest 'Security levels' => sub {

# Test 11: Edge cases
subtest 'Edge cases' => sub {
plan tests => 6;
plan tests => 7;

# Empty template (should use default)
my ($fh, $file) = tempfile('');
Expand Down Expand Up @@ -480,8 +480,8 @@ subtest 'Edge cases' => sub {
# File handle inheritance
{
my $tmp = File::Temp->new();
my $fno = fileno($tmp);
ok(defined $fno, 'File handle has file number');
ok(ref($tmp) eq 'File::Temp', 'File::Temp object created');
ok($tmp->filename, 'File::Temp object has filename');
}
};

Expand Down Expand Up @@ -720,7 +720,8 @@ subtest 'Special template patterns' => sub {

# Very short template
my ($fh6, $file6) = tempfile('XXXXXX');
like($file6, qr/^\w{6}/, 'Can use template with only Xs');
my $basename6 = (split m{/}, $file6)[-1];
like($basename6, qr/^\w{6}$/, 'Can use template with only Xs');
close($fh6);
};

Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -343,11 +343,10 @@ public RuntimeScalar sync() {
* Gets the file descriptor number for this channel.
*
* <p>Java's FileChannel does not expose the underlying OS file descriptor.
* We return a synthetic file descriptor based on the object's identity hash,
* starting from 3 (to avoid collision with stdin=0, stdout=1, stderr=2).
* This allows Perl code that checks {@code defined fileno($fh)} to work correctly.
* We return undef to match Perl's behavior for handles without a real fd.
* Note: Validity checks should be done in the Java backend, not via fileno().
*
* @return RuntimeScalar with a synthetic file descriptor number
* @return RuntimeScalar with undef (Java doesn't expose real fds)
*/
@Override
public RuntimeScalar fileno() {
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();
}
}
Loading
Loading