From c5fef9e392daacffd337a216bf19ceecb2c7dd9a Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 31 Jul 2026 10:58:49 +0200 Subject: [PATCH] fix: support File::Checksum and CPAN time/lvalue tests Add the Java XS implementation for File::Checksum, correct POSIX mktime normalization and DST handling used by Time::Simple, and preserve substr lvalue identity through references for Scalar::Util::Reftype. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- .../perlonjava/backend/jvm/EmitOperator.java | 4 ++ .../runtime/operators/ReferenceOperators.java | 18 ++++--- .../runtime/perlmodule/FileChecksum.java | 54 +++++++++++++++++++ .../perlonjava/runtime/perlmodule/POSIX.java | 20 ++++++- .../runtime/perlmodule/ScalarUtil.java | 4 ++ 5 files changed, 92 insertions(+), 8 deletions(-) create mode 100644 src/main/java/org/perlonjava/runtime/perlmodule/FileChecksum.java diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index 3ab178ff3..19752c4c3 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -1785,6 +1785,10 @@ static void handleCreateReference(EmitterVisitor emitterVisitor, OperatorNode no // *{EXPR} — EXPR is evaluated in scalar context (e.g. Symbol::qualify_to_ref's // \*{ qualify $_[0], ... }). LIST context breaks the comma/ternary inside braces. contextType = RuntimeContextType.SCALAR; + } else if (node.operand instanceof OperatorNode op && op.operator.equals("substr")) { + // \substr(...) must retain the substring proxy so reftype() + // and assignment observe Perl's LVALUE reference. + contextType = RuntimeContextType.SCALAR; } else if (node.operand instanceof BinaryOperatorNode binOp && binOp.operator.equals("=")) { contextType = RuntimeContextType.SCALAR; } diff --git a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java index 5295fb84e..e335b402f 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java @@ -254,13 +254,17 @@ public static RuntimeScalar ref(RuntimeScalar runtimeScalar) { // Handle nested references String ref = "REF"; if (runtimeScalar.value instanceof RuntimeScalar scalar) { - ref = switch (scalar.type) { - case VSTRING -> "VSTRING"; - case REGEX, ARRAYREFERENCE, HASHREFERENCE, CODE, GLOBREFERENCE, REFERENCE -> "REF"; - case GLOB -> "GLOB"; - case READONLY_SCALAR -> ref((RuntimeScalar) scalar.value).toString(); - default -> "SCALAR"; - }; + if (scalar instanceof RuntimeSubstrLvalue) { + ref = "LVALUE"; + } else { + ref = switch (scalar.type) { + case VSTRING -> "VSTRING"; + case REGEX, ARRAYREFERENCE, HASHREFERENCE, CODE, GLOBREFERENCE, REFERENCE -> "REF"; + case GLOB -> "GLOB"; + case READONLY_SCALAR -> ref((RuntimeScalar) scalar.value).toString(); + default -> "SCALAR"; + }; + } } if (runtimeScalar.value == null) { str = ref; diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileChecksum.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileChecksum.java new file mode 100644 index 000000000..61ec898ce --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileChecksum.java @@ -0,0 +1,54 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeList; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +/** Java XS implementation of File::Checksum's portable 16-bit checksum. */ +public class FileChecksum extends PerlModuleBase { + + public static final String XS_VERSION = "0.01"; + + public FileChecksum() { + super("File::Checksum", false); + } + + public static void initialize() { + FileChecksum module = new FileChecksum(); + module.defineExport("EXPORT", "Checksum"); + try { + module.registerMethod("Checksum", null); + } catch (NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + + /** Matches Checksum.xs: native-endian 16-bit words, folded and complemented. */ + public static RuntimeList Checksum(RuntimeArray args, int ctx) { + if (args.size() < 2) { + return new RuntimeScalar(0).getList(); + } + + long sum = 0; + int count = args.get(1).getInt(); + try { + byte[] bytes = Files.readAllBytes(Path.of(args.get(0).toString())); + int limit = Math.min(Math.max(count, 0), bytes.length); + // PerlOnJava runs on the supported little-endian platforms. The XS + // implementation reads an unsigned short directly from FILE*. + for (int i = 0; i + 1 < limit; i += 2) { + sum += (bytes[i] & 0xffL) | ((bytes[i + 1] & 0xffL) << 8); + } + } catch (IOException | RuntimeException ignored) { + return new RuntimeScalar(0).getList(); + } + + sum = (sum >>> 16) + (sum & 0xffff); + sum += sum >>> 16; + return new RuntimeScalar((~sum) & 0xffff).getList(); + } +} diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java index 38bcd6534..d07bcfbaf 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java @@ -407,8 +407,26 @@ public static RuntimeList mktime(RuntimeArray args, int ctx) { int actualMon = mon + 1; try { - LocalDateTime ldt = LocalDateTime.of(actualYear, actualMon, mday, hour, min, sec); + // mktime normalizes out-of-range fields (Time::Simple deliberately + // passes hour-1 during DST), so construct from midnight and add the + // supplied hour instead of rejecting hour == -1. + LocalDateTime ldt = LocalDateTime.of(actualYear, actualMon, mday, 0, 0, 0) + .plusHours(hour).plusMinutes(min).plusSeconds(sec); ZonedDateTime zdt = ldt.atZone(ZoneId.systemDefault()); + // Perl's mktime honors an explicitly supplied isdst flag. Time::Simple + // passes isdst=0 after subtracting the current DST hour; Java's default + // resolution otherwise leaves the value one hour early during DST. + if (args.size() >= 9) { + int isdst = args.get(8).getInt(); + if (isdst == 0) { + ZoneId zone = ZoneId.systemDefault(); + int daylightSeconds = zdt.getOffset().getTotalSeconds() + - zone.getRules().getStandardOffset(zdt.toInstant()).getTotalSeconds(); + if (daylightSeconds > 0) { + zdt = zdt.plusSeconds(daylightSeconds); + } + } + } return new RuntimeScalar(zdt.toEpochSecond()).getList(); } catch (Exception e) { return new RuntimeScalar(-1).getList(); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java index e217bb0e2..19aa0d760 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java @@ -149,11 +149,15 @@ public static RuntimeList reftype(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for reftype() method"); } RuntimeScalar scalar = magicallyDeref(args.get(0)); + if (scalar instanceof RuntimeSubstrLvalue) { + return new RuntimeScalar("LVALUE").getList(); + } String type = switch (scalar.type) { case REFERENCE -> { // Inspect the referent to distinguish SCALAR refs from REF (ref-to-ref) if (scalar.value instanceof RuntimeScalar inner) { if (inner.type == READONLY_SCALAR) inner = (RuntimeScalar) inner.value; + if (inner instanceof RuntimeSubstrLvalue) yield "LVALUE"; yield switch (inner.type) { case VSTRING -> "VSTRING"; case REGEX, ARRAYREFERENCE, HASHREFERENCE, CODE, GLOBREFERENCE, REFERENCE -> "REF";