diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index e58842ed8..9d2de9905 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 = "dfa87a6c4"; + public static final String gitCommitId = "846d4d854"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -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() { diff --git a/src/main/java/org/perlonjava/runtime/io/SocketIO.java b/src/main/java/org/perlonjava/runtime/io/SocketIO.java index 8280755df..82c6c2198 100644 --- a/src/main/java/org/perlonjava/runtime/io/SocketIO.java +++ b/src/main/java/org/perlonjava/runtime/io/SocketIO.java @@ -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) { diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index 4bae7091f..7c50855fd 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -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 + "'"); @@ -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 + "'"); @@ -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); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java b/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java index 8bf7a94e9..e21e05c0d 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Storable.java @@ -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.*; @@ -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) @@ -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()); @@ -321,6 +338,11 @@ private static RuntimeScalar deserializeBinary(String data, int[] pos, Listnew} 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) { @@ -581,7 +639,7 @@ private static RuntimeScalar deepClone(RuntimeScalar scalar, IdentityHashMap 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 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 @@ -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);