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
4 changes: 2 additions & 2 deletions 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 = "dfa87a6c4";
public static final String gitCommitId = "846d4d854";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand All @@ -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 28 2026 16:14:51";
public static final String buildTimestamp = "Apr 28 2026 17:18:57";

// Prevent instantiation
private Configuration() {
Expand Down
21 changes: 15 additions & 6 deletions src/main/java/org/perlonjava/runtime/io/SocketIO.java
Original file line number Diff line number Diff line change
Expand Up @@ -197,13 +197,22 @@ public RuntimeScalar connect(String address, int port) {
return scalarUndef;
}

// Auto-bind if not already bound so getsockname() returns local address
// even before the connection completes (Java NIO doesn't expose the
// local address until finishConnect() without this).
// Bind to the same IP as the target so getsockname() returns the
// correct local address (matching Perl's kernel behavior).
// Auto-bind to the wildcard address if not already bound so
// getsockname() returns *some* local address even before the
// connection completes (Java NIO doesn't expose the local
// address until finishConnect() otherwise).
// We bind to the wildcard (0.0.0.0:0) instead of the target's
// IP because we cannot bind to an address we don't own — doing
// so fails with "Can't assign requested address" for any
// remote target. The kernel will pick the proper source
// address based on routing once the connect proceeds.
if (socketChannel.getLocalAddress() == null) {
socketChannel.bind(new InetSocketAddress(target.getAddress(), 0));
try {
socketChannel.bind(new InetSocketAddress(0));
} catch (IOException ignore) {
// If even wildcard bind fails, fall through to connect
// and let it surface the real error.
}
}
boolean connected = socketChannel.connect(target);
if (!connected) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,26 @@ private static void populateIsaMapHelper(String className,
* @return RuntimeScalar representing the found method, or null if not found
*/
public static RuntimeScalar findMethodInHierarchy(String methodName, String perlClassName, String cacheKey, int startFromIndex) {
return findMethodInHierarchy(methodName, perlClassName, cacheKey, startFromIndex, true);
}

/**
* Like {@link #findMethodInHierarchy(String, String, String, int)} but without the
* AUTOLOAD fallback. Pass {@code checkAutoload=false} for callers that need
* Perl's {@code gv_fetchmethod_autoload(..., FALSE)} semantics — for example,
* Storable's STORABLE_freeze / STORABLE_thaw / STORABLE_attach hook lookup,
* which must NOT promote an inherited AUTOLOAD into the hook (the AUTOLOAD
* would be invoked with {@code $AUTOLOAD = "Pkg::STORABLE_freeze"}, which the
* AUTOLOAD typically isn't expecting and just dies on).
*
* @param methodName method name to find
* @param perlClassName starting class
* @param cacheKey cache key (null = default)
* @param startFromIndex starting index in linearized hierarchy
* @param checkAutoload whether to fall back to AUTOLOAD when method is not directly defined
* @return RuntimeScalar representing the found method, or null if not found
*/
public static RuntimeScalar findMethodInHierarchy(String methodName, String perlClassName, String cacheKey, int startFromIndex, boolean checkAutoload) {
if (TRACE_METHOD_RESOLUTION) {
System.err.println("TRACE InheritanceResolver.findMethodInHierarchy:");
System.err.println(" methodName: '" + methodName + "'");
Expand All @@ -309,6 +329,11 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
// Normalize the method name for consistent caching
cacheKey = NameNormalizer.normalizeVariableName(methodName, perlClassName);
}
// Use a separate cache slot for no-AUTOLOAD lookups so they don't
// pollute (or get polluted by) normal lookups which DO promote AUTOLOAD.
if (!checkAutoload) {
cacheKey = cacheKey + "\0noautoload";
}

if (TRACE_METHOD_RESOLUTION) {
System.err.println(" cacheKey: '" + cacheKey + "'");
Expand Down Expand Up @@ -368,7 +393,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
// Second pass — method not found anywhere, check AUTOLOAD in class hierarchy.
// This matches Perl semantics: AUTOLOAD is only tried after the full MRO
// search (including UNIVERSAL) fails to find the method.
if (autoloadEnabled && !methodName.startsWith("(")) {
if (autoloadEnabled && checkAutoload && !methodName.startsWith("(")) {
for (int i = startFromIndex; i < linearizedClasses.size(); i++) {
String className = linearizedClasses.get(i);
String effectiveClassName = GlobalVariable.resolveStashAlias(className);
Expand Down
118 changes: 101 additions & 17 deletions src/main/java/org/perlonjava/runtime/perlmodule/Storable.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.perlonjava.runtime.perlmodule;

import org.perlonjava.runtime.operators.ModuleOperators;
import org.perlonjava.runtime.operators.ReferenceOperators;
import org.perlonjava.runtime.operators.WarnDie;
import org.perlonjava.runtime.runtimetypes.*;
Expand Down Expand Up @@ -176,7 +177,7 @@ private static void serializeBinary(RuntimeScalar scalar, StringBuilder sb, Iden

// Check for STORABLE_freeze hook
RuntimeScalar freezeMethod = InheritanceResolver.findMethodInHierarchy(
"STORABLE_freeze", className, null, 0);
"STORABLE_freeze", className, null, 0, false);

if (freezeMethod != null && freezeMethod.type == RuntimeScalarType.CODE) {
// Call STORABLE_freeze($self, $cloning=0)
Expand All @@ -195,11 +196,27 @@ private static void serializeBinary(RuntimeScalar scalar, StringBuilder sb, Iden
// 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
// Emit SX_HOOK + class name + ref-type byte + serialized string + extra refs
// The ref-type byte tells SX_HOOK reader what kind of empty
// reference to create before passing to STORABLE_thaw
// (required because hooks like URI's bless a SCALAR ref —
// creating a HASH ref would make `$$self = $str` croak).
sb.append((char) SX_HOOK);
appendInt(sb, className.length());
sb.append(className);

// Encode the original reference type so SX_HOOK reader can
// recreate the same kind of reference.
char refTypeByte;
if (scalar.type == RuntimeScalarType.ARRAYREFERENCE) {
refTypeByte = 'A';
} else if (scalar.type == RuntimeScalarType.REFERENCE) {
refTypeByte = 'S';
} else {
refTypeByte = 'H'; // hash ref (default)
}
sb.append(refTypeByte);

// Serialized string (first element of freeze result)
String serialized = freezeArray.get(0).toString();
appendInt(sb, serialized.length());
Expand Down Expand Up @@ -321,6 +338,11 @@ private static RuntimeScalar deserializeBinary(String data, int[] pos, List<Runt
String hookClass = data.substring(pos[0], pos[0] + classLen);
pos[0] += classLen;

// Reference type byte (matches what serializeBinary emitted):
// 'A'=array, 'S'=scalar, 'H'=hash. Created in 2026 to fix
// STORABLE_thaw on scalar-ref-blessed classes like URI.
char refTypeByte = data.charAt(pos[0]++);

// Read serialized string
int serLen = readInt(data, pos);
String serialized = data.substring(pos[0], pos[0] + serLen);
Expand All @@ -333,15 +355,24 @@ private static RuntimeScalar deserializeBinary(String data, int[] pos, List<Runt
extraRefs.add(deserializeBinary(data, pos, refList));
}

// Create new blessed object
RuntimeHash newHash = new RuntimeHash();
result = newHash.createAnonymousReference();
// Create new blessed object of the same reference type as the
// original. URI etc. expect a scalar ref, others expect a hash
// or array ref.
if (refTypeByte == 'A') {
result = new RuntimeArray().createAnonymousReference();
} else if (refTypeByte == 'S') {
result = new RuntimeScalar().createReference();
} else {
RuntimeHash newHash = new RuntimeHash();
result = newHash.createAnonymousReference();
}
requireClassForBlessOnRetrieve(hookClass);
ReferenceOperators.bless(result, new RuntimeScalar(hookClass));
refList.add(result);

// Call STORABLE_thaw($new_obj, $cloning=0, $serialized, @extra_refs)
RuntimeScalar thawMethod = InheritanceResolver.findMethodInHierarchy(
"STORABLE_thaw", hookClass, null, 0);
"STORABLE_thaw", hookClass, null, 0, false);
if (thawMethod != null && thawMethod.type == RuntimeScalarType.CODE) {
RuntimeArray thawArgs = new RuntimeArray();
RuntimeArray.push(thawArgs, result);
Expand Down Expand Up @@ -408,6 +439,7 @@ private static RuntimeScalar deserializeBinary(String data, int[] pos, List<Runt
}

if (blessClass != null) {
requireClassForBlessOnRetrieve(blessClass);
ReferenceOperators.bless(result, new RuntimeScalar(blessClass));
}
return result;
Expand Down Expand Up @@ -550,6 +582,32 @@ public static RuntimeList dclone(RuntimeArray args, int ctx) {
*
* @param args the temporary args array to release
*/
/**
* Best-effort attempt to load a class before blessing a retrieved object
* into it. Without this, blessing into a not-yet-loaded class causes the
* blessId to be allocated as "non-overloaded" (positive ID) — and once
* cached, that ID stays positive forever, so even after the class is later
* loaded with `use overload`, both the retrieved object AND every
* subsequent {@code Class->new} for the same class will skip overload
* dispatch (URI's stringification, comparison, etc. silently break).
*
* Failure to load is silently ignored: many recorded objects bless into
* pure-data packages that have no .pm file, and that's fine — they just
* don't have overload anyway.
*/
private static void requireClassForBlessOnRetrieve(String className) {
if (className == null || className.isEmpty()) return;
if (className.equals("main") || className.equals("UNIVERSAL")) return;
String filename = className.replace("::", "/").replace("'", "/") + ".pm";
RuntimeHash inc = GlobalVariable.getGlobalHash("main::INC");
if (inc.exists(new RuntimeScalar(filename)).getBoolean()) return;
try {
ModuleOperators.require(new RuntimeScalar(filename));
} catch (Exception ignore) {
// Class isn't a loadable module — fine, no overload to register.
}
}

private static void releaseApplyArgs(RuntimeArray args) {
if (args == null || args.elements == null) return;
for (RuntimeScalar elem : args.elements) {
Expand Down Expand Up @@ -581,7 +639,7 @@ private static RuntimeScalar deepClone(RuntimeScalar scalar, IdentityHashMap<Obj
if (blessId != 0) {
String className = NameNormalizer.getBlessStr(blessId);
RuntimeScalar freezeMethod = InheritanceResolver.findMethodInHierarchy(
"STORABLE_freeze", className, null, 0);
"STORABLE_freeze", className, null, 0, false);

if (freezeMethod != null && freezeMethod.type == RuntimeScalarType.CODE) {
// Call STORABLE_freeze($self, $cloning=1)
Expand Down Expand Up @@ -619,7 +677,7 @@ private static RuntimeScalar deepClone(RuntimeScalar scalar, IdentityHashMap<Obj

// Call STORABLE_thaw($new_obj, $cloning=1, $serialized, @extra_refs)
RuntimeScalar thawMethod = InheritanceResolver.findMethodInHierarchy(
"STORABLE_thaw", className, null, 0);
"STORABLE_thaw", className, null, 0, false);
if (thawMethod != null && thawMethod.type == RuntimeScalarType.CODE) {
RuntimeArray thawArgs = new RuntimeArray();
RuntimeArray.push(thawArgs, newObj);
Expand Down Expand Up @@ -816,7 +874,7 @@ private static Object convertToYAMLWithTags(RuntimeScalar scalar, IdentityHashMa

// Check for STORABLE_freeze hook
RuntimeScalar freezeMethod = InheritanceResolver.findMethodInHierarchy(
"STORABLE_freeze", className, null, 0);
"STORABLE_freeze", className, null, 0, false);
if (freezeMethod != null && freezeMethod.type == RuntimeScalarType.CODE) {
// Call STORABLE_freeze($self, $cloning=0) for serialization
RuntimeArray freezeArgs = new RuntimeArray();
Expand All @@ -831,11 +889,23 @@ private static Object convertToYAMLWithTags(RuntimeScalar scalar, IdentityHashMa
// 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
// Store serialized data with class tag.
// The tag encodes the original reference type so the
// reader can recreate a reference of the right kind
// before calling STORABLE_thaw — required for hooks like
// URI's that expect a scalar ref ($$self = $str).
String tagPrefix;
if (scalar.type == RuntimeScalarType.ARRAYREFERENCE) {
tagPrefix = "!!perl/freezeA:";
} else if (scalar.type == RuntimeScalarType.REFERENCE) {
tagPrefix = "!!perl/freezeS:";
} else {
tagPrefix = "!!perl/freeze:"; // hash ref (also legacy)
}
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());
taggedObject.put(tagPrefix + className, freezeArray.get(0).toString());
return taggedObject;
}
// Empty return — fall through to default !!perl/hash: serialization
Expand Down Expand Up @@ -929,19 +999,33 @@ private static RuntimeScalar convertFromYAMLWithTags(Object yaml, IdentityHashMa
String className = key.substring("!!perl/hash:".length());
RuntimeScalar obj = convertFromYAMLWithTags(entry.getValue(), seen);
if (RuntimeScalarType.isReference(obj)) {
requireClassForBlessOnRetrieve(className);
ReferenceOperators.bless(obj, new RuntimeScalar(className));
}
yield obj;
} else if (key.startsWith("!!perl/freeze:")) {
// Handle STORABLE_freeze/thaw hooks
String className = key.substring("!!perl/freeze:".length());
RuntimeHash newHash = new RuntimeHash();
RuntimeScalar newObj = newHash.createAnonymousReference();
} else if (key.startsWith("!!perl/freeze:") || key.startsWith("!!perl/freezeS:") || key.startsWith("!!perl/freezeA:")) {
// Handle STORABLE_freeze/thaw hooks. Tag encodes the
// original reference type so we can build a value the
// hook's STORABLE_thaw expects (URI's hook does
// `$$self = $str`, so $self must be a scalar ref).
String className;
RuntimeScalar newObj;
if (key.startsWith("!!perl/freezeS:")) {
className = key.substring("!!perl/freezeS:".length());
newObj = new RuntimeScalar().createReference();
} else if (key.startsWith("!!perl/freezeA:")) {
className = key.substring("!!perl/freezeA:".length());
newObj = new RuntimeArray().createAnonymousReference();
} else {
className = key.substring("!!perl/freeze:".length());
newObj = new RuntimeHash().createAnonymousReference();
}
requireClassForBlessOnRetrieve(className);
ReferenceOperators.bless(newObj, new RuntimeScalar(className));

// Call STORABLE_thaw($new_obj, $cloning=0, $serialized_string)
RuntimeScalar thawMethod = InheritanceResolver.findMethodInHierarchy(
"STORABLE_thaw", className, null, 0);
"STORABLE_thaw", className, null, 0, false);
if (thawMethod != null && thawMethod.type == RuntimeScalarType.CODE) {
RuntimeArray thawArgs = new RuntimeArray();
RuntimeArray.push(thawArgs, newObj);
Expand Down
Loading