From ee8a5eaecd435e4696ff4cede94ef61d9e9acaf2 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 28 Oct 2026 11:23:32 +0100 Subject: [PATCH 1/2] fix: implement Time::HiRes::stat (#1912) Register and export stat, delegate core behavior to the shared stat implementation, and preserve available sub-second file timestamps. Add system-Perl-validated regression coverage for both execution backends. Fixes #1912. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- dev/design/issue-queue.md | 1 + docs/about/changelog.md | 2 + .../runtime/perlmodule/TimeHiRes.java | 47 +++++++++++++++++++ src/main/perl/lib/Time/HiRes.pm | 2 +- src/test/resources/unit/time_hires_stat.t | 24 ++++++++++ 5 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/unit/time_hires_stat.t diff --git a/dev/design/issue-queue.md b/dev/design/issue-queue.md index bfc35041ae..dc4a1f5578 100644 --- a/dev/design/issue-queue.md +++ b/dev/design/issue-queue.md @@ -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 metadata | [#1912](https://github.com/fglock/PerlOnJava/issues/1912) `Time::HiRes::stat` | Normal / Active | Daiku 1.004 passes on system Perl and fails because the runtime lacks `Time::HiRes::stat`; implement the shared stat bridge and test both backends. Current claim is on the issue. | | 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. | | Frontend constants/quoting | [#1481](https://github.com/fglock/PerlOnJava/issues/1481) Literal control characters | Normal / Ready | One upstream failure on both backends, Perl passes. Reduce literal `qw` operands before choosing frontend versus regex code; require focused and upstream acceptance. | diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 2e7ce29e2a..a5ebb30201 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -6,6 +6,8 @@ priorities and future plans. ## Work in progress +- Implement `Time::HiRes::stat` with high-resolution file timestamps (#1912). + - Export `fieldhash` from `Hash::Util` for compatibility with core Perl. - Normalize tied-hash `exists` results to Perl's boolean values. diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/TimeHiRes.java b/src/main/java/org/perlonjava/runtime/perlmodule/TimeHiRes.java index 8dcb8f5c59..17ae22dba3 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/TimeHiRes.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/TimeHiRes.java @@ -1,12 +1,21 @@ package org.perlonjava.runtime.perlmodule; import org.perlonjava.runtime.operators.MathOperators; +import org.perlonjava.runtime.operators.Stat; import org.perlonjava.runtime.operators.Time; +import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; +import org.perlonjava.runtime.runtimetypes.RuntimeIO; import org.perlonjava.runtime.runtimetypes.RuntimeList; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import org.perlonjava.runtime.runtimetypes.RuntimeScalarType; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; import java.time.Instant; public class TimeHiRes extends PerlModuleBase { @@ -27,6 +36,7 @@ public static void initialize() { module.registerMethod("usleep", null); module.registerMethod("nanosleep", null); module.registerMethod("gettimeofday", null); + module.registerMethod("stat", null); // getLong() can fetch tied scalars or run numeric overload code. // Keep call-depth tracking so those callbacks remain visible. module.registerMethodWithReadOnlyArgsWithoutJvmClosureFrame("clock_gettime", null); @@ -68,6 +78,43 @@ public static RuntimeList gettimeofday(RuntimeArray args, int ctx) { return result; } + /** Return Perl stat fields with sub-second timestamps where the filesystem provides them. */ + public static RuntimeList stat(RuntimeArray args, int ctx) { + RuntimeScalar target = args.isEmpty() + ? GlobalVariable.getGlobalVariable("main::_") + : args.get(0); + RuntimeList result = Stat.stat(target, ctx).getList(); + if (ctx != RuntimeContextType.LIST || result.elements.size() < 11 + || target.type == RuntimeScalarType.GLOB + || target.type == RuntimeScalarType.GLOBREFERENCE) { + return result; + } + + try { + Path path = RuntimeIO.resolvePath(target.toString()); + if (path == null) return result; + BasicFileAttributes attrs = Files.readAttributes(path, BasicFileAttributes.class); + result.elements.set(8, new RuntimeScalar(epochSeconds(attrs.lastAccessTime()))); + result.elements.set(9, new RuntimeScalar(epochSeconds(attrs.lastModifiedTime()))); + try { + Object ctime = Files.getAttribute(path, "unix:ctime"); + if (ctime instanceof FileTime fileTime) { + result.elements.set(10, new RuntimeScalar(epochSeconds(fileTime))); + } + } catch (UnsupportedOperationException | IllegalArgumentException ignored) { + // Non-Unix filesystems do not expose a distinct status-change time. + } + } catch (IOException | RuntimeException ignored) { + // Keep the ordinary stat result if high-resolution attributes are unavailable. + } + return result; + } + + private static double epochSeconds(FileTime fileTime) { + Instant instant = fileTime.toInstant(); + return instant.getEpochSecond() + instant.getNano() / 1_000_000_000.0; + } + /** Return a high-resolution clock reading in seconds. */ public static RuntimeList clock_gettime(RuntimeArray args, int ctx) { long clockId = args.get(0).getLong(); diff --git a/src/main/perl/lib/Time/HiRes.pm b/src/main/perl/lib/Time/HiRes.pm index 86b8c6d160..5e5a13f239 100644 --- a/src/main/perl/lib/Time/HiRes.pm +++ b/src/main/perl/lib/Time/HiRes.pm @@ -21,7 +21,7 @@ use Exporter 'import'; our $VERSION = '1.9764'; our @EXPORT_OK = qw(usleep nanosleep ualarm gettimeofday tv_interval time sleep alarm - clock_gettime CLOCK_REALTIME CLOCK_MONOTONIC); + stat clock_gettime CLOCK_REALTIME CLOCK_MONOTONIC); require XSLoader; XSLoader::load('Time::HiRes'); diff --git a/src/test/resources/unit/time_hires_stat.t b/src/test/resources/unit/time_hires_stat.t new file mode 100644 index 0000000000..8eb0827203 --- /dev/null +++ b/src/test/resources/unit/time_hires_stat.t @@ -0,0 +1,24 @@ +use strict; +use warnings; +use Test::More; +use File::Temp qw(tempfile); +use Time::HiRes qw(stat); + +my ($fh, $path) = tempfile(); +print {$fh} "stat test\n"; +close $fh or die "close $path: $!"; + +my @core_stat = CORE::stat($path); +my @qualified_stat = Time::HiRes::stat($path); +is(scalar @qualified_stat, 13, 'qualified stat returns the stat fields'); +is($qualified_stat[7], $core_stat[7], 'qualified stat reports the file size'); +cmp_ok($qualified_stat[9], '>=', $core_stat[9], 'high-resolution mtime is not before core stat'); +cmp_ok($qualified_stat[9], '<', $core_stat[9] + 1, 'high-resolution mtime is within the same second'); + +my @imported_stat = stat($path); +is($imported_stat[7], $core_stat[7], 'imported stat returns the file size'); +ok(Time::HiRes::stat($path), 'qualified stat returns true in scalar context'); +is(scalar(Time::HiRes::stat("$path.missing")), '', 'stat on a missing file returns false'); + +unlink $path or die "unlink $path: $!"; +done_testing(); From 3cad18477e360f2a04efb16c7eec0d7bbd73dbce Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 28 Oct 2026 11:26:07 +0100 Subject: [PATCH 2/2] docs: link Time::HiRes::stat issue to PR #1913 Update the project issue queue with the active implementation PR. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- dev/design/issue-queue.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/design/issue-queue.md b/dev/design/issue-queue.md index dc4a1f5578..eceb2d1fd2 100644 --- a/dev/design/issue-queue.md +++ b/dev/design/issue-queue.md @@ -26,7 +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 metadata | [#1912](https://github.com/fglock/PerlOnJava/issues/1912) `Time::HiRes::stat` | Normal / Active | Daiku 1.004 passes on system Perl and fails because the runtime lacks `Time::HiRes::stat`; implement the shared stat bridge and test both backends. Current claim is on the issue. | +| 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. | | Frontend constants/quoting | [#1481](https://github.com/fglock/PerlOnJava/issues/1481) Literal control characters | Normal / Ready | One upstream failure on both backends, Perl passes. Reduce literal `qw` operands before choosing frontend versus regex code; require focused and upstream acceptance. |