diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 394a83a3e..2d55a9bf8 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 = "ed722c0cd"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). 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. *