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
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
97 changes: 54 additions & 43 deletions src/main/java/org/perlonjava/runtime/perlmodule/Storable.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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
Expand Down Expand Up @@ -524,27 +529,32 @@ private static RuntimeScalar deepClone(RuntimeScalar scalar, IdentityHashMap<Obj
RuntimeArray freezeArray = new RuntimeArray();
freezeResult.setArrayOfAlias(freezeArray);

// 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));
// Per Perl 5 Storable: empty return from STORABLE_freeze cancels the
// hook and falls through to default deep-copy
if (freezeArray.size() > 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
}
}

Expand Down Expand Up @@ -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<String, Object> 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<String, Object> 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<String, Object> taggedObject = new LinkedHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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.
*
Expand Down
Loading