From 4e15150d774c1803e5824523a70a37a858d25033 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 25 Mar 2026 13:13:17 +0100 Subject: [PATCH 1/3] Fix Unicode Is-prefix and B::perlstring escaping Two bugs were causing Specio test failures: 1. \P{IsPrint} matching all characters (76 test failures) - ICU4J doesn't recognize Is-prefixed property names like IsPrint - Fixed by stripping 'Is' prefix (e.g., IsPrint -> Print) before ICU lookup - The 'Is' prefix is a Perl/Java convention, not an official Unicode property 2. B::perlstring not escaping $ and \@ (caused Sub::Quote failures) - quote_sub() generates code like: my $_UNQUOTED = ${...->{"\"}} - Without escaped $, the hash key was interpolated as variable - This caused deferred subs to return undef instead of executing Test results improved from 76/311 failed to 6/311 failed. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/main/java/org/perlonjava/core/Configuration.java | 2 +- .../java/org/perlonjava/runtime/regex/UnicodeResolver.java | 6 ++++++ src/main/perl/lib/B.pm | 2 ++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index acbc7dcd4..b74d6474e 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "ad0d59ddc"; + public static final String gitCommitId = "4e68ad0b2"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java b/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java index 8740afe2e..ec735859b 100644 --- a/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java +++ b/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java @@ -389,6 +389,12 @@ private static String translateUnicodeProperty(String property, boolean negated, } } + // Strip 'Is' prefix for Perl compatibility (e.g., IsPrint -> Print, IsDigit -> Digit) + // ICU4J doesn't recognize Is-prefixed property names, but they're valid in Perl + if (property.startsWith("Is") && property.length() > 2 && Character.isUpperCase(property.charAt(2))) { + property = property.substring(2); + } + // Map Perl block aliases to Unicode block names if (property.equalsIgnoreCase("ASCII")) { property = "Basic_Latin"; diff --git a/src/main/perl/lib/B.pm b/src/main/perl/lib/B.pm index 2fdf19bb0..4021cfd12 100644 --- a/src/main/perl/lib/B.pm +++ b/src/main/perl/lib/B.pm @@ -202,6 +202,8 @@ sub perlstring { # Escape special characters $str =~ s/\\/\\\\/g; $str =~ s/"/\\"/g; + $str =~ s/\$/\\\$/g; # Escape $ to prevent interpolation + $str =~ s/\@/\\\@/g; # Escape @ to prevent interpolation $str =~ s/\n/\\n/g; $str =~ s/\r/\\r/g; $str =~ s/\t/\\t/g; From bef8bcb547dcec091c4f887f115584a111cab4c8 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 25 Mar 2026 13:17:58 +0100 Subject: [PATCH 2/3] Implement Scalar::Util::openhandle Previously openhandle was a placeholder always returning false. Now it properly checks if the argument is a GLOB/GLOBREFERENCE with an open IO handle (not ClosedIOHandle) and returns the filehandle itself if open, undef otherwise. This fixes FileHandle type detection in Specio. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/perlmodule/ScalarUtil.java | 37 ++++++++++++++++--- 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index b74d6474e..9df9f82bc 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "4e68ad0b2"; + public static final String gitCommitId = "4e15150d7"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java index 2f6a348d6..6be318a4a 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/ScalarUtil.java @@ -1,5 +1,6 @@ package org.perlonjava.runtime.perlmodule; +import org.perlonjava.runtime.io.ClosedIOHandle; import org.perlonjava.runtime.runtimetypes.*; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarType.*; @@ -234,18 +235,44 @@ public static RuntimeList looks_like_number(RuntimeArray args, int ctx) { } /** - * Placeholder for the openhandle functionality. + * Checks if a value is an open filehandle. + * Returns the filehandle itself if it's open, undef otherwise. * - * @param args The arguments passed to the method. + * @param args The arguments passed to the method (a single value). * @param ctx The context in which the method is called. - * @return A RuntimeList indicating if the scalar is an open handle. + * @return The filehandle if open, undef otherwise. */ public static RuntimeList openhandle(RuntimeArray args, int ctx) { if (args.size() != 1) { throw new IllegalStateException("Bad number of arguments for openhandle() method"); } - // Placeholder for openhandle functionality - return new RuntimeScalar(false).getList(); + RuntimeScalar arg = args.get(0); + + // Check if it's a GLOB or GLOBREFERENCE (filehandle) + if (arg.type == GLOB || arg.type == GLOBREFERENCE) { + Object value = arg.value; + + // Handle RuntimeGlob + if (value instanceof RuntimeGlob glob) { + RuntimeScalar io = glob.getIO(); + if (io != null && io.value instanceof RuntimeIO runtimeIO) { + // Check if the handle is open (not a ClosedIOHandle) + if (!(runtimeIO.ioHandle instanceof ClosedIOHandle)) { + return arg.getList(); // Return the filehandle itself + } + } + } + // Handle RuntimeIO directly + else if (value instanceof RuntimeIO runtimeIO) { + if (!(runtimeIO.ioHandle instanceof ClosedIOHandle)) { + return arg.getList(); // Return the filehandle itself + } + } + } + // Check for blessed object with *{} overload (handled in Perl code) + // For now, just return undef for non-glob types + + return new RuntimeScalar().getList(); // Return undef } /** From 806d5b03d700ce2fe241084ca7d45c93567b8ba7 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 25 Mar 2026 13:33:22 +0100 Subject: [PATCH 3/3] Fix @_ not captured in nested blocks for eval-compiled code When code was compiled via eval, the args stack (used by List::Util::any, all, etc. to pass the outer @_ to code blocks) was not being maintained correctly. The issue was that InterpretedCode.apply() overrode RuntimeCode.apply() but bypassed the pushArgs/popArgs calls that maintain the args stack. Similarly, BytecodeInterpreter fast path for calling InterpretedCode also bypassed these calls. Fixed by: 1. Making RuntimeCode.pushArgs/popArgs public 2. Adding pushArgs/popArgs to InterpretedCode.apply() 3. Adding pushArgs/popArgs to BytecodeInterpreter direct-call fast path This fixes Specio t/dict.t test which uses slurpy Dict types that rely on List::Util::all accessing $_[0] from the enclosing scope. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/bytecode/BytecodeInterpreter.java | 18 +++++++++++++++--- .../backend/bytecode/InterpretedCode.java | 17 +++++++++++++++-- .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/runtimetypes/RuntimeCode.java | 6 ++++-- 4 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 7e80a9fca..7222cacaf 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -844,8 +844,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // bypassing RuntimeCode.apply() indirection chain if (codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof InterpretedCode interpCode) { // Direct call to interpreter - skip RuntimeCode.apply overhead - // Pass null for subroutineName to enable frame caching - result = BytecodeInterpreter.execute(interpCode, callArgs, context, null); + // Push args to argsStack for getCallerArgs() support (used by List::Util::any/all/etc.) + RuntimeCode.pushArgs(callArgs); + try { + // Pass null for subroutineName to enable frame caching + result = BytecodeInterpreter.execute(interpCode, callArgs, context, null); + } finally { + RuntimeCode.popArgs(); + } } else { // Slow path for JVM-compiled code, symbolic references, etc. result = RuntimeCode.apply(codeRef, "", callArgs, context); @@ -860,7 +866,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c callArgs = flow.getTailCallArgs(); // Use fast path for InterpretedCode if (codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof InterpretedCode interpCode) { - result = BytecodeInterpreter.execute(interpCode, callArgs, context, null); + // Push args for tail call too + RuntimeCode.pushArgs(callArgs); + try { + result = BytecodeInterpreter.execute(interpCode, callArgs, context, null); + } finally { + RuntimeCode.popArgs(); + } } else { result = RuntimeCode.apply(codeRef, "tailcall", callArgs, context); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 91f73a453..a61d34d02 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -228,12 +228,25 @@ public InterpreterState.InterpreterFrame getOrCreateFrame(String packageName, St */ @Override public RuntimeList apply(RuntimeArray args, int callContext) { - return BytecodeInterpreter.execute(this, args, callContext); + // Push args for getCallerArgs() support (used by List::Util::any/all/etc.) + // This matches what RuntimeCode.apply() does for JVM-compiled subs + RuntimeCode.pushArgs(args); + try { + return BytecodeInterpreter.execute(this, args, callContext); + } finally { + RuntimeCode.popArgs(); + } } @Override public RuntimeList apply(String subroutineName, RuntimeArray args, int callContext) { - return BytecodeInterpreter.execute(this, args, callContext, subroutineName); + // Push args for getCallerArgs() support (used by List::Util::any/all/etc.) + RuntimeCode.pushArgs(args); + try { + return BytecodeInterpreter.execute(this, args, callContext, subroutineName); + } finally { + RuntimeCode.popArgs(); + } } /** diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 9df9f82bc..95889cc4e 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "4e15150d7"; + public static final String gitCommitId = "bef8bcb54"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 0b47daa96..8549cf816 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -174,15 +174,17 @@ public static RuntimeArray getCallerArgs() { /** * Push @_ onto the args stack when entering a subroutine. + * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ - private static void pushArgs(RuntimeArray args) { + public static void pushArgs(RuntimeArray args) { argsStack.get().push(args); } /** * Pop @_ from the args stack when exiting a subroutine. + * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ - private static void popArgs() { + public static void popArgs() { Deque stack = argsStack.get(); if (!stack.isEmpty()) { stack.pop();