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
7 changes: 7 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ priorities and future plans.

## Work in progress

- Preserve blessed IO classes during `can()` checks so socket-specific methods remain discoverable.
- Report native socketpair readiness from the descriptor instead of treating every event as ready.
- Apply IO::Handle blocking mode changes to native descriptors so nonblocking socket writes can return EAGAIN.
- Retain queued IO::Async call futures until dispatch completes, preserving queued worker results.

- Autovivify undefined hash references when an element is read, including in
`defined` checks.
- Support `sprintf` field widths up to 1,000,000 characters, including widths
Expand Down Expand Up @@ -76,6 +81,8 @@ priorities and future plans.
representation for case-folded negated singleton regex classes.
- Remove deleted package stashes from their parent namespace in
`Symbol::delete_package`.
- Forward only parent-declared named parameters to generated parent class
constructors, preserving child fields and validation across inheritance.
- Preserve direct-call semantics for lexical and package `->&` methods across
both backends, support `CORE::bless` and `CORE::break` code references, and
allow same-finally local `goto` targets.
Expand Down
39 changes: 36 additions & 3 deletions src/main/java/org/perlonjava/frontend/parser/ClassTransformer.java
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,12 @@ public static BlockNode transformClassBlock(BlockNode block, String className, P

// Generate constructor if not present
if (existingConstructor == null) {
FieldRegistry.registerGeneratedConstructor(className);
SubroutineNode constructor = generateConstructor(fields, className, adjustNodes);
block.elements.add(constructor);
block.setAnnotation("deferredConstructor", constructor);
} else {
FieldRegistry.unregisterGeneratedConstructor(className);
}

// Generate reader and writer methods
Expand Down Expand Up @@ -340,14 +343,44 @@ private static SubroutineNode generateConstructor(List<OperatorNode> fields, Str

Node selfValue;
if (parentClass != null) {
// Call SUPER::new() to get the blessed object with parent fields initialized
// my $self = $class->SUPER::new(%args);
// A generated parent constructor validates only parameters declared
// on that parent and its ancestors. Pass it that subset so a child
// field parameter is not rejected before this constructor initializes
// it. Preserve all arguments for a user-defined parent constructor.
String parentArgumentVariable = "args";
if (FieldRegistry.hasGeneratedConstructor(parentClass)) {
parentArgumentVariable = "parentArgs";
OperatorNode parentArgs = new OperatorNode("my",
new OperatorNode("%", new IdentifierNode(parentArgumentVariable, 0), 0), 0);
((OperatorNode) parentArgs.operand).setAnnotation("reuseBytecodeLexicalRegister", Boolean.TRUE);
body.elements.add(parentArgs);
for (String parameterName : FieldRegistry.getParameterNamesInHierarchy(parentClass)) {
OperatorNode argsForExists = new OperatorNode("$", new IdentifierNode("args", 0), 0);
HashLiteralNode existsKey = new HashLiteralNode(
List.of(new StringNode(parameterName, 0)), 0);
OperatorNode exists = new OperatorNode("exists",
new ListNode(List.of(new BinaryOperatorNode("{", argsForExists, existsKey, 0)), 0), 0);
OperatorNode parentArgsForWrite = new OperatorNode("$",
new IdentifierNode(parentArgumentVariable, 0), 0);
HashLiteralNode writeKey = new HashLiteralNode(
List.of(new StringNode(parameterName, 0)), 0);
BinaryOperatorNode write = new BinaryOperatorNode("=",
new BinaryOperatorNode("{", parentArgsForWrite, writeKey, 0),
new BinaryOperatorNode("{", new OperatorNode("$", new IdentifierNode("args", 0), 0),
new HashLiteralNode(List.of(new StringNode(parameterName, 0)), 0), 0), 0);
body.elements.add(new IfNode("if", exists,
new BlockNode(new ArrayList<>(List.of(write)), 0), null, 0));
}
}

// Call SUPER::new() to get the blessed object with parent fields initialized.
// The child's own %args remains intact for its field initialization.
OperatorNode classVar = new OperatorNode("$", new IdentifierNode("class", 0), 0);

// Create SUPER::new as a method call
// First create the method name with arguments
ListNode methodArgs = new ListNode(0);
methodArgs.elements.add(new OperatorNode("%", new IdentifierNode("args", 0), 0));
methodArgs.elements.add(new OperatorNode("%", new IdentifierNode(parentArgumentVariable, 0), 0));

// Create SUPER::new(args) as a subroutine call
OperatorNode superNewCall = new OperatorNode("&",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,7 @@ public static Node parseFieldDeclaration(Parser parser) {
List<OperatorNode> fields = parser.unitClassFields.computeIfAbsent(
currentClass, ignored -> new java.util.ArrayList<>());
fields.add(fieldPlaceholder);
FieldRegistry.registerGeneratedConstructor(currentClass);
SubroutineNode constructor = ClassTransformer.generateUnitClassConstructor(fields, currentClass);
SubroutineParser.handleNamedSubWithFilter(parser, constructor.name, constructor.prototype,
constructor.attributes, (org.perlonjava.frontend.astnode.BlockNode) constructor.block,
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/org/perlonjava/frontend/parser/FieldRegistry.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ private static Map<String, Set<String>> classParameters() {
return PerlRuntime.current().globalState().classParameters();
}

private static Set<String> generatedConstructors() {
return PerlRuntime.current().globalState().generatedClassConstructors();
}

/**
* Register a field declaration in a class
*/
Expand Down Expand Up @@ -70,6 +74,18 @@ public static void registerParameterName(String className, String parameterName)
classParameters().computeIfAbsent(className, ignored -> new HashSet<>()).add(parameterName);
}

public static void registerGeneratedConstructor(String className) {
generatedConstructors().add(className);
}

public static void unregisterGeneratedConstructor(String className) {
generatedConstructors().remove(className);
}

public static boolean hasGeneratedConstructor(String className) {
return generatedConstructors().contains(className);
}

/**
* Check if a field exists in the class hierarchy
* This works if parent classes were parsed before child classes
Expand Down Expand Up @@ -125,5 +141,6 @@ public static void clear() {
classParents().clear();
classFields().clear();
classParameters().clear();
generatedConstructors().clear();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,7 @@ static Node parseSpecialBlock(Parser parser) {
adjustBlocks.add(adjustSub);
List<OperatorNode> fields = parser.unitClassFields.computeIfAbsent(
className, ignored -> new ArrayList<>());
FieldRegistry.registerGeneratedConstructor(className);
SubroutineNode constructor = ClassTransformer.generateUnitClassConstructor(
fields, className, adjustBlocks);
SubroutineParser.handleNamedSubWithFilter(parser, constructor.name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1871,6 +1871,13 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S
// CODE-only assignment can split the two CV slots while the other
// typeglob slots remain aliased.
fullName = GlobalVariable.resolveCodeDefinitionGlobAlias(fullName);
if ("new".equals(subName)
&& (block == null || !block.getBooleanAnnotation("generatedClassConstructor"))) {
int packageSeparator = fullName.lastIndexOf("::new");
if (packageSeparator >= 0) {
FieldRegistry.unregisterGeneratedConstructor(fullName.substring(0, packageSeparator));
}
}
RuntimeScalar codeRef = GlobalVariable.defineGlobalCodeRef(fullName);
InheritanceResolver.invalidateCache();

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.perlonjava.runtime.io;

import org.perlonjava.runtime.nativ.ffm.FFMPosix;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

Expand Down Expand Up @@ -168,6 +169,9 @@ public static boolean isReadReady(IOHandle handle) {
if (handle instanceof StandardIO standardIO) {
return standardIO.isReadReady();
}
if (handle instanceof NativeFdIOHandle nativeFd) {
return FFMPosix.get().pollReadReady(nativeFd.getNativeFd());
}
// For unknown handle types, report as ready to avoid blocking
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.perlonjava.runtime.nativ.ffm.FFMPosix;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache;
import java.nio.charset.StandardCharsets;

/** A real POSIX socket descriptor exposed through the generic I/O layer. */
public final class NativeSocketIOHandle extends NativeFdIOHandle {
Expand All @@ -15,6 +16,13 @@ public NativeSocketIOHandle(int nativeFd, int socketType) {

public int socketType() { return socketType; }

/** Return the packed AF_UNIX address for an unnamed POSIX socketpair peer. */
public RuntimeScalar getpeername() {
// POSIX socketpair() creates connected, unnamed AF_UNIX sockets. The
// sockaddr family is sufficient here; there is no pathname to append.
return new RuntimeScalar(new String(new byte[] { 0, 1 }, StandardCharsets.ISO_8859_1));
}

@Override
public RuntimeScalar shutdown(int how) {
return FFMPosix.get().shutdown(getNativeFd(), how) == 0
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package org.perlonjava.runtime.perlmodule;

import org.perlonjava.runtime.runtimetypes.*;
import org.perlonjava.runtime.io.NativeFdIOHandle;
import org.perlonjava.runtime.nativ.ffm.FFMPosix;

/**
* Java::System - Perl module for accessing IO::Handle internals
Expand Down Expand Up @@ -178,6 +180,14 @@ public static RuntimeList _blocking(RuntimeArray args, int ctx) {
currentBlocking = socketIO.isBlocking();
} else if (ioHandle instanceof org.perlonjava.runtime.io.InternalPipeHandle pipeHandle) {
currentBlocking = pipeHandle.isBlocking();
} else if (ioHandle instanceof NativeFdIOHandle nativeFd) {
int flags = FFMPosix.get().fcntl(nativeFd.getNativeFd(), 3, 0); // F_GETFL
if (flags == -1) {
RuntimeIO.handleIOError(FFMPosix.get().errno());
return new RuntimeList();
}
int nonblockFlag = org.perlonjava.runtime.nativ.NativeUtils.IS_MAC ? 4 : 04000;
currentBlocking = (flags & nonblockFlag) == 0;
}

if (args.size() == 2) {
Expand All @@ -188,6 +198,18 @@ public static RuntimeList _blocking(RuntimeArray args, int ctx) {
} else if (ioHandle instanceof org.perlonjava.runtime.io.InternalPipeHandle pipeHandle) {
// For internal pipes, set blocking mode
pipeHandle.setBlocking(newBlocking);
} else if (ioHandle instanceof NativeFdIOHandle nativeFd) {
int flags = FFMPosix.get().fcntl(nativeFd.getNativeFd(), 3, 0); // F_GETFL
if (flags == -1) {
RuntimeIO.handleIOError(FFMPosix.get().errno());
return new RuntimeList();
}
int nonblockFlag = org.perlonjava.runtime.nativ.NativeUtils.IS_MAC ? 4 : 04000;
int updatedFlags = newBlocking ? flags & ~nonblockFlag : flags | nonblockFlag;
if (FFMPosix.get().fcntl(nativeFd.getNativeFd(), 4, updatedFlags) == -1) { // F_SETFL
RuntimeIO.handleIOError(FFMPosix.get().errno());
return new RuntimeList();
}
} else if (!newBlocking) {
// Non-blocking I/O not supported for other handle types
RuntimeIO.handleIOError("Non-blocking I/O not supported");
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/org/perlonjava/runtime/perlmodule/Universal.java
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,16 @@ public static RuntimeList can(RuntimeArray args, int ctx) {
// A bare glob and a handle bareword are both valid IO invocants in
// Perl. Resolve an actual IO slot before the generic scalar path,
// which would otherwise treat *STDOUT or "STDOUT" as package names.
if (RuntimeIO.getRuntimeIO(object) != null) {
RuntimeIO runtimeIO = RuntimeIO.getRuntimeIO(object);
if (runtimeIO != null && object.value instanceof RuntimeBase runtimeBase
&& runtimeBase.blessId != 0) {
// Blessed socket/filehandle objects retain their Perl class even
// though their storage is backed by RuntimeIO. Resolve their
// methods from that class first (for example IO::Socket::IP's
// peerhost/peerport).
perlClassName = NameNormalizer.getBlessStr(runtimeBase.blessId);
} else if (runtimeIO != null) {
// Unblessed handles use the IO::Handle interface.
perlClassName = "IO::Handle";
} else {
switch (object.type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ public final class GlobalRuntimeState {
private final Set<String> classNames = new HashSet<>();
private final Map<String, Set<String>> classFields = new HashMap<>();
private final Map<String, Set<String>> classParameters = new HashMap<>();
private final Set<String> generatedClassConstructors = new HashSet<>();
private final Map<String, String> classParents = new HashMap<>();
private final Map<String, String> packageVersions = new HashMap<>();
private CustomClassLoader generatedClassLoader =
Expand Down Expand Up @@ -223,6 +224,11 @@ public Map<String, String> classParents() {
return classParents;
}

/** Classes whose {@code new} method is synthesized from their field declarations. */
public Set<String> generatedClassConstructors() {
return generatedClassConstructors;
}

/** Package versions visible to later compilation units in this runtime. */
public Map<String, String> packageVersions() {
return packageVersions;
Expand Down Expand Up @@ -405,7 +411,9 @@ void clearDeclarationsAndPackageServices() {
declaredGlobalHashes.clear();
classNames.clear();
classFields.clear();
classParameters.clear();
classParents.clear();
generatedClassConstructors.clear();
packageVersions.clear();
generatedClassLoader = new CustomClassLoader(GlobalVariable.class.getClassLoader());
}
Expand Down Expand Up @@ -456,7 +464,10 @@ synchronized void snapshotInto(GlobalRuntimeState target, RuntimeGraphCloner clo
target.declaredGlobalHashes.addAll(declaredGlobalHashes);
target.classNames.addAll(classNames);
classFields.forEach((name, fields) -> target.classFields.put(name, new HashSet<>(fields)));
classParameters.forEach((name, parameters) ->
target.classParameters.put(name, new HashSet<>(parameters)));
target.classParents.putAll(classParents);
target.generatedClassConstructors.addAll(generatedClassConstructors);
target.packageVersions.putAll(packageVersions);
// Lazy named CVs may compile in the parent after this snapshot while
// the child also compiles new code. Give every snapshot runtime its
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2152,6 +2152,13 @@ public RuntimeScalar getpeername() {
if (socket != null) {
return socket.getpeername();
}
IOHandle handle = ioHandle;
while (handle instanceof LayeredIOHandle layered) {
handle = layered.getDelegate();
}
if (handle instanceof org.perlonjava.runtime.io.NativeSocketIOHandle nativeSocket) {
return nativeSocket.getpeername();
}
return scalarUndef;
}

Expand Down
2 changes: 2 additions & 0 deletions src/main/perl/lib/CPAN/Config.pm
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,8 @@ sub _bootstrap_patches {
'PerlOnJava/CpanPatches/Pod-Parser-1.67/Pod-Find-core-probe.patch' ],
[ 'IO-Async/NoFork.patch',
'PerlOnJava/CpanPatches/IO-Async-0.805/NoFork.patch' ],
[ 'IO-Async/RetainQueuedFutures.patch',
'PerlOnJava/CpanPatches/IO-Async-0.805/RetainQueuedFutures.patch' ],
[ 'IO-Async/PerlOnJava.patch',
'PerlOnJava/CpanPatches/IO-Async-0.805/PerlOnJava.patch' ],
[ 'IO-Async/SkipUnsupportedSocketTests.patch',
Expand Down
3 changes: 2 additions & 1 deletion src/main/perl/lib/IO/Socket.pm
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,8 @@ sub blocking {
my $sock = shift;

return $sock->SUPER::blocking(@_)
if $^O ne 'MSWin32' && $^O ne 'VMS';
if ($^O ne 'MSWin32' && $^O ne 'VMS')
|| defined &Internals::jperl_refstate_str;

# Windows handles blocking differently
#
Expand Down
1 change: 1 addition & 0 deletions src/main/perl/lib/PerlOnJava/CpanDistroprefs/IO-Async.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ match:
distribution: "^PEVANS/IO-Async-[0-9]"
patches:
- "IO-Async/NoFork.patch"
- "IO-Async/RetainQueuedFutures.patch"
- "IO-Async/PerlOnJava.patch"
- "IO-Async/SkipUnsupportedSocketTests.patch"
test:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
--- lib/IO/Async/Function.pm.orig
+++ lib/IO/Async/Function.pm
@@ -519,0 +520,2 @@
+ # The queued call must survive until it is dispatched and completes.
+ $future->retain;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use strict;
use warnings;
use feature 'class';
no warnings 'experimental::class';
use Test::More;

class InheritedGeneratedConstructorBase {
field $base :param;
method base_value { $base }
}

class InheritedGeneratedConstructorChild :isa(InheritedGeneratedConstructorBase) {
field $data :param;
method data_value { $data }
}

my $child = InheritedGeneratedConstructorChild->new(base => 10, data => 20);
isa_ok($child, 'InheritedGeneratedConstructorChild',
'child generated constructor accepts its own parameter through the parent constructor');
is($child->base_value, 10, 'parent generated constructor initializes its parameter');
is($child->data_value, 20, 'child generated constructor initializes its parameter');

my $unknown = eval { InheritedGeneratedConstructorChild->new(base => 10, data => 20, extra => 30); 1 };
ok(!$unknown, 'the child hierarchy still rejects undeclared constructor parameters');
like $@,
qr/^Unrecogni[sz]ed parameters for "InheritedGeneratedConstructorChild" constructor: extra at /,
'unknown-parameter diagnostic names the child constructor';

done_testing;
16 changes: 16 additions & 0 deletions src/test/resources/unit/io_poll_socketpair_readiness.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use v5.10;
use strict;
use warnings;
use Test::More;
use IO::Poll qw(POLLIN);
use IO::Socket::UNIX;
use Socket qw(PF_UNIX SOCK_STREAM);

my ($left, $right) = IO::Socket::UNIX->socketpair(PF_UNIX, SOCK_STREAM, 0);
plan skip_all => "UNIX socketpair is unavailable: $!" unless $left && $right;

my $poll = IO::Poll->new;
$poll->mask($left, POLLIN);
is($poll->poll(0), 0, 'an idle socketpair endpoint is not readable');

done_testing;
Loading
Loading