diff --git a/dev/design/issue-queue.md b/dev/design/issue-queue.md index c61988b837..383e4f1b82 100644 --- a/dev/design/issue-queue.md +++ b/dev/design/issue-queue.md @@ -1,6 +1,6 @@ # Reviewed issue queues -Snapshot: 2026-10-28, from live open issues, PRs and the #1649 phase contract. +Snapshot: 2026-10-29, from live open issues, PRs and the #1649 phase contract. This is a bounded selection aid, not the complete backlog or fresh runtime validation. Follow the [triage and claim protocol](../../docs/guides/issue-triage.md) and recheck live issue comments/PRs before starting. Unlisted work is unreviewed @@ -26,6 +26,7 @@ resolved before implementation. No new pass or failure is asserted here. | Code/test boundary | Issue | Priority / state | Evidence, next action and blocker | |---|---|---|---| +| File-handle EOF state | [#1922](https://github.com/fglock/PerlOnJava/issues/1922) `IO::Handle::clearerr` | Normal / Active | `Log::Procmail` 0.14 passes on system Perl but failed on both PerlOnJava backends when reading appended data after EOF. The shared runtime fix and permanent regression are in [draft PR #1924](https://github.com/fglock/PerlOnJava/pull/1924); the full make gate and all 281 CPAN tests pass. Await review and CI. | | File metadata | [#1912](https://github.com/fglock/PerlOnJava/issues/1912) `Time::HiRes::stat` | Normal / Active | Daiku 1.004 passes on system Perl and exposed the missing runtime function; the implementation and both-backend regression are in [PR #1913](https://github.com/fglock/PerlOnJava/pull/1913). | | XML provider APIs | [#1892](https://github.com/fglock/PerlOnJava/issues/1892) XSLT output | High / Active | [PR #1894](https://github.com/fglock/PerlOnJava/pull/1894) is open; verify its serialization fix against HTML::ToDocBook. Coordinate through that work rather than start a duplicate. | | XML provider APIs | [#1323](https://github.com/fglock/PerlOnJava/issues/1323) `indexElements` | Normal / Ready | Missing Document method is identified. Add standard-Perl-validated focused coverage and Cindy acceptance; check XML provider overlap with #1894. No established ownership prerequisite. | diff --git a/docs/about/changelog.md b/docs/about/changelog.md index acb6686b9c..acb69ba30b 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -11,6 +11,8 @@ priorities and future plans. - Run MakeMaker `MY::post_initialize` hooks so generated files join `pm_to_blib`, and parse Perl's `$]` version variable. - Preserve hash-reference constant calls after typeglob undefinition, dereference glob-backed hash values during Morpheus configuration traversal, parse Error.pm's dynamic `catch $class with { ... }` form on both backends, and retain integer scalar identity through `int()` for decimal stringification; preserve regex referent types through blessed scalar references and cover Morpheus relational overload dispatch and caller filename normalization (#1342, #1376, #1427, #1428). +- Clear EOF state across file-handle wrappers when `IO::Handle::clearerr` is called, allowing reads to resume after a file grows (#1922). +- Preserve exact integer results for wide `unpack` checksums instead of zeroing sums rounded by double conversion. - Implement `Time::HiRes::stat` with high-resolution file timestamps (#1912). - Export `fieldhash` from `Hash::Util` for compatibility with core Perl. diff --git a/src/main/java/org/perlonjava/runtime/io/BorrowedIOHandle.java b/src/main/java/org/perlonjava/runtime/io/BorrowedIOHandle.java index 6438e34968..2332a6acff 100644 --- a/src/main/java/org/perlonjava/runtime/io/BorrowedIOHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/BorrowedIOHandle.java @@ -136,6 +136,12 @@ public RuntimeScalar eof() { return delegate.eof(); } + @Override + public RuntimeScalar clearerr() { + if (closed) return handleClosed("clearerr"); + return delegate.clearerr(); + } + @Override public RuntimeScalar tell() { if (closed) return handleClosed("tell"); diff --git a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java index 798abdc407..03868480e7 100644 --- a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java @@ -525,6 +525,18 @@ public RuntimeScalar eof() { } } + /** + * Clear EOF state so a regular file can be read again after it grows. + * Keep any unread bytes in the read-ahead buffer; only discard the physical + * EOF observation so the next read can probe the channel again. + */ + @Override + public RuntimeScalar clearerr() { + isEOF = false; + physicalEof = false; + return new RuntimeScalar(0); + } + /** * Gets the current position in the file. * diff --git a/src/main/java/org/perlonjava/runtime/io/DupIOHandle.java b/src/main/java/org/perlonjava/runtime/io/DupIOHandle.java index 425e23e51e..04384b803e 100644 --- a/src/main/java/org/perlonjava/runtime/io/DupIOHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/DupIOHandle.java @@ -214,6 +214,12 @@ public RuntimeScalar eof() { return delegate.eof(); } + @Override + public RuntimeScalar clearerr() { + if (closed) return handleClosed("clearerr"); + return delegate.clearerr(); + } + @Override public RuntimeScalar tell() { if (closed) return handleClosed("tell"); diff --git a/src/main/java/org/perlonjava/runtime/io/IOHandle.java b/src/main/java/org/perlonjava/runtime/io/IOHandle.java index b58ece476a..cc8a3937ec 100644 --- a/src/main/java/org/perlonjava/runtime/io/IOHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/IOHandle.java @@ -241,6 +241,15 @@ default RuntimeScalar eof() { return RuntimeIO.handleIOError("eof operation is not supported."); } + /** + * Clears the handle's EOF indicator. The Perl-level IO::Handle method also + * clears the process error variable; handles with buffered EOF state must + * reset that state here so reads can resume after the underlying source grows. + */ + default RuntimeScalar clearerr() { + return new RuntimeScalar(0); + } + default RuntimeScalar tell() { return RuntimeIO.handleIOError("tell operation is not supported."); } diff --git a/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java b/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java index 2c9ae0749e..09c2cd661d 100644 --- a/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/LayeredIOHandle.java @@ -830,6 +830,11 @@ public RuntimeScalar eof() { return delegate.eof(); } + @Override + public RuntimeScalar clearerr() { + return delegate.clearerr(); + } + /** * Returns the current position in the file. * diff --git a/src/main/java/org/perlonjava/runtime/io/NativeFdIOHandle.java b/src/main/java/org/perlonjava/runtime/io/NativeFdIOHandle.java index 59f0105ee2..16f1fd9adf 100644 --- a/src/main/java/org/perlonjava/runtime/io/NativeFdIOHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/NativeFdIOHandle.java @@ -138,6 +138,12 @@ public RuntimeScalar eof() { return eofReached ? RuntimeScalarCache.scalarTrue : RuntimeScalarCache.scalarFalse; } + @Override + public RuntimeScalar clearerr() { + eofReached = false; + return new RuntimeScalar(0); + } + @Override public boolean isReadReady() { return !closed; diff --git a/src/main/java/org/perlonjava/runtime/io/SharedTransportIOHandle.java b/src/main/java/org/perlonjava/runtime/io/SharedTransportIOHandle.java index a45b813f08..e44ee93a5b 100644 --- a/src/main/java/org/perlonjava/runtime/io/SharedTransportIOHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/SharedTransportIOHandle.java @@ -56,6 +56,7 @@ public ThreadInheritancePolicy threadInheritancePolicy() { @Override public RuntimeScalar fileno() { return open() ? delegate.fileno() : closed("fileno"); } @Override public boolean isReadReady() { return !open() || delegate.isReadReady(); } @Override public RuntimeScalar eof() { return open() ? delegate.eof() : scalarTrue; } + @Override public RuntimeScalar clearerr() { return open() ? delegate.clearerr() : closed("clearerr"); } @Override public RuntimeScalar tell() { return open() ? delegate.tell() : closed("tell"); } @Override public RuntimeScalar seek(long pos, int whence) { return open() ? delegate.seek(pos, whence) : closed("seek"); } @Override public RuntimeScalar truncate(long length) { return open() ? delegate.truncate(length) : closed("truncate"); } diff --git a/src/main/java/org/perlonjava/runtime/operators/Unpack.java b/src/main/java/org/perlonjava/runtime/operators/Unpack.java index b52cc3dc3f..902760775b 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Unpack.java +++ b/src/main/java/org/perlonjava/runtime/operators/Unpack.java @@ -483,22 +483,10 @@ private static RuntimeList unpackInternal(String template, UnpackState state, BigInteger mask = BigInteger.ONE.shiftLeft(checksumBits).subtract(BigInteger.ONE); bigChecksum = bigChecksum.and(mask); - // Now check if the masked value would lose precision as a double. - double maskedAsDouble = bigChecksum.doubleValue(); - if ((format != 'Q' && format != 'J') - && maskedAsDouble > 0 && maskedAsDouble == maskedAsDouble - 1.0) { - values.add(new RuntimeScalar(0)); - } else { - // Perl's unsigned-native checksum promotion at - // 63 bits retains the carry that crosses the - // signed-long boundary. - if ((format == 'Q' || format == 'J') && checksumBits == 63 - && bigChecksum.equals(BigInteger.ONE.shiftLeft(63).subtract(BigInteger.ONE))) { - values.add(new RuntimeScalar(bigChecksum.add(BigInteger.TWO))); - } else { - values.add(new RuntimeScalar(bigChecksum.longValue())); - } - } + // Keep integer checksums exact. Converting to double + // here incorrectly turns values such as 2**54 - 1 + // into zero when the double rounds up to 2**54. + values.add(new RuntimeScalar(bigChecksum.longValue())); } else if (checksumBits == 64) { // 64-bit mask: 2^64 - 1 = 0xFFFFFFFFFFFFFFFF BigInteger mask = BigInteger.ONE.shiftLeft(64).subtract(BigInteger.ONE); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java b/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java index b3453d2e1e..a7949ee7e4 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/IOHandle.java @@ -106,9 +106,10 @@ public static RuntimeList _clearerr(RuntimeArray args, int ctx) { return new RuntimeList(new RuntimeScalar(-1)); } - // Clear $! + RuntimeScalar result = fh.ioHandle.clearerr(); + // Clear $! after the underlying handle has reset its EOF/error state. GlobalVariable.getGlobalVariable("main::!").set(""); - return new RuntimeList(new RuntimeScalar(0)); + return result.getList(); } /** diff --git a/src/test/resources/unit/io_clearerr_after_eof.t b/src/test/resources/unit/io_clearerr_after_eof.t new file mode 100644 index 0000000000..6c81979213 --- /dev/null +++ b/src/test/resources/unit/io_clearerr_after_eof.t @@ -0,0 +1,22 @@ +use strict; +use warnings; +use Test::More tests => 4; +use File::Temp qw(tempfile); +use IO::File; + +my ($writer, $path) = tempfile(); +print {$writer} "first\n" or die "write initial line: $!"; +close $writer or die "close initial writer: $!"; + +my $reader = IO::File->new($path, '<') or die "open reader: $!"; +is(scalar <$reader>, "first\n", 'read initial line'); +is(scalar <$reader>, undef, 'read reaches EOF'); + +open $writer, '>>', $path or die "open append writer: $!"; +print {$writer} "second\n" or die "append line: $!"; +close $writer or die "close append writer: $!"; + +is($reader->clearerr, 0, 'clearerr succeeds'); +is(scalar <$reader>, "second\n", 'read data appended after EOF'); + +$reader->close; diff --git a/src/test/resources/unit/pack_core_regressions.t b/src/test/resources/unit/pack_core_regressions.t index d04b48cac9..f574941dfa 100644 --- a/src/test/resources/unit/pack_core_regressions.t +++ b/src/test/resources/unit/pack_core_regressions.t @@ -68,6 +68,14 @@ is(length(pack('D', 0)), $Config::Config{longdblsize}, 'Config reports the implemented long-double pack width'); is(unpack('%54Q', pack('Q', 18014398509481983)), 18014398509481983, 'wide Q checksum retains integer precision modulo 2**54'); +is(unpack('%63Q*', pack('Q*', 0, 1, 9223372036854775807, + 9223372036854775808, 18446744073709551615)), + '9223372036854775807', 'wide Q checksum preserves the exact 63-bit masked sum'); +for my $bits (54, 63) { + my $expected = $bits == 54 ? '18014398509481983' : '9223372036854775807'; + is(unpack("%${bits}c*", pack('c*', -128, -1, 0, 1, 127)), $expected, + "wide c checksum retains integer precision modulo 2**$bits"); +} is(unpack('w', pack('w', '18014398509481982')), '18014398509481982', 'BER preserves a large exact integer string'); {