From 627bd90552c7cb1da5bd816c84a10019fd78002a Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 09:46:14 +0100 Subject: [PATCH 1/3] Fix list slice with range indices: (expr)[0..2] now works correctly The handleArrowArrayDeref method had an overly restrictive check that only treated a single range index (0..2) as a slice when the left side was a "true array literal" with multiple literal elements. This caused (func())[0..2] and similar expressions to evaluate the range in scalar context (flip-flop) instead of list context (range expansion). Simplified the condition: any single range operator in the index now always uses the slice path, regardless of what the left side is. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/jvm/Dereference.java | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/Dereference.java b/src/main/java/org/perlonjava/backend/jvm/Dereference.java index 830fffabc..6d4131340 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Dereference.java +++ b/src/main/java/org/perlonjava/backend/jvm/Dereference.java @@ -794,20 +794,11 @@ public static void handleArrowArrayDeref(EmitterVisitor emitterVisitor, BinaryOp ArrayLiteralNode right = (ArrayLiteralNode) node.right; - // Check if this is a true array literal (contains only literal elements like strings and numbers) - // and has a single range operator in the indices - boolean isArrayLiteral = node.left instanceof ArrayLiteralNode leftArray && - leftArray.elements.stream().allMatch(elem -> - elem instanceof StringNode || - elem instanceof NumberNode) && - leftArray.elements.size() > 1; // Must have multiple literal elements - boolean isSingleRange = right.elements.size() == 1 && right.elements.getFirst() instanceof BinaryOperatorNode binOp && "..".equals(binOp.operator); - - // Only apply the fix to true array literals with range operators - if (right.elements.size() == 1 && !(isArrayLiteral && isSingleRange)) { + + if (right.elements.size() == 1 && !isSingleRange) { // Single index: use get/delete/exists methods Node elem = right.elements.getFirst(); elem.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); From 43e1a75917ea2fd49a73518dd31ba9910f433019 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 09:49:24 +0100 Subject: [PATCH 2/3] Fix localtime/gmtime for large epochs and update Time::Local to 1.35 - Time.java: Use getDouble()+Math.floor() instead of getInt() for epoch values in localtime/gmtime, fixing truncation for values > 2^31 - Time.java: Fix DayOfWeek conversion (Java Mon=1..Sun=7 to Perl Sun=0..Sat=6) - Update Time::Local.pm from 1.30 to 1.35 (upstream CPAN) - Add Time-Local test import to config.yaml All 9 Time::Local test subtests now pass. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/import-perl5/config.yaml | 9 + .../perlonjava/runtime/operators/Time.java | 11 +- src/main/perl/lib/Time/Local.pm | 162 +++++++++++------- 3 files changed, 112 insertions(+), 70 deletions(-) diff --git a/dev/import-perl5/config.yaml b/dev/import-perl5/config.yaml index e8df5dd7e..61eca74ad 100644 --- a/dev/import-perl5/config.yaml +++ b/dev/import-perl5/config.yaml @@ -400,6 +400,15 @@ imports: target: perl5_t/Term-Table type: directory + # From CPAN distribution + - source: perl5/cpan/Time-Local/lib/Time/Local.pm + target: src/main/perl/lib/Time/Local.pm + + # Tests for distribution + - source: perl5/cpan/Time-Local/t + target: perl5_t/Time-Local + type: directory + # Add more imports below as needed # Example with minimal fields: # - source: perl5/lib/SomeModule.pm diff --git a/src/main/java/org/perlonjava/runtime/operators/Time.java b/src/main/java/org/perlonjava/runtime/operators/Time.java index 85b5931da..7fd5392e1 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Time.java +++ b/src/main/java/org/perlonjava/runtime/operators/Time.java @@ -89,8 +89,8 @@ public static RuntimeList localtime(RuntimeList args, int ctx) { if (args.isEmpty()) { date = ZonedDateTime.now(); } else { - long arg = args.getFirst().getInt(); - date = Instant.ofEpochSecond(arg).atZone(ZoneId.systemDefault()); + long epoch = (long) Math.floor(args.getFirst().getDouble()); + date = Instant.ofEpochSecond(epoch).atZone(ZoneId.systemDefault()); } return getTimeComponents(ctx, date); } @@ -107,8 +107,8 @@ public static RuntimeList gmtime(RuntimeList args, int ctx) { if (args.isEmpty()) { date = ZonedDateTime.now(ZoneOffset.UTC); } else { - long arg = args.getFirst().getInt(); - date = Instant.ofEpochSecond(arg).atZone(ZoneId.of("UTC")); + long epoch = (long) Math.floor(args.getFirst().getDouble()); + date = Instant.ofEpochSecond(epoch).atZone(ZoneId.of("UTC")); } return getTimeComponents(ctx, date); } @@ -127,7 +127,8 @@ private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) { res.add(date.getDayOfMonth()); res.add(date.getMonth().getValue() - 1); res.add(date.getYear() - 1900); - res.add(date.getDayOfWeek().getValue()); + int dow = date.getDayOfWeek().getValue(); // Mon=1..Sun=7 + res.add(dow == 7 ? 0 : dow); // Sun=0, Mon=1..Sat=6 res.add(date.getDayOfYear() - 1); res.add(date.getZone().getRules().isDaylightSavings(date.toInstant()) ? 1 : 0); return res; diff --git a/src/main/perl/lib/Time/Local.pm b/src/main/perl/lib/Time/Local.pm index 773414f72..7cd8c5204 100644 --- a/src/main/perl/lib/Time/Local.pm +++ b/src/main/perl/lib/Time/Local.pm @@ -5,7 +5,7 @@ use strict; use Carp (); use Exporter; -our $VERSION = '1.30'; +our $VERSION = '1.35'; use parent 'Exporter'; @@ -58,6 +58,18 @@ if ( $] < 5.012000 ) { else { # recent localtime()'s limit is the year 2**31 $MaxDay = 365 * ( 2**31 ); + + # On (some?) 32-bit platforms this overflows and we end up with a negative + # $MaxDay, which totally breaks this module. This is the old calculation + # we used from the days before Perl always had 64-bit time_t. + if ( $MaxDay < 0 ) { + require Config; + ## no critic (Variables::ProhibitPackageVars) + my $max_int + = ( ( 1 << ( 8 * $Config::Config{intsize} - 2 ) ) - 1 ) * 2 + 1; + $MaxDay + = int( ( $max_int - ( SECS_PER_DAY / 2 ) ) / SECS_PER_DAY ) - 1; + } } # Determine the EPOC day for this machine @@ -80,7 +92,7 @@ else { $Epoc = _daygm( gmtime(0) ); } -%Cheat = (); # clear the cache as epoc has changed +%Cheat = (); # clear the cache as epoc has changed sub _daygm { @@ -95,7 +107,8 @@ sub _daygm { + int( $year / 4 ) - int( $year / 100 ) + int( $year / 400 ) - + int( ( ( $month * 306 ) + 5 ) / 10 ) ) - $Epoc; + + int( ( ( $month * 306 ) + 5 ) / 10 ) ) + - $Epoc; } ); } @@ -111,6 +124,8 @@ sub _timegm { sub timegm { my ( $sec, $min, $hour, $mday, $month, $year ) = @_; + my $subsec = $sec - int($sec); + $sec = int($sec); if ( $Options{no_year_munging} ) { $year -= 1900; @@ -145,9 +160,8 @@ sub timegm { my $days = _daygm( undef, undef, undef, $mday, $month, $year ); - unless ( $Options{no_range_check} or abs($days) < $MaxDay ) { - my $msg = q{}; - $msg .= "Day too big - $days > $MaxDay\n" if $days > $MaxDay; + if ( abs($days) > $MaxDay && !$Options{no_range_check} ) { + my $msg = "Day too big - abs($days) > $MaxDay\n"; $year += 1900; $msg @@ -156,11 +170,16 @@ sub timegm { Carp::croak($msg); } - return - $sec + $SecOff - + ( SECS_PER_MINUTE * $min ) - + ( SECS_PER_HOUR * $hour ) - + ( SECS_PER_DAY * $days ); + # Adding in the $subsec value last seems to prevent floating point errors + # from creeping in. + return ( + ( + $sec + $SecOff + + ( SECS_PER_MINUTE * $min ) + + ( SECS_PER_HOUR * $hour ) + + ( SECS_PER_DAY * $days ) + ) + $subsec + ); } sub _is_leap_year { @@ -187,11 +206,16 @@ sub timegm_posix { } sub timelocal { + my $sec = shift; + my $subsec = $sec - int($sec); + $sec = int($sec); + unshift @_, $sec; + my $ref_t = &timegm; my $loc_for_ref_t = _timegm( localtime($ref_t) ); my $zone_off = $loc_for_ref_t - $ref_t - or return $loc_for_ref_t; + or return $loc_for_ref_t + $subsec; # Adjust for timezone my $loc_t = $ref_t - $zone_off; @@ -207,20 +231,20 @@ sub timelocal { && ( ( $ref_t - SECS_PER_HOUR ) - _timegm( localtime( $loc_t - SECS_PER_HOUR ) ) < 0 ) ) { - return $loc_t - SECS_PER_HOUR; + return ( $loc_t - SECS_PER_HOUR ) + $subsec; } # Adjust for DST change $loc_t += $dst_off; - return $loc_t if $dst_off > 0; + return $loc_t + $subsec if $dst_off > 0; # If the original date was a non-existent gap in a forward DST jump, we # should now have the wrong answer - undo the DST adjustment my ( $s, $m, $h ) = localtime($loc_t); $loc_t -= $dst_off if $s != $_[0] || $m != $_[1] || $h != $_[2]; - return $loc_t; + return $loc_t + $subsec; } sub timelocal_nocheck { @@ -254,7 +278,7 @@ Time::Local - Efficiently compute time from local and GMT time =head1 VERSION -version 1.30 +version 1.35 =head1 SYNOPSIS @@ -281,6 +305,8 @@ consistent with the values returned from C and C. =head2 C and C +I + These functions are the exact inverse of Perl's built-in C and C functions. That means that calling C<< timelocal_posix( localtime($value) ) >> will always give you the same C<$value> you started @@ -293,9 +319,9 @@ more details. These functions expect the year value to be the number of years since 1900, which is what the C and C built-ins returns. -They perform range checking by default on the input C<$sec>, C<$min>, -C<$hour>, C<$mday>, and C<$mon> values and will croak (using C) -if given a value outside the allowed ranges. +They perform range checking by default on the input C<$sec>, C<$min>, C<$hour>, +C<$mday>, and C<$mon> values and will croak (using C) if given a +value outside the allowed ranges. While it would be nice to make this the default behavior, that would almost certainly break a lot of code, so you must explicitly import these functions @@ -307,6 +333,8 @@ surprising. =head2 C and C +I + When C was first written, it was a common practice to represent years as a two-digit value like C<99> for C<1999> or C<1> for C<2001>. This caused all sorts of problems (google "Y2K problem" if you're very young) and @@ -316,26 +344,26 @@ The default exports of C and C do a complicated calculation when given a year value less than 1000. This leads to surprising results in many cases. See L for details. -The C functions do not do this year munging and simply take -the year value as provided. +The C functions do not do this year munging and simply take the +year value as provided. -They perform range checking by default on the input C<$sec>, C<$min>, -C<$hour>, C<$mday>, and C<$mon> values and will croak (using C) -if given a value outside the allowed ranges. +They perform range checking by default on the input C<$sec>, C<$min>, C<$hour>, +C<$mday>, and C<$mon> values and will croak (using C) if given a +value outside the allowed ranges. =head2 C and C This module exports two functions by default, C and C. -They perform range checking by default on the input C<$sec>, C<$min>, -C<$hour>, C<$mday>, and C<$mon> values and will croak (using C) -if given a value outside the allowed ranges. +They perform range checking by default on the input C<$sec>, C<$min>, C<$hour>, +C<$mday>, and C<$mon> values and will croak (using C) if given a +value outside the allowed ranges. -B or -C<*_modern> functions if possible.> +B or C<*_modern> +functions if possible.> =head2 C and C @@ -343,8 +371,8 @@ If you are working with data you know to be valid, you can use the "nocheck" variants, C and C. These variants must be explicitly imported. -If you supply data which is not valid (month 27, second 1,000) the results -will be unpredictable (so don't do that). +If you supply data which is not valid (month 27, second 1,000) the results will +be unpredictable (so don't do that). Note that my benchmarks show that this is just a 3% speed increase over the checked versions, so unless calling C is the hottest spot in your @@ -358,17 +386,16 @@ exports if you want to ensure consistent behavior as your code ages.> Strictly speaking, the year should be specified in a form consistent with C, i.e. the offset from 1900. In order to make the interpretation -of the year easier for humans, however, who are more accustomed to seeing -years as two-digit or four-digit values, the following conventions are -followed: +of the year easier for humans, however, who are more accustomed to seeing years +as two-digit or four-digit values, the following conventions are followed: =over 4 =item * Years greater than 999 are interpreted as being the actual year, rather than -the offset from 1900. Thus, 1964 would indicate the year Martin Luther King -won the Nobel prize, not the year 3864. +the offset from 1900. Thus, 1964 would indicate the year Martin Luther King won +the Nobel prize, not the year 3864. =item * @@ -379,11 +406,11 @@ below regarding date range). =item * Years in the range 0..99 are interpreted as shorthand for years in the rolling -"current century," defined as 50 years on either side of the current -year. Thus, today, in 1999, 0 would refer to 2000, and 45 to 2045, but 55 -would refer to 1955. Twenty years from now, 55 would instead refer to -2055. This is messy, but matches the way people currently think about two -digit dates. Whenever possible, use an absolute four digit year instead. +"current century," defined as 50 years on either side of the current year. +Thus, today, in 1999, 0 would refer to 2000, and 45 to 2045, but 55 would refer +to 1955. Twenty years from now, 55 would instead refer to 2055. This is messy, +but matches the way people currently think about two digit dates. Whenever +possible, use an absolute four digit year instead. =back @@ -414,15 +441,15 @@ occurs for two different GMT times on the same day. For example, in the "Europe/Paris" time zone, the local time of 2001-10-28 02:30:00 can represent either 2001-10-28 00:30:00 GMT, B 2001-10-28 01:30:00 GMT. -When given an ambiguous local time, the timelocal() function will always -return the epoch for the I of the two possible GMT times. +When given an ambiguous local time, the timelocal() function will always return +the epoch for the I of the two possible GMT times. =head2 Non-Existent Local Times (DST) -When a DST change causes a locale clock to skip one hour forward, there will -be an hour's worth of local times that don't exist. Again, for the -"Europe/Paris" time zone, the local clock jumped from 2001-03-25 01:59:59 to -2001-03-25 03:00:00. +When a DST change causes a locale clock to skip one hour forward, there will be +an hour's worth of local times that don't exist. Again, for the "Europe/Paris" +time zone, the local clock jumped from 2001-03-25 01:59:59 to 2001-03-25 +03:00:00. If the C function is given a non-existent local time, it will simply return an epoch value for the time one hour later. @@ -445,21 +472,20 @@ These routines are quite efficient and yet are always guaranteed to agree with C and C. We manage this by caching the start times of any months we've seen before. If we know the start time of the month, we can always calculate any time within the month. The start times are calculated -using a mathematical formula. Unlike other algorithms that do multiple calls -to C. +using a mathematical formula. Unlike other algorithms that do multiple calls to +C. -The C function is implemented using the same cache. We just -assume that we're translating a GMT time, and then fudge it when we're done -for the timezone and daylight savings arguments. Note that the timezone is -evaluated for each date because countries occasionally change their official -timezones. Assuming that C corrects for these changes, this -routine will also be correct. +The C function is implemented using the same cache. We just assume +that we're translating a GMT time, and then fudge it when we're done for the +timezone and daylight savings arguments. Note that the timezone is evaluated +for each date because countries occasionally change their official timezones. +Assuming that C corrects for these changes, this routine will also +be correct. =head1 AUTHORS EMERITUS -This module is based on a Perl 4 library, timelocal.pl, that was -included with Perl 4.036, and was most likely written by Tom -Christiansen. +This module is based on a Perl 4 library, timelocal.pl, that was included with +Perl 4.036, and was most likely written by Tom Christiansen. The current version was written by Graham Barr. @@ -472,8 +498,6 @@ Bugs may be submitted at L. There is a mailing list available for users of this distribution, L. -I am also usually active on IRC as 'autarch' on C. - =head1 SOURCE The source code repository for Time-Local can be found at L. @@ -484,7 +508,7 @@ Dave Rolsky =head1 CONTRIBUTORS -=for stopwords Florian Ragwitz J. Nick Koston Unknown +=for stopwords Florian Ragwitz Gregory Oschwald J. Nick Koston Tom Wyant Unknown =over 4 @@ -494,17 +518,25 @@ Florian Ragwitz =item * +Gregory Oschwald + +=item * + J. Nick Koston =item * +Tom Wyant + +=item * + Unknown =back =head1 COPYRIGHT AND LICENSE -This software is copyright (c) 1997 - 2020 by Graham Barr & Dave Rolsky. +This software is copyright (c) 1997 - 2023 by Graham Barr & Dave Rolsky. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself. From dbbc24a666759534a184ac93e0fb9a4d860da4e8 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 10:03:47 +0100 Subject: [PATCH 3/3] Fix scalar localtime/gmtime to use Perl ctime format Was using Java DateTimeFormatter.RFC_1123_DATE_TIME which produces Fri, 27 Feb 2026 10:00:43 +0100. Now produces the correct Perl asctime format: Fri Feb 27 10:00:43 2026. op/time.t: 21 to 71 of 72 passing (only ENV TZ test remains). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../java/org/perlonjava/runtime/operators/Time.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/operators/Time.java b/src/main/java/org/perlonjava/runtime/operators/Time.java index 7fd5392e1..c4a7dd499 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Time.java +++ b/src/main/java/org/perlonjava/runtime/operators/Time.java @@ -8,7 +8,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import java.time.format.DateTimeFormatter; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -116,7 +116,15 @@ public static RuntimeList gmtime(RuntimeList args, int ctx) { private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) { RuntimeList res = new RuntimeList(); if (ctx == RuntimeContextType.SCALAR) { - res.add(date.format(DateTimeFormatter.RFC_1123_DATE_TIME)); + String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; + String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; + int dow = date.getDayOfWeek().getValue(); // Mon=1..Sun=7 + String dayStr = days[dow - 1]; + String monStr = months[date.getMonth().getValue() - 1]; + int mday = date.getDayOfMonth(); + String timeStr = String.format("%02d:%02d:%02d", date.getHour(), date.getMinute(), date.getSecond()); + res.add(new RuntimeScalar(String.format("%s %s %2d %s %d", dayStr, monStr, mday, timeStr, date.getYear()))); return res; } // 0 1 2 3 4 5 6 7 8