diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 9f6452232..f755c80ff 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 = "7385e7908"; + public static final String gitCommitId = "8e15479f4"; /** * 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 13 2026 16:27:44"; + public static final String buildTimestamp = "Apr 13 2026 22:14:46"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/parser/StringParser.java b/src/main/java/org/perlonjava/frontend/parser/StringParser.java index f8d56d928..3b5d4ac3e 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StringParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StringParser.java @@ -253,6 +253,7 @@ 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("~")) { @@ -260,6 +261,13 @@ public static ParsedString parseRawStringWithDelimiter(EmitterContext ctx, List< // 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 } diff --git a/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java b/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java index d63bed63a..0e32da315 100644 --- a/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java +++ b/src/main/java/org/perlonjava/runtime/nativ/ExtendedNativeUtils.java @@ -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)); @@ -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)); @@ -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)); } @@ -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)); } @@ -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; } @@ -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)); } @@ -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")); @@ -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")); } diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index dd80628bc..e4f95c763 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -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) { @@ -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)); @@ -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(); @@ -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)) { diff --git a/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java b/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java index b6d0dcd17..ce9da9d7f 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SprintfOperator.java @@ -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; } diff --git a/src/main/java/org/perlonjava/runtime/operators/TieOperators.java b/src/main/java/org/perlonjava/runtime/operators/TieOperators.java index 62acc9bd0..5deab9d5e 100644 --- a/src/main/java/org/perlonjava/runtime/operators/TieOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/TieOperators.java @@ -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) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index cdd3c96a4..83e5ffbf7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -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; @@ -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; @@ -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; @@ -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; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java index 168771965..ea66b6c2f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/TieHandle.java @@ -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 + ")"; }