Skip to content

Commit 6b9fa2c

Browse files
Fix require expression parsing, non-blocking I/O, and 4-arg select
- Fix parser to handle require File::Spec->catfile(...) as an expression rather than treating File::Spec as a module name. This allows POE to dynamically load Time::HiRes via require. - Add non-blocking I/O support for internal pipe handles: InternalPipeHandle now supports setBlocking/isBlocking, and sysread returns undef with EAGAIN (errno 11) when non-blocking and no data available. - Fix IO::Handle::blocking() to properly delegate to the underlying handle blocking state, and fix argument passing (shift on glob copy issue). - Add FileDescriptorTable for managing file descriptors and implement 4-argument select() (pselect-style) for POE I/O multiplexing. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent e0be7d8 commit 6b9fa2c

8 files changed

Lines changed: 373 additions & 20 deletions

File tree

src/main/java/org/perlonjava/core/Configuration.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public final class Configuration {
3333
* Automatically populated by Gradle/Maven during build.
3434
* DO NOT EDIT MANUALLY - this value is replaced at build time.
3535
*/
36-
public static final String gitCommitId = "338bd4a90";
36+
public static final String gitCommitId = "e0be7d89b";
3737

3838
/**
3939
* Git commit date of the build (ISO format: YYYY-MM-DD).

src/main/java/org/perlonjava/frontend/parser/OperatorParser.java

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,19 +1120,37 @@ static OperatorNode parseRequire(Parser parser) {
11201120
// This avoids treating module names like "Encode" as subroutine calls when a sub
11211121
// with the same name exists in the current package (e.g., sub Encode in Image::ExifTool)
11221122
// But don't intercept quote-like operators like q(), qq(), etc.
1123+
//
1124+
// However, if the bareword is followed by `->`, it's a method call expression
1125+
// (e.g., `require File::Spec->catfile(...)`) and should be parsed as an expression.
1126+
int savedIndex = parser.tokenIndex;
11231127
String moduleName = IdentifierParser.parseSubroutineIdentifier(parser);
11241128
if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("require module name `" + moduleName + "`");
11251129
if (moduleName == null) {
11261130
throw new PerlCompilerException(parser.tokenIndex, "Syntax error", parser.ctx.errorUtil);
11271131
}
11281132

1129-
// Check if module name starts with ::
1130-
if (moduleName.startsWith("::")) {
1131-
throw new PerlCompilerException(parser.tokenIndex, "Bareword in require must not start with a double-colon: \"" + moduleName + "\"", parser.ctx.errorUtil);
1132-
}
1133+
// Check if followed by `->` — if so, this is a method call, not a module name
1134+
LexerToken nextToken = peek(parser);
1135+
if (nextToken.type == OPERATOR && nextToken.text.equals("->")) {
1136+
// Restore position and fall through to expression parsing
1137+
parser.tokenIndex = savedIndex;
1138+
ListNode op = ListParser.parseZeroOrOneList(parser, 0);
1139+
if (op.elements.isEmpty()) {
1140+
op.elements.add(scalarUnderscore(parser));
1141+
operand = op;
1142+
} else {
1143+
operand = op;
1144+
}
1145+
} else {
1146+
// Check if module name starts with ::
1147+
if (moduleName.startsWith("::")) {
1148+
throw new PerlCompilerException(parser.tokenIndex, "Bareword in require must not start with a double-colon: \"" + moduleName + "\"", parser.ctx.errorUtil);
1149+
}
11331150

1134-
String fileName = NameNormalizer.moduleToFilename(moduleName);
1135-
operand = ListNode.makeList(new StringNode(fileName, parser.tokenIndex));
1151+
String fileName = NameNormalizer.moduleToFilename(moduleName);
1152+
operand = ListNode.makeList(new StringNode(fileName, parser.tokenIndex));
1153+
}
11361154
} else {
11371155
// Check for the specific pattern: :: followed by identifier (which is invalid for require)
11381156
if (token.type == OPERATOR && token.text.equals("::")) {
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package org.perlonjava.runtime.io;
2+
3+
import java.util.concurrent.ConcurrentHashMap;
4+
import java.util.concurrent.atomic.AtomicInteger;
5+
6+
/**
7+
* Maps simulated file descriptor numbers to IOHandle objects.
8+
*
9+
* <p>Java doesn't expose real POSIX file descriptors. This table assigns
10+
* sequential integers starting from 3 (0, 1, 2 are reserved for
11+
* stdin, stdout, stderr) and allows lookup by FD number.
12+
*
13+
* <p>Used by:
14+
* <ul>
15+
* <li>{@code fileno()} — to return a consistent FD for each handle</li>
16+
* <li>4-arg {@code select()} — to map bit-vector bits back to handles</li>
17+
* </ul>
18+
*
19+
* <p>Thread-safe: uses ConcurrentHashMap and AtomicInteger.
20+
*/
21+
public class FileDescriptorTable {
22+
23+
// Next FD number to assign. 0–2 are stdin/stdout/stderr.
24+
private static final AtomicInteger nextFd = new AtomicInteger(3);
25+
26+
// FD number → IOHandle (for select() lookup)
27+
private static final ConcurrentHashMap<Integer, IOHandle> fdToHandle = new ConcurrentHashMap<>();
28+
29+
// IOHandle identity → FD number (to avoid assigning multiple FDs to the same handle)
30+
private static final ConcurrentHashMap<Integer, Integer> handleToFd = new ConcurrentHashMap<>();
31+
32+
/**
33+
* Register an IOHandle and return its FD number.
34+
* If the handle was already registered, returns the existing FD.
35+
*
36+
* @param handle the IOHandle to register
37+
* @return the file descriptor number
38+
*/
39+
public static int register(IOHandle handle) {
40+
int identity = System.identityHashCode(handle);
41+
Integer existing = handleToFd.get(identity);
42+
if (existing != null) {
43+
return existing;
44+
}
45+
int fd = nextFd.getAndIncrement();
46+
fdToHandle.put(fd, handle);
47+
handleToFd.put(identity, fd);
48+
return fd;
49+
}
50+
51+
/**
52+
* Look up an IOHandle by its FD number.
53+
*
54+
* @param fd the file descriptor number
55+
* @return the IOHandle, or null if not found
56+
*/
57+
public static IOHandle getHandle(int fd) {
58+
return fdToHandle.get(fd);
59+
}
60+
61+
/**
62+
* Remove a handle from the table (e.g., on close).
63+
*
64+
* @param fd the file descriptor number to remove
65+
*/
66+
public static void unregister(int fd) {
67+
IOHandle handle = fdToHandle.remove(fd);
68+
if (handle != null) {
69+
handleToFd.remove(System.identityHashCode(handle));
70+
}
71+
}
72+
73+
/**
74+
* Check if a read-end handle has data available without blocking.
75+
* Returns true if the handle is "ready for reading".
76+
*
77+
* @param handle the IOHandle to check
78+
* @return true if data is available or handle is at EOF/closed
79+
*/
80+
public static boolean isReadReady(IOHandle handle) {
81+
if (handle instanceof InternalPipeHandle pipeHandle) {
82+
return pipeHandle.hasDataAvailable();
83+
}
84+
if (handle instanceof StandardIO) {
85+
// stdin: check System.in.available()
86+
try {
87+
return System.in.available() > 0;
88+
} catch (Exception e) {
89+
return false;
90+
}
91+
}
92+
// For unknown handle types, report as ready to avoid blocking
93+
return true;
94+
}
95+
96+
/**
97+
* Check if a write-end handle can accept writes without blocking.
98+
*
99+
* @param handle the IOHandle to check
100+
* @return true if the handle can accept writes
101+
*/
102+
public static boolean isWriteReady(IOHandle handle) {
103+
// Pipes and most handles can always accept writes (they buffer internally)
104+
return true;
105+
}
106+
}

src/main/java/org/perlonjava/runtime/io/IOHandle.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,26 @@ default RuntimeScalar flock(int operation) {
243243
return RuntimeIO.handleIOError("flock operation is not supported on this handle type.");
244244
}
245245

246+
/**
247+
* Get the current blocking mode for this handle.
248+
*
249+
* @return true if the handle is in blocking mode (default), false if non-blocking
250+
*/
251+
default boolean isBlocking() {
252+
return true;
253+
}
254+
255+
/**
256+
* Set the blocking mode for this handle.
257+
*
258+
* @param blocking true for blocking mode, false for non-blocking
259+
* @return true if the mode was successfully set
260+
*/
261+
default boolean setBlocking(boolean blocking) {
262+
// Default: only blocking mode is supported
263+
return blocking;
264+
}
265+
246266
// System-level I/O operations
247267
default RuntimeScalar sysread(int length) {
248268
return RuntimeIO.handleIOError("sysread operation is not supported.");

src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ public class InternalPipeHandle implements IOHandle {
2525
private final boolean isReader;
2626
private boolean isClosed = false;
2727
private boolean isEOF = false;
28+
private final int fd; // Simulated file descriptor number
29+
private boolean blocking = true; // Default: blocking mode
2830

2931
private InternalPipeHandle(PipedInputStream inputStream, PipedOutputStream outputStream, boolean isReader) {
3032
this.inputStream = inputStream;
3133
this.outputStream = outputStream;
3234
this.isReader = isReader;
35+
this.fd = FileDescriptorTable.register(this);
3336
}
3437

3538
/**
@@ -130,6 +133,7 @@ public RuntimeScalar close() {
130133
}
131134
isClosed = true;
132135
isEOF = true;
136+
FileDescriptorTable.unregister(fd);
133137
return scalarTrue;
134138
} catch (IOException e) {
135139
return handleIOException(e, "Close pipe failed");
@@ -173,7 +177,27 @@ public RuntimeScalar flush() {
173177

174178
@Override
175179
public RuntimeScalar fileno() {
176-
return RuntimeScalarCache.scalarUndef; // Internal pipes don't have file descriptors
180+
return new RuntimeScalar(fd);
181+
}
182+
183+
/**
184+
* Check if this pipe handle has data available for reading without blocking.
185+
* Used by the 4-arg select() implementation.
186+
*
187+
* @return true if data is available, the pipe is at EOF, or this is a write-end pipe
188+
*/
189+
public boolean hasDataAvailable() {
190+
if (!isReader) {
191+
return false; // Write end is not "read-ready"
192+
}
193+
if (isClosed || isEOF) {
194+
return true; // EOF/closed counts as "ready" (read returns 0/empty)
195+
}
196+
try {
197+
return inputStream.available() > 0;
198+
} catch (IOException e) {
199+
return true; // Error counts as ready (will be detected on actual read)
200+
}
177201
}
178202

179203
@Override
@@ -204,6 +228,17 @@ public RuntimeScalar syswrite(String data) {
204228
}
205229
}
206230

231+
@Override
232+
public boolean isBlocking() {
233+
return blocking;
234+
}
235+
236+
@Override
237+
public boolean setBlocking(boolean blocking) {
238+
this.blocking = blocking;
239+
return true;
240+
}
241+
207242
@Override
208243
public RuntimeScalar sysread(int length) {
209244
if (!isReader) {
@@ -216,7 +251,31 @@ public RuntimeScalar sysread(int length) {
216251
}
217252

218253
try {
219-
// Always use polling for pipe reads to allow signal interruption
254+
// Non-blocking mode: return immediately if no data available
255+
if (!blocking) {
256+
int available = inputStream.available();
257+
if (available <= 0) {
258+
// Set $! to EAGAIN (Resource temporarily unavailable)
259+
getGlobalVariable("main::!").set(new RuntimeScalar(11)); // EAGAIN = 11 on most systems
260+
return new RuntimeScalar(); // undef
261+
}
262+
// Data available - read it
263+
byte[] buffer = new byte[Math.min(length, available)];
264+
int bytesRead = inputStream.read(buffer);
265+
266+
if (bytesRead == -1) {
267+
isEOF = true;
268+
return new RuntimeScalar("");
269+
}
270+
271+
StringBuilder result = new StringBuilder(bytesRead);
272+
for (int i = 0; i < bytesRead; i++) {
273+
result.append((char) (buffer[i] & 0xFF));
274+
}
275+
return new RuntimeScalar(result.toString());
276+
}
277+
278+
// Blocking mode: poll with sleep for signal interruption
220279
while (true) {
221280
if (Thread.interrupted()) {
222281
PerlSignalQueue.checkPendingSignals();

0 commit comments

Comments
 (0)