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 = "7385e7908";
public static final String gitCommitId = "8e15479f4";

/**
* 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 13 2026 16:27:44";
public static final String buildTimestamp = "Apr 13 2026 22:14:46";

// Prevent instantiation
private Configuration() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -253,13 +253,21 @@ public static ParsedString parseRawStringWithDelimiter(EmitterContext ctx, List<
// regex close delimiter, the remainder ("=") may need to be merged with the next
// token to reconstruct a binding operator. Example: qr/x/=~ was tokenized as
// qr, /, x, /=, ~ — after consuming "/" from "/=", remainder "=" + next "~" = "=~"
// Similarly, qr/x/=>'val' needs "=" + ">" merged into "=>" (fat comma).
if ((remainStr.equals("=") || remainStr.equals("!"))
&& tokPos + 1 < tokens.size()
&& tokens.get(tokPos + 1).text.equals("~")) {
remainStr = remainStr + "~";
// Neutralize the consumed ~ token so it won't be parsed again
tokens.get(tokPos + 1).text = " ";
tokens.get(tokPos + 1).type = LexerTokenType.WHITESPACE;
} else if (remainStr.equals("=")
&& tokPos + 1 < tokens.size()
&& tokens.get(tokPos + 1).text.equals(">")) {
remainStr = "=>";
// Neutralize the consumed > token so it won't be parsed again
tokens.get(tokPos + 1).text = " ";
tokens.get(tokPos + 1).type = LexerTokenType.WHITESPACE;
}
tokens.get(tokPos).text = remainStr; // Put the remaining string back in the tokens list
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -297,8 +297,9 @@ public static RuntimeArray gethostbyname(int ctx, RuntimeBase... args) {
InetAddress addr = InetAddress.getByName(hostname);

RuntimeArray.push(result, new RuntimeScalar(addr.getHostName()));
RuntimeArray aliases = new RuntimeArray();
RuntimeArray.push(result, aliases);
// Aliases field: must be a scalar (empty string), not an empty array
// which would flatten to zero elements and shift subsequent fields
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(2));
RuntimeArray.push(result, new RuntimeScalar(4));

Expand Down Expand Up @@ -343,7 +344,7 @@ public static RuntimeArray gethostbyaddr(int ctx, RuntimeBase... args) {

RuntimeArray result = new RuntimeArray();
RuntimeArray.push(result, new RuntimeScalar(inetAddr.getHostName()));
RuntimeArray.push(result, new RuntimeArray());
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(2));
RuntimeArray.push(result, new RuntimeScalar(4));

Expand Down Expand Up @@ -375,7 +376,7 @@ public static RuntimeArray getservbyname(int ctx, RuntimeBase... args) {
Integer port = commonPorts.get(service.toLowerCase());
if (port != null) {
RuntimeArray.push(result, new RuntimeScalar(service));
RuntimeArray.push(result, new RuntimeArray());
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(port));
RuntimeArray.push(result, new RuntimeScalar(protocol));
}
Expand All @@ -399,7 +400,7 @@ public static RuntimeArray getservbyport(int ctx, RuntimeBase... args) {
String service = commonServices.get(port);
if (service != null) {
RuntimeArray.push(result, new RuntimeScalar(service));
RuntimeArray.push(result, new RuntimeArray());
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(port));
RuntimeArray.push(result, new RuntimeScalar(protocol));
}
Expand Down Expand Up @@ -430,7 +431,7 @@ public static RuntimeBase getprotobyname(int ctx, RuntimeBase... args) {
// List context: return (name, aliases, proto_number)
RuntimeArray result = new RuntimeArray();
RuntimeArray.push(result, new RuntimeScalar(protocol));
RuntimeArray.push(result, new RuntimeArray());
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(protoNum));
return result;
}
Expand All @@ -447,7 +448,7 @@ public static RuntimeArray getprotobynumber(int ctx, RuntimeBase... args) {
String protocol = protocols.get(protoNum);
if (protocol != null) {
RuntimeArray.push(result, new RuntimeScalar(protocol));
RuntimeArray.push(result, new RuntimeArray());
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(protoNum));
}

Expand Down Expand Up @@ -841,7 +842,7 @@ public static RuntimeArray getnetbyaddr(int ctx, RuntimeBase... args) {

RuntimeArray result = new RuntimeArray();
RuntimeArray.push(result, new RuntimeScalar("loopback"));
RuntimeArray.push(result, new RuntimeArray());
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(addrtype));
RuntimeArray.push(result, new RuntimeScalar("127.0.0.1"));

Expand All @@ -856,7 +857,7 @@ public static RuntimeArray getnetbyname(int ctx, RuntimeBase... args) {
RuntimeArray result = new RuntimeArray();
if (name.equals("loopback") || name.equals("localhost")) {
RuntimeArray.push(result, new RuntimeScalar(name));
RuntimeArray.push(result, new RuntimeArray());
RuntimeArray.push(result, new RuntimeScalar(""));
RuntimeArray.push(result, new RuntimeScalar(2));
RuntimeArray.push(result, new RuntimeScalar("127.0.0.1"));
}
Expand Down
74 changes: 52 additions & 22 deletions src/main/java/org/perlonjava/runtime/operators/IOOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,11 @@ public class IOOperator {

public static RuntimeScalar select(RuntimeList runtimeList, int ctx) {
if (runtimeList.isEmpty()) {
// select (returns current filehandle)
// select() with no args returns the currently selected filehandle.
// In Perl 5 this returns a string name like "main::STDOUT".
// We return the RuntimeIO wrapped as a GLOB scalar, which stringifies
// to the glob name. This preserves the round-trip: select(select())
// correctly restores the previous handle for tied handles too.
return new RuntimeScalar(RuntimeIO.selectedHandle);
}
if (runtimeList.size() == 4) {
Expand Down Expand Up @@ -533,7 +537,41 @@ public static RuntimeScalar open(int ctx, RuntimeBase... args) {
// We assert it's a RuntimeScalar rather than calling .scalar() which would create a copy
RuntimeScalar fileHandle = (RuntimeScalar) args[0];
if (args.length < 2) {
throw new PerlJavaUnimplementedException("1 argument open is not implemented");
// 1-argument open: open FILEHANDLE
// Uses $_ as the filename (with embedded mode prefix parsed from it)
String fileName = getGlobalVariable("main::_").toString();
RuntimeIO oneFh = RuntimeIO.open(fileName);
if (oneFh == null) {
return scalarUndef;
}
// Assign the IO handle to the filehandle glob (reuse the existing assignment logic below)
RuntimeGlob targetGlob = null;
if ((fileHandle.type == RuntimeScalarType.GLOB || fileHandle.type == RuntimeScalarType.GLOBREFERENCE) && fileHandle.value instanceof RuntimeGlob glob) {
targetGlob = glob;
} else if ((fileHandle.type == RuntimeScalarType.STRING || fileHandle.type == RuntimeScalarType.BYTE_STRING) && fileHandle.value instanceof String name) {
if (!name.isEmpty() && name.matches("^[A-Za-z_][A-Za-z0-9_]*(::[A-Za-z_][A-Za-z0-9_]*)*$")) {
String fullName = name.contains("::") ? name : ("main::" + name);
targetGlob = GlobalVariable.getGlobalIO(fullName);
RuntimeScalar newGlob = new RuntimeScalar();
newGlob.type = RuntimeScalarType.GLOBREFERENCE;
newGlob.value = targetGlob;
fileHandle.set(newGlob);
}
}
if (targetGlob != null) {
targetGlob.setIO(oneFh);
} else {
RuntimeScalar newGlob = new RuntimeScalar();
newGlob.type = RuntimeScalarType.GLOBREFERENCE;
RuntimeGlob anonGlob = new RuntimeGlob(null).setIO(oneFh);
newGlob.value = anonGlob;
RuntimeIO.registerGlobForFdRecycling(anonGlob, oneFh);
fileHandle.set(newGlob);
fileHandle.ioOwner = true;
}
long pid = oneFh.getPid();
if (pid > 0) return new RuntimeScalar(pid);
return scalarTrue;
}
String mode = args[1].toString();
RuntimeList runtimeList = new RuntimeList(Arrays.copyOfRange(args, 1, args.length));
Expand Down Expand Up @@ -1152,16 +1190,9 @@ public static RuntimeScalar syswrite(int ctx, RuntimeBase... args) {
RuntimeScalar fileHandle = args[0].scalar();
RuntimeIO fh = fileHandle.getRuntimeIO();

// Check if fh is null (invalid filehandle)
if (fh == null || fh.ioHandle == null || fh.ioHandle instanceof ClosedIOHandle) {
getGlobalVariable("main::!").set("Bad file descriptor");
WarnDie.warn(
new RuntimeScalar("syswrite() on closed filehandle"),
new RuntimeScalar("\n")
);
return new RuntimeScalar(); // undef
}

// Check TieHandle FIRST (before closed handle check), matching sysread/print pattern.
// TieHandle extends RuntimeIO which initializes ioHandle as ClosedIOHandle,
// so the closed-handle check would incorrectly catch tied handles.
if (fh instanceof TieHandle tieHandle) {
RuntimeScalar data = args[1].scalar();
int dataLen = data.toString().length();
Expand All @@ -1173,16 +1204,15 @@ public static RuntimeScalar syswrite(int ctx, RuntimeBase... args) {
}
}

// // Check for closed handle - but based on the debug output,
// // closed handles still have their original ioHandle, not ClosedIOHandle
// if (fh.ioHandle == null) {
// getGlobalVariable("main::!").set("Bad file descriptor");
// WarnDie.warn(
// new RuntimeScalar("syswrite() on closed filehandle"),
// new RuntimeScalar("\n")
// );
// return new RuntimeScalar(); // undef
// }
// Check if fh is null or closed (after TieHandle check)
if (fh == null || fh.ioHandle == null || fh.ioHandle instanceof ClosedIOHandle) {
getGlobalVariable("main::!").set("Bad file descriptor");
WarnDie.warn(
new RuntimeScalar("syswrite() on closed filehandle"),
new RuntimeScalar("\n")
);
return new RuntimeScalar(); // undef
}

// Check for :utf8 layer
if (hasUtf8Layer(fh)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -715,7 +715,9 @@ private static String handleMissingArgument(FormatSpecifier spec,
int digits = args.precision > 0 ? args.precision : 1;
String zeros = "0".repeat(digits);
if (args.width > zeros.length()) {
zeros = " ".repeat(args.width - zeros.length()) + zeros;
// Respect the '0' flag for zero-padding (e.g., %03d should produce "000", not " 0")
String padChar = spec.flags.contains("0") ? "0" : " ";
zeros = padChar.repeat(args.width - zeros.length()) + zeros;
}
yield zeros;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ public static RuntimeScalar tie(int ctx, RuntimeBase... scalars) {
RuntimeIO previousValue = (RuntimeIO) glob.IO.value;
glob.IO.type = TIED_SCALAR;
TieHandle tieHandle = new TieHandle(className, previousValue, self);
// Propagate the glob name so select() returns the correct name
// (e.g., "main::STDOUT") even when the handle is tied.
if (previousValue != null) {
tieHandle.globName = previousValue.globName;
}
glob.IO.value = tieHandle;
// Update selectedHandle so that `print` without explicit filehandle
// goes through the tied handle (e.g., Test2::Plugin::IOEvents)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1828,6 +1828,12 @@ public RuntimeGlob globDeref() {
case GLOBREFERENCE -> {
// Some internal representations store PVIO as GLOBREFERENCE with a RuntimeIO value.
if (value instanceof RuntimeIO io) {
if (io.globName != null) {
RuntimeGlob actual = GlobalVariable.getExistingGlobalIO(io.globName);
if (actual != null) {
yield actual;
}
}
RuntimeGlob tmp = new RuntimeGlob("__ANON__::__ANONIO__");
tmp.setIO(io);
yield tmp;
Expand All @@ -1839,6 +1845,15 @@ public RuntimeGlob globDeref() {
// Perl allows postfix glob deref (->**) of PVIO by creating a temporary glob
// with the IO slot set to that handle.
if (value instanceof RuntimeIO io) {
// If the IO has a known glob name (e.g., "main::STDOUT"), look up the
// actual global glob so that operations like tie *{select()}, 'Class'
// affect the real handle, not a temporary copy.
if (io.globName != null) {
RuntimeGlob actual = GlobalVariable.getExistingGlobalIO(io.globName);
if (actual != null) {
yield actual;
}
}
RuntimeGlob tmp = new RuntimeGlob("__ANON__::__ANONIO__");
tmp.setIO(io);
yield tmp;
Expand Down Expand Up @@ -1881,6 +1896,12 @@ public RuntimeGlob globDerefNonStrict(String packageName) {
case GLOBREFERENCE -> {
// Some internal representations store PVIO as GLOBREFERENCE with a RuntimeIO value.
if (value instanceof RuntimeIO io) {
if (io.globName != null) {
RuntimeGlob actual = GlobalVariable.getExistingGlobalIO(io.globName);
if (actual != null) {
yield actual;
}
}
RuntimeGlob tmp = new RuntimeGlob("__ANON__::__ANONIO__");
tmp.setIO(io);
yield tmp;
Expand All @@ -1892,6 +1913,15 @@ public RuntimeGlob globDerefNonStrict(String packageName) {
// Perl allows postfix glob deref (->**) of PVIO by creating a temporary glob
// with the IO slot set to that handle.
if (value instanceof RuntimeIO io) {
// If the IO has a known glob name (e.g., "main::STDOUT"), look up the
// actual global glob so that operations like tie *{select()}, 'Class'
// affect the real handle, not a temporary copy.
if (io.globName != null) {
RuntimeGlob actual = GlobalVariable.getExistingGlobalIO(io.globName);
if (actual != null) {
yield actual;
}
}
RuntimeGlob tmp = new RuntimeGlob("__ANON__::__ANONIO__");
tmp.setIO(io);
yield tmp;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ public String getTiedPackage() {

@Override
public String toString() {
// Return the glob name (e.g., "main::STDOUT") when available, so that
// select() returns the correct name even when the handle is tied.
if (globName != null) {
return globName;
}
return "TIED_HANDLE(" + tiedPackage + ")";
}

Expand Down
Loading