From 33e29073cbb79962dd52064221e0c9cb6ce363e7 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 15 Mar 2026 22:52:48 +0100 Subject: [PATCH 1/3] Fix stash keys for nested packages and &{} overload handling Two fixes for Moo compatibility: 1. Stash keys for nested packages must include trailing `::` - `keys %Foo::Bar::` was returning `Baz` instead of `Baz::` - This broke Role::Tiny::_load_module which checks for `/::\z/` suffix - Fix: HashSpecialVariable.java - include `::` in entryKey 2. \&{$blessed_obj} must throw "Not a subroutine reference" - For objects without &{} overload, was creating symbolic reference - Perl throws "Not a subroutine reference" in this case - Fix: RuntimeCode.java - check blessId and throw if no &{} overload Moo test results: 61/71 programs passing (86%), 765/829 subtests (92%) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/design/moo_support.md | 44 ++++++++++++++++++- .../org/perlonjava/core/Configuration.java | 2 +- .../runtimetypes/HashSpecialVariable.java | 11 +++-- .../runtime/runtimetypes/RuntimeCode.java | 36 +++++++++++++++ 4 files changed, 84 insertions(+), 9 deletions(-) diff --git a/dev/design/moo_support.md b/dev/design/moo_support.md index a6b4c46ef..8a883ad83 100644 --- a/dev/design/moo_support.md +++ b/dev/design/moo_support.md @@ -547,11 +547,51 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas - This ensures the actual hash/array container is passed to `undefine()`, not a scalar - ExifTool PDF.t now passes all 26 tests (was 7/26) +- [x] Phase 22: Fix stash keys for nested packages and overload &{} handling (2026-03-15) + - **Two fixes in this phase**: + + - **Fix 1: Stash keys for nested packages must include trailing `::`** + - Root cause: `keys %Foo::Bar::` was returning `Baz` instead of `Baz::` for nested packages + - This broke `Role::Tiny::_load_module` which uses `grep !/::\z/, keys %{_getstash($module)}` + to detect if a module's stash has actual symbols vs just sub-package markers + - **HashSpecialVariable.java fix**: + - Changed `entryKey = remainingKey.substring(0, nextSeparatorIndex)` + to `entryKey = remainingKey.substring(0, nextSeparatorIndex + 2)` + - Now stash keys correctly include `::` suffix for sub-packages + - Fixes t/load_module_role_tiny.t (0/2 → 2/2) + + - **Fix 2: \&{$blessed_obj} must throw "Not a subroutine reference" for objects without &{} overload** + - Root cause: `\&{$obj}` on a blessed object without `&{}` overload was creating a symbolic + reference instead of throwing an error + - In Perl: `\&{bless({}, "Foo")}` throws "Not a subroutine reference" + - In PerlOnJava: was creating CODE ref pointing to non-existent `&Foo=HASH(...)` sub + - **RuntimeCode.java fix (createCodeReference)**: + - Check `blessedId()`: negative = blessed with overload, positive = blessed without, 0 = not blessed + - For `blessId != 0` (blessed), try `&{}` overload if available + - If no `&{}` overload exists, throw "Not a subroutine reference" + - Also added check for unblessed REFERENCE types to throw same error + - Fixes t/method-generate-accessor.t (41/49 → 46/49), t/coerce-1.t (0/2 → 2/2) + +### Current Status + +**Test Results (after Phase 22):** +- 61/71 test programs passing (86%) +- ~765/829 subtests passing (92%) + +**Remaining Failures (categorized):** +1. **accessor-weaken tests** (20 failures) - Expected, weak references not supported in Java GC +2. **croak-locations.t** (29 failures) - Carp reports `(eval N)` instead of actual filename +3. **demolish tests** (6 failures) - Expected, DESTROY not supported +4. **method-generate-accessor.t** (3 failures) - quote_sub inlinification issues +5. **moo-utils-_subname-Sub-Name.t** (1 failure) - Expected, we have Sub::Util (no fallback to Sub::Name) +6. **no-moo.t** (5 failures) - Namespace cleanup requires weak references +7. **overloaded-coderefs.t** - Expected, B::Deparse not available + ### Next Steps -1. **Fix no-moo.t cleanup** - `no Moo` should remove `extends`, `has`, etc. from namespace +1. **Investigate croak-locations.t** - Carp reports `(eval N)` instead of actual filename -2. **Prototype checking** - `$$` prototype should accept `@array` argument (workaround: removed prototype) +2. **Investigate quote_sub issues** - Sub::Quote inlinification not working correctly for coerce/trigger/isa 3. **DEMOLISH support** - Expected to remain unsupported (requires DESTROY/GC hooks) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index a85fe0308..06f76919b 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ 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 = "e78cba6d4"; + public static final String gitCommitId = "5b9397d5d"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java index aeeb64b64..3a3f7ef80 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java @@ -104,9 +104,9 @@ public Set> entrySet() { if (nextSeparatorIndex == -1) { entryKey = remainingKey; } else { - // Stash keys for nested packages are reported without the trailing "::" - // (e.g. "Foo" instead of "Foo::") - entryKey = remainingKey.substring(0, nextSeparatorIndex); + // Stash keys for nested packages include the trailing "::" + // (e.g. "Foo::" not "Foo") - this is how Perl indicates sub-packages + entryKey = remainingKey.substring(0, nextSeparatorIndex + 2); } // Special sort variables should not show up in stash enumeration @@ -118,9 +118,8 @@ public Set> entrySet() { continue; } - String globName = (nextSeparatorIndex == -1) - ? (namespace + entryKey) - : (namespace + entryKey + "::"); + // entryKey already includes "::" for nested packages + String globName = namespace + entryKey; // Add the entry only if it's not already in the set of unique keys if (uniqueKeys.add(entryKey)) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 4ea578696..9dd8d12b6 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1922,6 +1922,42 @@ public static RuntimeScalar createCodeReference(RuntimeScalar runtimeScalar, Str return runtimeScalar; } + // Check if object is eligible for &{} overloading (e.g., blessed object with &{} operator) + // This handles cases like \&{$constraint_obj} where $constraint_obj overloads &{} + // blessId: negative = blessed with overload, positive = blessed without overload, 0 = not blessed + int blessId = blessedId(runtimeScalar); + // System.err.println("DEBUG createCodeReference: type=" + runtimeScalar.type + " blessId=" + blessId + " value=" + runtimeScalar.value); + if (blessId != 0) { + // Object is blessed + if (blessId < 0) { + // Has overloading - try to get &{} overload + OverloadContext ctx = OverloadContext.prepare(blessId); + if (ctx != null) { + RuntimeScalar result = ctx.tryOverload("(&{}", new RuntimeArray(runtimeScalar)); + if (result != null && result.value.hashCode() != runtimeScalar.value.hashCode()) { + // Successfully got a CODE reference via overload, return it + if (result.type == RuntimeScalarType.CODE) { + return result; + } + // Recursively handle if not CODE yet + return createCodeReference(result, packageName); + } + } + } + // Blessed reference without &{} overload - this is an error in Perl + // "Not a subroutine reference" + throw new PerlCompilerException("Not a subroutine reference"); + } + + // Check if this is a reference type that isn't CODE - error "Not a subroutine reference" + // This catches cases like \&{$hashref} where $hashref is an unblessed reference + if (runtimeScalar.type == RuntimeScalarType.REFERENCE) { + RuntimeScalar deref = (RuntimeScalar) runtimeScalar.value; + if (deref.type != RuntimeScalarType.CODE) { + throw new PerlCompilerException("Not a subroutine reference"); + } + } + String name = NameNormalizer.normalizeVariableName(runtimeScalar.toString(), packageName); // System.out.println("Creating code reference: " + name + " got: " + GlobalContext.getGlobalCodeRef(name)); RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(name); From 897f68ddc692bb06a09e985a3392f1d42a8293f6 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sun, 15 Mar 2026 23:02:17 +0100 Subject: [PATCH 2/3] Fix local @_ in string eval to use register-based localization When compiling local @_ = (...) in string eval, the compiler was localizing @main::_ (global array) instead of register 1 which holds the actual @_ for the subroutine. In PerlOnJava, @_ in a subroutine (register 1) and @main::_ are different arrays. This caused Sub::Quote local @_ = ($value) inlinification pattern to fail silently. Fix: For reserved variables like @_, use PUSH_LOCAL_VARIABLE on the register directly instead of loading and localizing the global array. Moo test results: 62/71 programs (87%), 768/829 subtests (93%) - method-generate-accessor.t: 49/49 (was 46/49) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/design/moo_support.md | 43 ++++++++++++++----- .../backend/bytecode/BytecodeCompiler.java | 27 ++++++++++++ .../backend/bytecode/CompileAssignment.java | 11 +++++ .../org/perlonjava/core/Configuration.java | 2 +- 4 files changed, 72 insertions(+), 11 deletions(-) diff --git a/dev/design/moo_support.md b/dev/design/moo_support.md index 8a883ad83..f1fd39bec 100644 --- a/dev/design/moo_support.md +++ b/dev/design/moo_support.md @@ -572,28 +572,51 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas - Also added check for unblessed REFERENCE types to throw same error - Fixes t/method-generate-accessor.t (41/49 → 46/49), t/coerce-1.t (0/2 → 2/2) +- [x] Phase 23: Fix `local @_` in string eval (2026-03-15) + - Root cause: `local @_ = (...)` in string eval was localizing `@main::_` (global) instead of + register 1 which holds the actual `@_` for the subroutine + - In PerlOnJava, `@_` in a subroutine (register 1) and `@main::_` are different arrays + - When compiling `local @_ = (...)`, the compiler was emitting: + - `LOAD_GLOBAL_ARRAY @main::_` followed by `PUSH_LOCAL_VARIABLE` and assignment + - For reserved variables like `@_`, we need to use register-based localization: + - `PUSH_LOCAL_VARIABLE r1` to save register 1's state + - `ARRAY_SET_FROM_LIST r1` to assign new values + - **CompileAssignment.java fix**: + - In `handleLocalAssignment`, check `bc.isReservedVariable(varName)` for `@` case + - If reserved, use `PUSH_LOCAL_VARIABLE` on the register directly instead of loading global + - **BytecodeCompiler.java fix**: + - Similar fix for standalone `local @_` without assignment + - This fixes Sub::Quote's `local @_ = ($value)` inlinification pattern + - Fixes t/method-generate-accessor.t (46/49 → 49/49) + ### Current Status -**Test Results (after Phase 22):** -- 61/71 test programs passing (86%) -- ~765/829 subtests passing (92%) +**Test Results (after Phase 23):** +- 62/71 test programs passing (87%) +- ~768/829 subtests passing (93%) **Remaining Failures (categorized):** 1. **accessor-weaken tests** (20 failures) - Expected, weak references not supported in Java GC 2. **croak-locations.t** (29 failures) - Carp reports `(eval N)` instead of actual filename 3. **demolish tests** (6 failures) - Expected, DESTROY not supported -4. **method-generate-accessor.t** (3 failures) - quote_sub inlinification issues -5. **moo-utils-_subname-Sub-Name.t** (1 failure) - Expected, we have Sub::Util (no fallback to Sub::Name) -6. **no-moo.t** (5 failures) - Namespace cleanup requires weak references -7. **overloaded-coderefs.t** - Expected, B::Deparse not available +4. **moo-utils-_subname-Sub-Name.t** (1 failure) - Expected, we have Sub::Util (no fallback to Sub::Name) +5. **no-moo.t** (5 failures) - Namespace cleanup requires weak references +6. **overloaded-coderefs.t** - Expected, B::Deparse not available + +**Expected failures** (not fixable without fundamental changes): +- Weak references: accessor-weaken tests (20), no-moo.t cleanup (5) +- DESTROY/GC: demolish tests (6) +- Missing B::Deparse: overloaded-coderefs.t +- Sub::Name fallback: moo-utils-_subname-Sub-Name.t (1) + +**Potentially fixable**: +- croak-locations.t (29) - Carp filename in string eval ### Next Steps 1. **Investigate croak-locations.t** - Carp reports `(eval N)` instead of actual filename -2. **Investigate quote_sub issues** - Sub::Quote inlinification not working correctly for coerce/trigger/isa - -3. **DEMOLISH support** - Expected to remain unsupported (requires DESTROY/GC hooks) +2. **DEMOLISH support** - Expected to remain unsupported (requires DESTROY/GC hooks) ### PR Information - **Branch**: `feature/moo-support` (PR #319 - merged) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 03946e53f..38c83acad 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3068,6 +3068,33 @@ void compileVariableDeclaration(OperatorNode node, String op) { boolean isDeclaredReference = node.annotations != null && Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + // For reserved variables like @_, use register-based localization + // because register 1 holds @_ which is different from @main::_ + if (isReservedVariable(varName)) { + int regIdx = getVariableRegister(varName); + + // Emit PUSH_LOCAL_VARIABLE to save register state + emit(Opcodes.PUSH_LOCAL_VARIABLE); + emitReg(regIdx); + + // The result is the register itself (for assignment) + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg1 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg1); + emitReg(regIdx); + + int refReg2 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg2); + emitReg(refReg1); + lastResultReg = refReg2; + } else { + lastResultReg = regIdx; + } + return; + } + String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); int nameIdx = addToStringPool(globalVarName); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 19fb3845d..9ad602449 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -53,6 +53,17 @@ private static boolean handleLocalAssignment(BytecodeCompiler bc, BinaryOperator bc.emitReg(valueReg); } case "@" -> { + // For reserved variables like @_, use register-based localization + if (bc.isReservedVariable(varName)) { + int regIdx = bc.getVariableRegister(varName); + bc.emit(Opcodes.PUSH_LOCAL_VARIABLE); + bc.emitReg(regIdx); + bc.emit(Opcodes.ARRAY_SET_FROM_LIST); + bc.emitReg(regIdx); + bc.emitReg(valueReg); + bc.lastResultReg = regIdx; + return true; + } bc.emit(Opcodes.LOAD_GLOBAL_ARRAY); bc.emitReg(localReg); bc.emit(nameIdx); diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 06f76919b..4b35f9c14 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ 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 = "5b9397d5d"; + public static final String gitCommitId = "a89a23a58"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). From 9654baee45e368071e249122b52e4e84c7b5aeb1 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 16 Mar 2026 07:43:21 +0100 Subject: [PATCH 3/3] Fix glob aliasing regression in mro/isa_aliases.t When *a = *b is followed by *a = \@x, both aliased globs should be updated because they share the same glob slots. Previously, only the directly assigned glob was updated, breaking ISA resolution. Added glob alias tracking: - GlobalVariable.globAliases map tracks which globs share slots - getGlobAliasGroup() returns all globs aliased to the same target - When assigning array/hash refs to a glob, all aliased globs are updated This fixes the regression where mro/isa_aliases.t dropped from 11/13 to 10/13 passing tests. Now all 13 tests pass. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/runtimetypes/GlobalVariable.java | 49 +++++++++++++++++++ .../runtime/runtimetypes/RuntimeGlob.java | 23 +++++++-- 2 files changed, 68 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index a48c41a77..bf79834df 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -40,6 +40,11 @@ public class GlobalVariable { // to Dst:: symbols can still point to their original objects. static final Map stashAliases = new HashMap<>(); + // Glob aliasing: `*a = *b` makes a and b share the same glob. + // Maps glob names to their canonical (target) name. + // When looking up or assigning to glob slots, we resolve through this map. + static final Map globAliases = new HashMap<>(); + // Flags used by operator override // globalGlobs: Tracks typeglob assignments (e.g., *CORE::GLOBAL::hex = sub {...}) // Used to detect when built-in operators have been globally overridden @@ -66,6 +71,7 @@ public static void resetAllGlobals() { globalGlobs.clear(); isSubs.clear(); stashAliases.clear(); + globAliases.clear(); clearPackageCache(); RuntimeCode.clearCaches(); @@ -105,6 +111,49 @@ public static String resolveStashAlias(String namespace) { return aliased; } + /** + * Sets a glob alias. After `*a = *b`, calling setGlobAlias("a", "b") makes + * all slot assignments to "a" also affect "b" and vice versa. + */ + public static void setGlobAlias(String fromGlob, String toGlob) { + // Find the canonical name for toGlob (in case it's already an alias) + String canonical = resolveGlobAlias(toGlob); + globAliases.put(fromGlob, canonical); + // Also ensure toGlob points to the canonical name + if (!toGlob.equals(canonical)) { + globAliases.put(toGlob, canonical); + } + } + + /** + * Resolves a glob name to its canonical name. + * If the glob is aliased, returns the target name; otherwise returns the input. + */ + public static String resolveGlobAlias(String globName) { + String aliased = globAliases.get(globName); + if (aliased != null) { + // Follow the chain in case of multiple aliases + return resolveGlobAlias(aliased); + } + return globName; + } + + /** + * Gets all glob names that are aliased to the same canonical name. + * This is used when assigning to a glob slot - we need to update all aliases. + */ + public static java.util.List getGlobAliasGroup(String globName) { + String canonical = resolveGlobAlias(globName); + java.util.List group = new java.util.ArrayList<>(); + group.add(canonical); + for (Map.Entry entry : globAliases.entrySet()) { + if (resolveGlobAlias(entry.getKey()).equals(canonical) && !group.contains(entry.getKey())) { + group.add(entry.getKey()); + } + } + return group; + } + /** * Retrieves a global variable by its key, initializing it if necessary. * If the key matches a regex capture variable pattern, it initializes a special variable. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 4b73866e5..adb239949 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -106,14 +106,20 @@ public RuntimeScalar set(RuntimeScalar value) { case ARRAYREFERENCE: // Handle the case where a typeglob is assigned a reference to an array // `*foo = \@bar` creates an alias - both names refer to the same array + // Also update all glob aliases if (value.value instanceof RuntimeArray arr) { - GlobalVariable.globalArrays.put(this.globName, arr); + for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) { + GlobalVariable.globalArrays.put(aliasedName, arr); + } } return value; case HASHREFERENCE: // `*foo = \%bar` creates an alias - both names refer to the same hash + // Also update all glob aliases if (value.value instanceof RuntimeHash hash) { - GlobalVariable.globalHashes.put(this.globName, hash); + for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) { + GlobalVariable.globalHashes.put(aliasedName, hash); + } } return value; case REFERENCE: @@ -127,10 +133,16 @@ public RuntimeScalar set(RuntimeScalar value) { InheritanceResolver.invalidateCache(); } else if (deref.type == RuntimeScalarType.ARRAYREFERENCE && deref.value instanceof RuntimeArray arr) { // `*foo = \@bar` assigns to the ARRAY slot. - GlobalVariable.globalArrays.put(this.globName, arr); + // Also update all glob aliases + for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) { + GlobalVariable.globalArrays.put(aliasedName, arr); + } } else if (deref.type == RuntimeScalarType.HASHREFERENCE && deref.value instanceof RuntimeHash hash) { // `*foo = \%bar` assigns to the HASH slot. - GlobalVariable.globalHashes.put(this.globName, hash); + // Also update all glob aliases + for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) { + GlobalVariable.globalHashes.put(aliasedName, hash); + } } else if (value.type == RuntimeScalarType.REFERENCE && deref.type == RuntimeScalarType.ARRAYREFERENCE) { // `*foo = \$array_ref` creates a constant subroutine returning the array reference RuntimeCode constSub = new RuntimeCode("", null); @@ -205,6 +217,9 @@ public RuntimeScalar set(RuntimeGlob value) { return value.scalar(); } + // Register glob alias so future slot assignments affect both globs + GlobalVariable.setGlobAlias(this.globName, value.globName); + // Retrieve the RuntimeScalar value associated with the provided RuntimeGlob. RuntimeScalar result = value.scalar();