diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 394a83a3e..bbf54ca5b 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 = "1fbd174ea"; + public static final String gitCommitId = "20340661d"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java b/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java index 545c646bf..37c5091d9 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java @@ -164,9 +164,6 @@ private static void serializeBinary(RuntimeScalar scalar, StringBuilder sb, Iden "STORABLE_freeze", className, null, 0); if (freezeMethod != null && freezeMethod.type == RuntimeScalarType.CODE) { - // Track for circular reference detection before calling hook - if (scalar.value != null) seen.put(scalar.value, seen.size()); - // Call STORABLE_freeze($self, $cloning=0) RuntimeArray freezeArgs = new RuntimeArray(); RuntimeArray.push(freezeArgs, scalar); @@ -175,23 +172,31 @@ private static void serializeBinary(RuntimeScalar scalar, StringBuilder sb, Iden RuntimeArray freezeArray = new RuntimeArray(); freezeResult.setArrayOfAlias(freezeArray); - // Emit SX_HOOK + class name + serialized string + extra refs - sb.append((char) SX_HOOK); - appendInt(sb, className.length()); - sb.append(className); - - // Serialized string (first element of freeze result) - String serialized = freezeArray.size() > 0 ? freezeArray.get(0).toString() : ""; - appendInt(sb, serialized.length()); - sb.append(serialized); - - // Extra refs (remaining elements) - int extraRefs = Math.max(0, freezeArray.size() - 1); - appendInt(sb, extraRefs); - for (int i = 1; i <= extraRefs; i++) { - serializeBinary(freezeArray.get(i), sb, seen); + // Per Perl 5 Storable: empty return from STORABLE_freeze cancels the + // hook and falls through to default serialization (SX_BLESS path) + if (freezeArray.size() > 0) { + // Track for circular reference detection before emitting + if (scalar.value != null) seen.put(scalar.value, seen.size()); + + // Emit SX_HOOK + class name + serialized string + extra refs + sb.append((char) SX_HOOK); + appendInt(sb, className.length()); + sb.append(className); + + // Serialized string (first element of freeze result) + String serialized = freezeArray.get(0).toString(); + appendInt(sb, serialized.length()); + sb.append(serialized); + + // Extra refs (remaining elements) + int extraRefs = freezeArray.size() - 1; + appendInt(sb, extraRefs); + for (int i = 1; i <= extraRefs; i++) { + serializeBinary(freezeArray.get(i), sb, seen); + } + return; } - return; + // Empty return — fall through to default SX_BLESS serialization } // No hook — emit SX_BLESS + class name before the data @@ -524,27 +529,32 @@ private static RuntimeScalar deepClone(RuntimeScalar scalar, IdentityHashMap 0) { + // Create a new empty blessed object of the same class + RuntimeHash newHash = new RuntimeHash(); + RuntimeScalar newObj = newHash.createReference(); + ReferenceOperators.bless(newObj, new RuntimeScalar(className)); + cloned.put(scalar.value, newObj); + + // Call STORABLE_thaw($new_obj, $cloning=1, $serialized, @extra_refs) + RuntimeScalar thawMethod = InheritanceResolver.findMethodInHierarchy( + "STORABLE_thaw", className, null, 0); + if (thawMethod != null && thawMethod.type == RuntimeScalarType.CODE) { + RuntimeArray thawArgs = new RuntimeArray(); + RuntimeArray.push(thawArgs, newObj); + RuntimeArray.push(thawArgs, new RuntimeScalar(1)); // cloning = true + // Pass serialized data and any extra refs from freeze + for (int i = 0; i < freezeArray.size(); i++) { + RuntimeArray.push(thawArgs, freezeArray.get(i)); + } + RuntimeCode.apply(thawMethod, thawArgs, RuntimeContextType.VOID); } - RuntimeCode.apply(thawMethod, thawArgs, RuntimeContextType.VOID); - } - return newObj; + return newObj; + } + // Empty return — fall through to default deep-copy } } @@ -671,16 +681,17 @@ private static Object convertToYAMLWithTags(RuntimeScalar scalar, IdentityHashMa RuntimeArray freezeArray = new RuntimeArray(); freezeResult.setArrayOfAlias(freezeArray); - // Store serialized data with class tag - Map taggedObject = new LinkedHashMap<>(); + // Per Perl 5 Storable: empty return from STORABLE_freeze cancels the + // hook and falls through to default !!perl/hash: serialization if (freezeArray.size() > 0) { + // Store serialized data with class tag + Map taggedObject = new LinkedHashMap<>(); // STORABLE_freeze returns (serialized_string, @extra_refs) // Store the serialized string directly taggedObject.put("!!perl/freeze:" + className, freezeArray.get(0).toString()); - } else { - taggedObject.put("!!perl/freeze:" + className, ""); + return taggedObject; } - return taggedObject; + // Empty return — fall through to default !!perl/hash: serialization } Map taggedObject = new LinkedHashMap<>(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 8f1450aaf..b636fb885 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -523,6 +523,17 @@ public static RuntimeScalar deleteGlobalCodeRefAsScalar(RuntimeScalar key, Strin return deleteGlobalCodeRefAsScalar(name); } + /** + * Clears pinned code references for all subroutines in a given namespace. + * This prevents deleted subs from being resurrected by getGlobalCodeRef() + * after stash namespace deletion (e.g., delete $::{"Foo::"}). + * + * @param prefix The namespace prefix (e.g., "Foo::") to clear. + */ + public static void clearPinnedCodeRefsForNamespace(String prefix) { + pinnedCodeRefs.keySet().removeIf(k -> k.startsWith(prefix)); + } + /** * Clears the package existence cache. * Should be called when new packages are loaded or code refs are modified. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java index 1cd748de2..b0362404f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java @@ -152,6 +152,12 @@ public RuntimeScalar delete(RuntimeScalar key) { } private RuntimeScalar deleteGlob(String k) { + // Special handling for namespace keys (ending with "::") + // e.g., delete $::{"Foo::"} should remove all symbols in the Foo:: namespace + if (k.endsWith("::")) { + return deleteNamespace(k); + } + // For stash, we need to delete from GlobalVariable maps and return the glob String fullKey = namespace + k; @@ -197,6 +203,42 @@ private RuntimeScalar deleteGlob(String k) { return detached; } + /** + * Deletes an entire namespace from the stash. + * When Perl does delete $::{"Foo::"}, all symbols in the Foo:: namespace + * should be removed, making Foo->can("bar") return false and preventing + * spurious "Subroutine redefined" warnings when the namespace is re-populated. + */ + private RuntimeScalar deleteNamespace(String k) { + // Compute the prefix for child symbols. + // For main:: stash: symbols are stored as "Foo::bar" (not "main::Foo::bar"), + // so the prefix is just k itself (e.g., "Foo::") + // For other stashes: symbols are stored as "Outer::Inner::bar", + // so the prefix is namespace + k (e.g., "Outer::" + "Inner::" = "Outer::Inner::") + String childPrefix = "main::".equals(namespace) ? k : namespace + k; + + // Remove all symbols with this prefix from all global maps (prefix-based removal) + GlobalVariable.globalCodeRefs.keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.globalVariables.keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.globalArrays.keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.globalHashes.keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.globalIORefs.keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.globalFormatRefs.keySet().removeIf(key -> key.startsWith(childPrefix)); + + // Clear pinned code refs so deleted subs don't get resurrected + // by getGlobalCodeRef() lookups (e.g., in SubroutineParser redefinition check) + GlobalVariable.clearPinnedCodeRefsForNamespace(childPrefix); + + // Clear stash alias if any + GlobalVariable.clearStashAlias(childPrefix); + + // Method resolution and package existence caches are now stale + InheritanceResolver.invalidateCache(); + GlobalVariable.clearPackageCache(); + + return new RuntimeScalar(); + } + /** * Gets the size of the hash. *