From 4ddfe3434362e1a7291a443aff6bf1b2c2238230 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 20 Apr 2026 14:04:56 +0200 Subject: [PATCH 1/2] fix: make Date::Calc test suite fully pass Address several issues surfaced by `jcpan -t Date::Calc`, getting all 3005 subtests across 51 files to pass (previously 38 failures in 3 files). - Stash hash deref through glob: %{*main::F::} now resolves to the F:: stash (strip redundant leading "main::" in getGlobalHash). - Overload autogeneration order: for lt/gt/le/ge/eq/ne (and numeric < > <= >= == !=), try direct operator -> autogenerate from cmp/<=> -> nomethod, matching Perl's documented order. Previously nomethod was invoked before autogeneration, so classes that define cmp plus a dying nomethod (e.g. Date::Calc) could not auto-generate lt/gt. Added tryTwoArgumentOverloadDirect and tryTwoArgumentNomethod. - x / x= operator overloading: wire up "(x" and "(x=" overload dispatch for Operator.repeat and REPEAT_ASSIGN. - Anonymous sub caller name: caller() inside an anon sub defined within Foo::import now reports Foo::__ANON__, not Foo::import. Root cause: EmitSubroutine copied the parent sub's name into the anon sub's compile-time context. - @DB::args outside debug mode: caller() in package DB now populates @DB::args from a new pristine-args stack snapshotted at sub entry, so Carp's stack traces include argument lists (and remain pristine even if the sub later shifts @_). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../bytecode/OpcodeHandlerExtended.java | 25 ++++ .../backend/jvm/EmitSubroutine.java | 4 + .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/operators/CompareOperators.java | 130 ++++++++++++------ .../runtime/operators/Operator.java | 14 ++ .../runtime/runtimetypes/GlobalVariable.java | 6 + .../runtime/runtimetypes/OverloadContext.java | 59 ++++++++ .../runtime/runtimetypes/RuntimeCode.java | 41 +++++- 8 files changed, 237 insertions(+), 44 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 15542b091..a5eafedf1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -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], diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index 0c1e14fbd..82c0f27f2 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -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("")) { + // 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()); } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 5cfefc1cd..11e4c25d1 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -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 20 2026 13:49:59"; + public static final String buildTimestamp = "Apr 20 2026 14:02:01"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java b/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java index b88cd0320..ed030a10c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/CompareOperators.java @@ -84,15 +84,19 @@ public static RuntimeScalar lessThan(RuntimeScalar arg1, RuntimeScalar arg2) { int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<", "<"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<"); if (result != null) return result; - // Try fallback to spaceship operator - result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<=>", "<=>"); + // Try autogeneration via spaceship operator + result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<=>"); if (result != null) { checkSpaceshipResult(result, "lt (<)"); return getScalarBoolean(result.getInt() < 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(arg1, arg2, blessId, blessId2, "<"); + if (result != null) return result; } // Convert strings to numbers if necessary @@ -123,15 +127,19 @@ public static RuntimeScalar lessThanOrEqual(RuntimeScalar arg1, RuntimeScalar ar int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<=", "<="); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<="); if (result != null) return result; - // Try fallback to spaceship operator - result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<=>", "<=>"); + // Try autogeneration via spaceship operator + result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<=>"); if (result != null) { checkSpaceshipResult(result, "le (<=)"); return getScalarBoolean(result.getInt() <= 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(arg1, arg2, blessId, blessId2, "<="); + if (result != null) return result; } // Convert strings to numbers if necessary @@ -162,15 +170,19 @@ public static RuntimeScalar greaterThan(RuntimeScalar arg1, RuntimeScalar arg2) int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(>", ">"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(>"); if (result != null) return result; - // Try fallback to spaceship operator - result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<=>", "<=>"); + // Try autogeneration via spaceship operator + result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<=>"); if (result != null) { checkSpaceshipResult(result, "gt (>)"); return getScalarBoolean(result.getInt() > 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(arg1, arg2, blessId, blessId2, ">"); + if (result != null) return result; } // Check for uninitialized values (only when using numeric comparison fallback) @@ -204,15 +216,19 @@ public static RuntimeScalar greaterThanOrEqual(RuntimeScalar arg1, RuntimeScalar int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(>=", ">="); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(>="); if (result != null) return result; - // Try fallback to spaceship operator - result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<=>", "<=>"); + // Try autogeneration via spaceship operator + result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<=>"); if (result != null) { checkSpaceshipResult(result, "ge (>=)"); return getScalarBoolean(result.getInt() >= 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(arg1, arg2, blessId, blessId2, ">="); + if (result != null) return result; } // Convert strings to numbers if necessary @@ -237,15 +253,19 @@ public static RuntimeScalar equalTo(RuntimeScalar arg1, int arg2) { // Prepare overload context and check if object is eligible for overloading int blessId = blessedId(arg1); if (blessId < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(arg1, new RuntimeScalar(arg2), blessId, 0, "(==", "=="); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, new RuntimeScalar(arg2), blessId, 0, "(=="); if (result != null) return result; - // Try fallback to spaceship operator - result = OverloadContext.tryTwoArgumentOverload(arg1, new RuntimeScalar(arg2), blessId, 0, "(<=>", "<=>"); + // Try autogeneration via spaceship operator + result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, new RuntimeScalar(arg2), blessId, 0, "(<=>"); if (result != null) { checkSpaceshipResult(result, "eq (==)"); return getScalarBoolean(result.getInt() == 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(arg1, new RuntimeScalar(arg2), blessId, 0, "=="); + if (result != null) return result; } // Convert strings to numbers if necessary @@ -275,15 +295,19 @@ public static RuntimeScalar equalTo(RuntimeScalar arg1, RuntimeScalar arg2) { int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(==", "=="); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(=="); if (result != null) return result; - // Try fallback to spaceship operator - result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<=>", "<=>"); + // Try autogeneration via spaceship operator + result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<=>"); if (result != null) { checkSpaceshipResult(result, "eq (==)"); return getScalarBoolean(result.getInt() == 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(arg1, arg2, blessId, blessId2, "=="); + if (result != null) return result; } // Convert strings to numbers if necessary @@ -314,15 +338,19 @@ public static RuntimeScalar notEqualTo(RuntimeScalar arg1, RuntimeScalar arg2) { int blessId = blessedId(arg1); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(!=", "!="); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(!="); if (result != null) return result; - // Try fallback to spaceship operator - result = OverloadContext.tryTwoArgumentOverload(arg1, arg2, blessId, blessId2, "(<=>", "<=>"); + // Try autogeneration via spaceship operator + result = OverloadContext.tryTwoArgumentOverloadDirect(arg1, arg2, blessId, blessId2, "(<=>"); if (result != null) { checkSpaceshipResult(result, "ne (!=)"); return getScalarBoolean(result.getInt() != 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(arg1, arg2, blessId, blessId2, "!="); + if (result != null) return result; } // Convert strings to numbers if necessary @@ -407,14 +435,18 @@ public static RuntimeScalar eq(RuntimeScalar runtimeScalar, RuntimeScalar arg2) int blessId = blessedId(runtimeScalar); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(eq", "eq"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(eq"); if (result != null) return result; - // Try fallback to cmp operator - result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(cmp", "cmp"); + // Try autogeneration via cmp operator + result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(cmp"); if (result != null) { return getScalarBoolean(result.getInt() == 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(runtimeScalar, arg2, blessId, blessId2, "eq"); + if (result != null) return result; } return getScalarBoolean(runtimeScalar.toString().equals(arg2.toString())); @@ -432,14 +464,18 @@ public static RuntimeScalar ne(RuntimeScalar runtimeScalar, RuntimeScalar arg2) int blessId = blessedId(runtimeScalar); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(ne", "ne"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(ne"); if (result != null) return result; - // Try fallback to cmp operator - result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(cmp", "cmp"); + // Try autogeneration via cmp operator + result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(cmp"); if (result != null) { return getScalarBoolean(result.getInt() != 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(runtimeScalar, arg2, blessId, blessId2, "ne"); + if (result != null) return result; } return getScalarBoolean(!runtimeScalar.toString().equals(arg2.toString())); @@ -457,14 +493,18 @@ public static RuntimeScalar lt(RuntimeScalar runtimeScalar, RuntimeScalar arg2) int blessId = blessedId(runtimeScalar); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(lt", "lt"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(lt"); if (result != null) return result; - // Try fallback to cmp operator - result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(cmp", "cmp"); + // Try autogeneration via cmp operator + result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(cmp"); if (result != null) { return getScalarBoolean(result.getInt() < 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(runtimeScalar, arg2, blessId, blessId2, "lt"); + if (result != null) return result; } return getScalarBoolean(runtimeScalar.toString().compareTo(arg2.toString()) < 0); @@ -482,14 +522,18 @@ public static RuntimeScalar le(RuntimeScalar runtimeScalar, RuntimeScalar arg2) int blessId = blessedId(runtimeScalar); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(le", "le"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(le"); if (result != null) return result; - // Try fallback to cmp operator - result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(cmp", "cmp"); + // Try autogeneration via cmp operator + result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(cmp"); if (result != null) { return getScalarBoolean(result.getInt() <= 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(runtimeScalar, arg2, blessId, blessId2, "le"); + if (result != null) return result; } return getScalarBoolean(runtimeScalar.toString().compareTo(arg2.toString()) <= 0); @@ -507,14 +551,18 @@ public static RuntimeScalar gt(RuntimeScalar runtimeScalar, RuntimeScalar arg2) int blessId = blessedId(runtimeScalar); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(gt", "gt"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(gt"); if (result != null) return result; - // Try fallback to cmp operator - result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(cmp", "cmp"); + // Try autogeneration via cmp operator + result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(cmp"); if (result != null) { return getScalarBoolean(result.getInt() > 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(runtimeScalar, arg2, blessId, blessId2, "gt"); + if (result != null) return result; } return getScalarBoolean(runtimeScalar.toString().compareTo(arg2.toString()) > 0); @@ -532,14 +580,18 @@ public static RuntimeScalar ge(RuntimeScalar runtimeScalar, RuntimeScalar arg2) int blessId = blessedId(runtimeScalar); int blessId2 = blessedId(arg2); if (blessId < 0 || blessId2 < 0) { - RuntimeScalar result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(ge", "ge"); + RuntimeScalar result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(ge"); if (result != null) return result; - // Try fallback to cmp operator - result = OverloadContext.tryTwoArgumentOverload(runtimeScalar, arg2, blessId, blessId2, "(cmp", "cmp"); + // Try autogeneration via cmp operator + result = OverloadContext.tryTwoArgumentOverloadDirect(runtimeScalar, arg2, blessId, blessId2, "(cmp"); if (result != null) { return getScalarBoolean(result.getInt() >= 0); } + + // Try nomethod fallback (may throw if fallback=0) + result = OverloadContext.tryTwoArgumentNomethod(runtimeScalar, arg2, blessId, blessId2, "ge"); + if (result != null) return result; } return getScalarBoolean(runtimeScalar.toString().compareTo(arg2.toString()) >= 0); diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index c843acbbf..d6338f4d2 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -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()) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index eeb3f5ed1..770b02c25 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -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 ::) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java index 79bcfb260..c9407ecba 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java @@ -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 "(+=") diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index f6764b33b..ee0703f78 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -150,6 +150,15 @@ protected boolean removeEldestEntry(Map.Entry, MethodHandle> eldest) { private static final ThreadLocal> 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>> 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 @@ -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<>()); } /** @@ -212,6 +225,10 @@ public static void popArgs() { if (!stack.isEmpty()) { stack.pop(); } + Deque> pStack = pristineArgsStack.get(); + if (!pStack.isEmpty()) { + pStack.pop(); + } Deque haStack = hasArgsStack.get(); if (!haStack.isEmpty()) { haStack.pop(); @@ -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> stack = pristineArgsStack.get(); + int argIdx = frame - 1; + if (argIdx >= 0 && argIdx < stack.size()) { + @SuppressWarnings("unchecked") + java.util.List[] arr = + (java.util.List[]) stack.toArray(new java.util.List[0]); + java.util.List 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()); + } } } From 9f38ef5506a4f91743ac02ac6455418224cc1f17 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 20 Apr 2026 14:22:52 +0200 Subject: [PATCH 2/2] fix: *Pkg::{HASH} returns the package stash + regression fix Fixes a regression in op/attrs.t (test 49, "Bogus CODE attribute shared should fail") caused by the previous @DB::args fix. Once @DB::args is populated with a CODE ref, Carp calls format_arg -> _maybe_isa. In our bundled Carp, _maybe_isa is installed via _fetch_sub("UNIVERSAL","isa"), which needs *UNIVERSAL::{HASH} to return the UNIVERSAL:: stash. - RuntimeGlob.getGlobSlot("HASH"): for stash globs (globName ends with "::"), always return the package's stash via getGlobalHash, even if nothing has explicitly materialized it yet. Matches Perl 5 where the stash is an intrinsic property of the package. - GlobalVariable.existsGlobalHash: also normalize "main::Pkg::" -> "Pkg::" for stash lookups (mirrors the existing normalization in getGlobalHash). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/main/java/org/perlonjava/core/Configuration.java | 6 +++--- .../perlonjava/runtime/runtimetypes/GlobalVariable.java | 7 ++++++- .../org/perlonjava/runtime/runtimetypes/RuntimeGlob.java | 6 ++++++ 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 11e4c25d1..bec167c66 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,14 +33,14 @@ 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"). @@ -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 20 2026 14:02:01"; + public static final String buildTimestamp = "Apr 20 2026 14:34:31"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 770b02c25..985faba87 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -385,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; } /** diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 9accc7559..ed7fb68d8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -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();