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
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,31 @@ public static int executeRepeatAssign(int[] bytecode, int pc, RuntimeBase[] regi
if (BytecodeInterpreter.isImmutableProxy(registers[rd])) {
registers[rd] = BytecodeInterpreter.ensureMutableScalar(registers[rd]);
}
// Check for overloaded x= (falling back to x via autogeneration)
RuntimeBase dVal = registers[rd];
if (dVal instanceof RuntimeScalar dScalar) {
int blessId = org.perlonjava.runtime.runtimetypes.RuntimeScalarType.blessedId(dScalar);
if (blessId < 0) {
RuntimeScalar times = (RuntimeScalar) registers[rs];
// Try (x= first
RuntimeScalar ovResult = org.perlonjava.runtime.runtimetypes.OverloadContext
.tryTwoArgumentOverloadDirect(dScalar, times, blessId, 0, "(x=");
if (ovResult == null) {
// Try autogenerate via (x
ovResult = org.perlonjava.runtime.runtimetypes.OverloadContext
.tryTwoArgumentOverloadDirect(dScalar, times, blessId, 0, "(x");
}
if (ovResult == null) {
// Try nomethod (may throw if fallback=0)
ovResult = org.perlonjava.runtime.runtimetypes.OverloadContext
.tryTwoArgumentNomethod(dScalar, times, blessId, 0, "x=");
}
if (ovResult != null) {
((RuntimeScalar) registers[rd]).set(ovResult);
return pc;
}
}
}
RuntimeBase result = Operator.repeat(
registers[rd],
(RuntimeScalar) registers[rs],
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) {
// For eval blocks "(eval)", set the subroutine name so caller() reports it correctly
if ("(eval)".equals(node.name)) {
newSymbolTable.setCurrentSubroutine("(eval)");
} else if (node.name == null || node.name.equals("<anon>")) {
// True anonymous sub: caller() should report it as "Package::__ANON__",
// NOT as the enclosing named sub. Matches Perl 5 behavior.
newSymbolTable.setCurrentSubroutine(ctx.symbolTable.getCurrentPackage() + "::__ANON__");
} else {
newSymbolTable.setCurrentSubroutine(ctx.symbolTable.getCurrentSubroutine());
}
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,22 @@ 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 = "8e0c96103";
public static final String gitCommitId = "5fbadc806";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitDate = "2026-04-14";
public static final String gitCommitDate = "2026-04-20";

/**
* Build timestamp in Perl 5 "Compiled at" format (e.g., "Apr 7 2026 11:20:00").
* Automatically populated by Gradle during build.
* 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 20 2026 13:49:59";
public static final String buildTimestamp = "Apr 20 2026 14:34:31";

// Prevent instantiation
private Configuration() {
Expand Down
130 changes: 91 additions & 39 deletions src/main/java/org/perlonjava/runtime/operators/CompareOperators.java

Large diffs are not rendered by default.

14 changes: 14 additions & 0 deletions src/main/java/org/perlonjava/runtime/operators/Operator.java
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,20 @@ private static RuntimeList reversePlainArray(RuntimeArray array) {
}

public static RuntimeBase repeat(RuntimeBase value, RuntimeScalar timesScalar, int ctx) {
// Check for overloaded `x` operator (only when left operand is a blessed scalar)
if (value instanceof RuntimeScalar valScalar) {
int blessId = org.perlonjava.runtime.runtimetypes.RuntimeScalarType.blessedId(valScalar);
if (blessId < 0) {
RuntimeScalar result = org.perlonjava.runtime.runtimetypes.OverloadContext
.tryTwoArgumentOverloadDirect(valScalar, timesScalar, blessId, 0, "(x");
if (result != null) return result;
// Try nomethod fallback (may throw if fallback=0)
result = org.perlonjava.runtime.runtimetypes.OverloadContext
.tryTwoArgumentNomethod(valScalar, timesScalar, blessId, 0, "x");
if (result != null) return result;
}
}

// Check for uninitialized values and generate warnings
// Use getDefinedBoolean() to handle tied scalars correctly
if (value instanceof RuntimeScalar && !value.getDefinedBoolean()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,12 @@ public static RuntimeArray removeGlobalArray(String key) {
* @return The RuntimeHash representing the global hash.
*/
public static RuntimeHash getGlobalHash(String key) {
// Normalize stash lookups: in Perl, all packages are children of main::,
// so %{main::F::} and %F:: refer to the same stash.
// Strip a leading "main::" from stash keys (but keep "main::" itself).
if (key.length() > 6 && key.endsWith("::") && key.startsWith("main::")) {
key = key.substring(6);
}
RuntimeHash var = globalHashes.get(key);
if (var == null) {
// Check if this is a package stash (ends with ::)
Expand All @@ -379,7 +385,12 @@ public static RuntimeHash getGlobalHash(String key) {
* @return True if the global hash exists, false otherwise.
*/
public static boolean existsGlobalHash(String key) {
return globalHashes.containsKey(key);
if (globalHashes.containsKey(key)) return true;
// Normalize stash lookups: %{main::F::} and %F:: refer to the same stash.
if (key.length() > 6 && key.endsWith("::") && key.startsWith("main::")) {
return globalHashes.containsKey(key.substring(6));
}
return false;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,65 @@ public static RuntimeScalar tryTwoArgumentOverload(RuntimeScalar arg1, RuntimeSc
return tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, overloadName, methodName, (String[]) null);
}

/**
* Tries only the direct overloaded operator without invoking nomethod.
* Used when autogeneration may still provide a result (e.g., try (lt first,
* then fall back to (cmp before invoking nomethod).
*
* @return The result of the direct overload, or null if no direct overload is defined.
*/
public static RuntimeScalar tryTwoArgumentOverloadDirect(RuntimeScalar arg1, RuntimeScalar arg2, int blessId, int blessId2, String overloadName) {
if (blessId < 0) {
OverloadContext ctx1 = prepare(blessId);
if (ctx1 != null) {
RuntimeScalar result = ctx1.tryOverload(overloadName, new RuntimeArray(arg1, arg2, scalarFalse));
if (result != null) return result;
}
}
if (blessId2 < 0) {
OverloadContext ctx2 = prepare(blessId2);
if (ctx2 != null) {
RuntimeScalar result = ctx2.tryOverload(overloadName, new RuntimeArray(arg2, arg1, scalarTrue));
if (result != null) return result;
}
}
return null;
}

/**
* Tries nomethod fallback on either blessed argument.
* Used as a last resort after direct overload and autogeneration have failed.
* Also enforces the fallback=0 restriction, throwing when no method is found
* and fallback explicitly forbids autogeneration.
*
* @return The result of nomethod, or null if no nomethod is defined (and fallback allows autogeneration).
*/
public static RuntimeScalar tryTwoArgumentNomethod(RuntimeScalar arg1, RuntimeScalar arg2, int blessId, int blessId2, String methodName) {
OverloadContext ctx1 = blessId < 0 ? prepare(blessId) : null;
OverloadContext ctx2 = blessId2 < 0 ? prepare(blessId2) : null;

if (ctx1 != null) {
RuntimeScalar result = ctx1.tryOverload("(nomethod", new RuntimeArray(arg1, arg2, scalarFalse, new RuntimeScalar(methodName)));
if (result != null) return result;
}
if (ctx2 != null) {
RuntimeScalar result = ctx2.tryOverload("(nomethod", new RuntimeArray(arg2, arg1, scalarTrue, new RuntimeScalar(methodName)));
if (result != null) return result;
}

// Enforce fallback=0 (explicitly deny autogeneration / native op)
OverloadContext activeCtx = (ctx1 != null) ? ctx1 : ctx2;
if (activeCtx != null) {
if (activeCtx.hasFallbackGlob && activeCtx.fallbackValue != null
&& activeCtx.fallbackValue.getDefinedBoolean() && !activeCtx.fallbackValue.getBoolean()) {
String className = activeCtx.perlClassName;
throw new PerlCompilerException("Operation \"" + methodName + "\": no method found, "
+ "argument in overloaded package " + className);
}
}
return null;
}

/**
* Tries overloaded binary operator with autogeneration support.
* @param autogenNames Additional overload names to try as autogeneration candidates (e.g., "(+" for "(+=")
Expand Down
41 changes: 37 additions & 4 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,15 @@ protected boolean removeEldestEntry(Map.Entry<Class<?>, MethodHandle> eldest) {
private static final ThreadLocal<Deque<RuntimeArray>> argsStack =
ThreadLocal.withInitial(ArrayDeque::new);

/**
* Thread-local stack of pristine (unshifted) @_ snapshots taken at sub-entry
* time. Used to populate @DB::args for caller(N) from package DB.
* In Perl, @DB::args reflects the args the sub was called with, regardless
* of whether the sub later shifted or otherwise mutated @_.
*/
private static final ThreadLocal<Deque<java.util.List<RuntimeScalar>>> pristineArgsStack =
ThreadLocal.withInitial(ArrayDeque::new);

/**
* Thread-local stack tracking whether each call frame created a fresh @_ (hasargs).
* In Perl 5, caller()[4] (hasargs) is 1 when the subroutine was called with explicit
Expand Down Expand Up @@ -200,6 +209,10 @@ public static RuntimeArray getCallerArgs() {
*/
public static void pushArgs(RuntimeArray args) {
argsStack.get().push(args);
// Snapshot the args list so @DB::args stays pristine even if the sub
// later shifts/pops from @_.
pristineArgsStack.get().push(
args != null ? new java.util.ArrayList<>(args.elements) : new java.util.ArrayList<>());
}

/**
Expand All @@ -212,6 +225,10 @@ public static void popArgs() {
if (!stack.isEmpty()) {
stack.pop();
}
Deque<java.util.List<RuntimeScalar>> pStack = pristineArgsStack.get();
if (!pStack.isEmpty()) {
pStack.pop();
}
Deque<Boolean> haStack = hasArgsStack.get();
if (!haStack.isEmpty()) {
haStack.pop();
Expand Down Expand Up @@ -2009,10 +2026,26 @@ public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar
dbArgs.setFromList(new RuntimeList());
}
} else {
// Not in debug mode - set to empty array
// This tells Carp we don't have args but prevents the
// "Incomplete caller override detected" message
dbArgs.setFromList(new RuntimeList());
// Look up pristine @_ snapshot for the requested frame.
// Pristine snapshots are captured at sub-entry, so shifts/pops
// inside the sub don't affect what @DB::args reports.
Deque<java.util.List<RuntimeScalar>> stack = pristineArgsStack.get();
int argIdx = frame - 1;
if (argIdx >= 0 && argIdx < stack.size()) {
@SuppressWarnings("unchecked")
java.util.List<RuntimeScalar>[] arr =
(java.util.List<RuntimeScalar>[]) stack.toArray(new java.util.List[0]);
java.util.List<RuntimeScalar> frameArgs = arr[argIdx];
if (frameArgs != null) {
RuntimeList rl = new RuntimeList();
rl.elements.addAll(frameArgs);
dbArgs.setFromList(rl);
} else {
dbArgs.setFromList(new RuntimeList());
}
} else {
dbArgs.setFromList(new RuntimeList());
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,12 @@ public RuntimeScalar getGlobSlot(RuntimeScalar index) {
}
yield this.hashSlot.createReference();
}
// Stash entries: *Pkg::{HASH} always returns the package's symbol table,
// even if it hasn't been explicitly materialized. This mirrors Perl 5
// where the stash is an intrinsic property of the package.
if (this.globName.endsWith("::")) {
yield GlobalVariable.getGlobalHash(this.globName).createReference();
}
// Only return reference if hash exists (has elements or was explicitly created)
if (GlobalVariable.existsGlobalHash(this.globName)) {
yield GlobalVariable.getGlobalHash(this.globName).createReference();
Expand Down
Loading