From 76478da82251a5fbcc4aa8f387b450c172613e6d Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 30 Oct 2026 12:01:03 +0100 Subject: [PATCH] fix: preserve input line context across borrowed handle scope Keep the last-read handle active when JVM scope cleanup sees a temporary alias of an anonymous IO glob that still has a live owner. This preserves Perl's `$.` and IO::Handle->input_line_number behavior through wrapper subroutines. Add focused coverage for scalar-backed IO::Handle reads and record the fix in the work-in-progress changelog. Fixes #1933. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- docs/about/changelog.md | 2 ++ .../runtime/runtimetypes/RuntimeScalar.java | 12 +++++++++-- .../unit/io_handle_input_line_number.t | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) create mode 100644 src/test/resources/unit/io_handle_input_line_number.t diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 6fe3d8dde5..82c18de71a 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,6 +6,8 @@ priorities and future plans. ## Work in progress +- Preserve the input line number when a borrowed anonymous filehandle alias leaves a JVM subroutine scope (#1933). + - Resolve glob-backed filehandles in `POSIX::isatty` without assuming the glob value is a `RuntimeIO` (#1932). - Add an initial CryptX Java XS bridge for binary/base64url helpers, functional digests, and HMAC using the existing JCA and Bouncy Castle providers (#1220). diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index b2eabcdf5b..d45c22c80e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -5201,8 +5201,16 @@ public static void scopeExitCleanup(RuntimeScalar scalar) { RuntimeIO last = RuntimeIO.getLastAccessedHandle(); if ((glob.globName != null && glob.globName.equals(last.globName)) || (glob.IO != null && glob.IO.value == last)) { - RuntimeIO.setLastAccessedHandle(null); - RuntimeIO.setLastReadlineHandleName(null); + // A borrowed anonymous handle alias can leave scope while the + // caller still owns the same IO glob. Keep its $. context until + // that durable owner leaves; clearing on the borrow breaks + // getline/input_line_number calls inside wrapper subroutines. + boolean borrowedLiveIo = isUnstashedIoGlob(glob) + && !scalar.ioOwner && glob.ioHolderCount > 0; + if (!borrowedLiveIo) { + RuntimeIO.setLastAccessedHandle(null); + RuntimeIO.setLastReadlineHandleName(null); + } } } diff --git a/src/test/resources/unit/io_handle_input_line_number.t b/src/test/resources/unit/io_handle_input_line_number.t new file mode 100644 index 0000000000..b4c92e5f84 --- /dev/null +++ b/src/test/resources/unit/io_handle_input_line_number.t @@ -0,0 +1,21 @@ +use strict; +use warnings; +use Test::More; +use IO::Handle; + +my $input = "first\nsecond\n"; +open my $fh, '<', \$input or die "open scalar handle: $!"; + +is($fh->getline, "first\n", 'read the first line from a scalar handle'); +is($., 1, 'the global input line number follows getline'); +my $read_line_number = sub { $. }; +is($read_line_number->(), 1, + 'the global input line number is visible inside a subroutine'); +is($fh->input_line_number, 1, + 'input_line_number reports the current line after getline'); + +is($fh->getline, "second\n", 'read the second line from a scalar handle'); +is($fh->input_line_number, 2, + 'input_line_number advances with subsequent getline calls'); + +done_testing;