From 73bc99bdca048c2b67977c40e96bf9b3450e65dc Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 17:28:47 +0100 Subject: [PATCH 01/16] Fix regex octal escapes inside character classes In Perl, \1-\7 inside character classes are always octal escapes, not backreferences. Java regex requires a leading zero for octal sequences. The preprocessor was passing single/double digit octals through as-is, causing Java Pattern.compile to reject them. Fixes 11 ExifTool test failures caused by MakerNoteHP2 condition regex [\0-\4]. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/regex/RegexPreprocessorHelper.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java index 761e905ff..18ed138f5 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java @@ -626,9 +626,15 @@ static int handleRegexCharacterClassEscape(int offset, String s, StringBuilder s sb.append(Character.toChars(c2)); lastChar = octalValue; } else { - // Short octal or single digit, pass through - sb.append(Character.toChars(c2)); - lastChar = c2; + // Short octal (1-2 digits) inside character class + // In character classes, \1-\7 are always octals, not backrefs + // Java requires leading zero: \4 → \04, \12 → \012 + sb.append('0'); + for (int i = 0; i < octalLength; i++) { + sb.append(s.charAt(offset + i)); + } + offset += octalLength - 1; + lastChar = octalValue; } } else if (c2 == '8' || c2 == '9') { // \8 and \9 are not valid octals - treat as literal digits From 3bdfbe0e04d27013f1d73ff305accaea1aa088e9 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 17:51:54 +0100 Subject: [PATCH 02/16] Fix \G regex anchor and grep scalar context in bytecode interpreter Two bugs fixed: 1. \G regex anchor was never active: RuntimeRegex had a hardcoded `private final boolean useGAssertion = false` field that shadowed the actual flag from RegexFlags. Now useGAssertion is set from regexFlags.useGAssertion() after compilation and in all copy paths. 2. grep in scalar context always returned 1 in bytecode interpreter: ListOperators.grep() already wraps the count in scalar context, but BytecodeInterpreter was re-wrapping with elements.size() which is always 1. Removed the double-wrapping to match the MAP handler. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../perlonjava/backend/bytecode/BytecodeInterpreter.java | 8 +------- .../java/org/perlonjava/runtime/regex/RuntimeRegex.java | 6 +++++- 2 files changed, 6 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 ab4e39927..64fe66c62 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1718,13 +1718,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeList list = listBase.getList(); RuntimeScalar closure = (RuntimeScalar) registers[closureReg]; RuntimeList result = ListOperators.grep(list, closure, ctx); - - // In scalar context, return the count of elements - if (ctx == RuntimeContextType.SCALAR) { - registers[rd] = new RuntimeScalar(result.elements.size()); - } else { - registers[rd] = result; - } + registers[rd] = result; break; } diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index c7f1e0de8..2ebdcb581 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -80,7 +80,7 @@ public static void restoreMatchState(Object[] state) { } // Indicates if \G assertion is used - private final boolean useGAssertion = false; + boolean useGAssertion = false; // Compiled regex pattern public Pattern pattern; int patternFlags; @@ -133,6 +133,7 @@ public static RuntimeRegex compile(String patternString, String modifiers) { regex.regexFlags = fromModifiers(modifiers, patternString); regex.patternFlags = regex.regexFlags.toPatternFlags(); + regex.useGAssertion = regex.regexFlags.useGAssertion(); String javaPattern = null; try { @@ -283,6 +284,7 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternString = originalRegex.patternString; regex.regexFlags = mergeRegexFlags(originalRegex.regexFlags, modifierStr, originalRegex.patternString); regex.patternFlags = regex.regexFlags.toPatternFlags(); + regex.useGAssertion = regex.regexFlags.useGAssertion(); return new RuntimeScalar(regex); } @@ -308,6 +310,7 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternString = originalRegex.patternString; regex.regexFlags = mergeRegexFlags(originalRegex.regexFlags, modifierStr, originalRegex.patternString); regex.patternFlags = regex.regexFlags.toPatternFlags(); + regex.useGAssertion = regex.regexFlags.useGAssertion(); return new RuntimeScalar(regex); } @@ -373,6 +376,7 @@ public static RuntimeScalar getReplacementRegex(RuntimeScalar patternString, Run } } + regex.useGAssertion = regex.regexFlags != null && regex.regexFlags.useGAssertion(); regex.replacement = replacement; return new RuntimeScalar(regex); } From 428461a899c697209521499aaeeb4d0e4bf8d1cc Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 18:11:25 +0100 Subject: [PATCH 03/16] Fix ScalarSpecialVariable not resolving in copy/set operations ScalarSpecialVariable (regex captures like $1, $&) computes its value dynamically via getValueAsScalar(), but its type/value fields stay as UNDEF/null. The RuntimeScalar copy constructor and set() method directly accessed these fields, resulting in UNDEF when $1 was used in array ref construction ([$1] -> empty), hash construction, or any copy context. Added instanceof checks to resolve ScalarSpecialVariable before copying. Fixes RTF Unicode parsing (Recompose([$2])) and MIFF metadata. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/runtime/runtimetypes/RuntimeScalar.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index bf054c56e..67c4942f0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -109,6 +109,9 @@ public RuntimeScalar(RuntimeScalar scalar) { if (scalar.type == TIED_SCALAR) { scalar = scalar.tiedFetch(); } + if (scalar instanceof ScalarSpecialVariable ssv) { + scalar = ssv.getValueAsScalar(); + } this.type = scalar.type; this.value = scalar.value; } @@ -636,6 +639,9 @@ public RuntimeScalar set(RuntimeScalar value) { if (value.type == TIED_SCALAR) { return set(value.tiedFetch()); } + if (value instanceof ScalarSpecialVariable ssv) { + return set(ssv.getValueAsScalar()); + } if (this.type == TIED_SCALAR) { return this.tiedStore(value); } From 8530ecbefe3b5e45ba6b82440295cd25c227759f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 18:31:17 +0100 Subject: [PATCH 04/16] Fix non-/g regex matches incorrectly using pos() In Perl, pos() only affects /g matches. Non-/g matches always start from position 0 regardless of pos(). PerlOnJava was incorrectly using pos() for all matches, causing regex failures when a prior /g match had set pos() on a string. This fixed ExifTool Text_5 test where IsUTF8() used /g matches on a string, leaving pos() set, and a subsequent non-/g match on the same string via $$dataPt !~ /[\x80-\x9f]/ would incorrectly start from the pos() position instead of 0. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../java/org/perlonjava/runtime/regex/RuntimeRegex.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 2ebdcb581..eea7b0899 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -442,13 +442,15 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc // hexPrinter(inputStr); // Use RuntimePosLvalue to get the current position + // In Perl, pos() only affects /g matches; non-/g matches always start from position 0 RuntimeScalar posScalar = RuntimePosLvalue.pos(string); - boolean isPosDefined = posScalar.getDefinedBoolean(); + boolean isGlobal = regex.regexFlags.isGlobalMatch(); + boolean isPosDefined = isGlobal && posScalar.getDefinedBoolean(); int startPos = isPosDefined ? posScalar.getInt() : 0; // Check if previous call had zero-length match at this position (for SCALAR context) // This prevents infinite loops in: while ($str =~ /pat/g) - if (regex.regexFlags.isGlobalMatch() && ctx == RuntimeContextType.SCALAR) { + if (isGlobal && ctx == RuntimeContextType.SCALAR) { String patternKey = regex.patternString; if (RuntimePosLvalue.hadZeroLengthMatchAt(string, startPos, patternKey)) { // Previous match was zero-length at this position - fail to break loop @@ -457,7 +459,7 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc } } - // Start matching from the current position if defined + // Start matching from the current position if defined (only for /g matches) if (isPosDefined) { matcher.region(startPos, inputStr.length()); } From 603b84b5969a9762482c8924d728c5196b01894b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 18:54:49 +0100 Subject: [PATCH 05/16] Fix multiple binary data handling and special variable bugs - Fix not() operator for ScalarSpecialVariable ($1, $2, etc.) which always returned true because it checked the type field (always UNDEF) instead of delegating to getBoolean(). This caused DICOM parsing to incorrectly treat explicit VR as implicit VR. - Fix Digest::MD5 and Digest::SHA add() methods to use ISO_8859_1 instead of UTF_8 for byte string conversion, matching the addfile() methods. Fixes wrong IPTC digest computation for data with bytes >= 0x80. - Fix lengthBytes (use bytes; length) to return character count for BYTE_STRING type instead of UTF-8 byte count. Bytes >= 0x80 were being double-counted as 2 UTF-8 bytes. - Fix string concatenation to preserve BYTE_STRING type when either operand is BYTE_STRING and all characters are <= 255. Previously, concatenating a BYTE_STRING with a STRING would always upgrade to STRING, causing downstream binary data corruption. - Fix unpack float/double handlers to use code point path for UTF-8 flagged data, matching the existing short/long handler pattern. Bytes >= 0x80 were being UTF-8 expanded before float interpretation, corrupting values like 1.0 into -1.78e-38. Fixes ExifTool tests: DICOM_2, PDF_2,3, PhotoMechanic_2 Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/operators/MathOperators.java | 3 + .../runtime/operators/StringOperators.java | 74 +++++++++---------- .../unpack/NumericFormatHandler.java | 54 ++++++++++---- .../runtime/perlmodule/DigestMD5.java | 4 +- .../runtime/perlmodule/DigestSHA.java | 4 +- 5 files changed, 78 insertions(+), 61 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java index 31a189797..e44b06f6c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java @@ -725,6 +725,9 @@ public static RuntimeScalar integer(RuntimeScalar arg1) { public static RuntimeScalar not(RuntimeScalar runtimeScalar) { + if (runtimeScalar instanceof ScalarSpecialVariable) { + return getScalarBoolean(!runtimeScalar.getBoolean()); + } return switch (runtimeScalar.type) { case INTEGER -> getScalarBoolean((int) runtimeScalar.value == 0); case DOUBLE -> getScalarBoolean((double) runtimeScalar.value == 0.0); diff --git a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java index 40cd65af4..fac5b71d7 100644 --- a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java @@ -41,18 +41,14 @@ public static RuntimeScalar length(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} containing the byte length of the input */ public static RuntimeScalar lengthBytes(RuntimeScalar runtimeScalar) { - // If the scalar is undefined, return undef if (!runtimeScalar.getDefinedBoolean()) { return RuntimeScalarCache.scalarUndef; } - // Convert the RuntimeScalar to a string and return its byte length String str = runtimeScalar.toString(); - try { - return getScalarInt(str.getBytes(StandardCharsets.UTF_8).length); - } catch (Exception e) { - // If UTF-8 encoding fails, fall back to character count - return getScalarInt(str.codePointCount(0, str.length())); + if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { + return getScalarInt(str.length()); } + return getScalarInt(str.getBytes(StandardCharsets.UTF_8).length); } /** @@ -281,37 +277,34 @@ public static RuntimeScalar stringConcat(RuntimeScalar runtimeScalar, RuntimeSca boolean aIsString = runtimeScalar.type == RuntimeScalarType.STRING || runtimeScalar.type == RuntimeScalarType.BYTE_STRING; boolean bIsString = b.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.BYTE_STRING; - // Preserve Perl-like UTF-8 flag semantics only for string scalars. - // For other types, keep legacy behavior to avoid wide behavioral changes. if (aIsString && bIsString) { - // If either operand is explicitly STRING type, return STRING - if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { - return new RuntimeScalar(aStr + bStr); - } - - // Both are BYTE_STRING - check if they actually contain only bytes 0-255 boolean hasUnicode = false; for (int i = 0; i < aStr.length(); i++) { - if (aStr.charAt(i) > 255) { - hasUnicode = true; - break; - } + if (aStr.charAt(i) > 255) { hasUnicode = true; break; } } if (!hasUnicode) { for (int i = 0; i < bStr.length(); i++) { - if (bStr.charAt(i) > 255) { - hasUnicode = true; - break; - } + if (bStr.charAt(i) > 255) { hasUnicode = true; break; } } } - // If Unicode present, upgrade to STRING to preserve characters if (hasUnicode) { return new RuntimeScalar(aStr + bStr); } - // Pure byte strings - concatenate as bytes + if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING || b.type == RuntimeScalarType.BYTE_STRING) { + byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] out = new byte[aBytes.length + bBytes.length]; + System.arraycopy(aBytes, 0, out, 0, aBytes.length); + System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); + return new RuntimeScalar(out); + } + + if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { + return new RuntimeScalar(aStr + bStr); + } + byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); byte[] out = new byte[aBytes.length + bBytes.length]; @@ -335,34 +328,33 @@ public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeS boolean bIsString = b.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.BYTE_STRING; if (aIsString && bIsString) { - // If either operand is explicitly STRING type, return STRING - if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { - return new RuntimeScalar(aStr + bStr); - } - - // Both are BYTE_STRING - check if they actually contain only bytes 0-255 boolean hasUnicode = false; for (int i = 0; i < aStr.length(); i++) { - if (aStr.charAt(i) > 255) { - hasUnicode = true; - break; - } + if (aStr.charAt(i) > 255) { hasUnicode = true; break; } } if (!hasUnicode) { for (int i = 0; i < bStr.length(); i++) { - if (bStr.charAt(i) > 255) { - hasUnicode = true; - break; - } + if (bStr.charAt(i) > 255) { hasUnicode = true; break; } } } - // If Unicode present, upgrade to STRING to preserve characters if (hasUnicode) { return new RuntimeScalar(aStr + bStr); } - // Pure byte strings - concatenate as bytes + if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING || b.type == RuntimeScalarType.BYTE_STRING) { + byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] out = new byte[aBytes.length + bBytes.length]; + System.arraycopy(aBytes, 0, out, 0, aBytes.length); + System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); + return new RuntimeScalar(out); + } + + if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { + return new RuntimeScalar(aStr + bStr); + } + byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); byte[] out = new byte[aBytes.length + bBytes.length]; diff --git a/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java b/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java index ed9e56fbd..dc271478a 100644 --- a/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java +++ b/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java @@ -614,24 +614,34 @@ public int getFormatSize() { public static class FloatHandler extends NumericFormatHandler { @Override public void unpack(UnpackState state, List output, int count, boolean isStarCount) { - // Save current mode - boolean wasCharacterMode = state.isCharacterMode(); + if (state.isUTF8Data() && state.isCharacterMode()) { + ByteBuffer buffer = state.getBuffer(); + boolean isBigEndian = (buffer.order() == java.nio.ByteOrder.BIG_ENDIAN); + for (int i = 0; i < count; i++) { + if (state.remainingCodePoints() < 4) break; + int b1 = state.nextCodePoint() & 0xFF; + int b2 = state.nextCodePoint() & 0xFF; + int b3 = state.nextCodePoint() & 0xFF; + int b4 = state.nextCodePoint() & 0xFF; + int intBits = isBigEndian + ? (b1 << 24) | (b2 << 16) | (b3 << 8) | b4 + : b1 | (b2 << 8) | (b3 << 16) | (b4 << 24); + output.add(new RuntimeScalar((double) Float.intBitsToFloat(intBits))); + } + return; + } - // Switch to byte mode for numeric reading + boolean wasCharacterMode = state.isCharacterMode(); if (wasCharacterMode) { state.switchToByteMode(); } ByteBuffer buffer = state.getBuffer(); - for (int i = 0; i < count; i++) { - if (buffer.remaining() < 4) { - break; - } + if (buffer.remaining() < 4) break; output.add(new RuntimeScalar(buffer.getFloat())); } - // Restore original mode if (wasCharacterMode) { state.switchToCharacterMode(); } @@ -646,24 +656,36 @@ public int getFormatSize() { public static class DoubleHandler extends NumericFormatHandler { @Override public void unpack(UnpackState state, List output, int count, boolean isStarCount) { - // Save current mode - boolean wasCharacterMode = state.isCharacterMode(); + if (state.isUTF8Data() && state.isCharacterMode()) { + ByteBuffer buffer = state.getBuffer(); + boolean isBigEndian = (buffer.order() == java.nio.ByteOrder.BIG_ENDIAN); + for (int i = 0; i < count; i++) { + if (state.remainingCodePoints() < 8) break; + long bits = 0; + for (int j = 0; j < 8; j++) { + int b = state.nextCodePoint() & 0xFF; + if (isBigEndian) { + bits = (bits << 8) | b; + } else { + bits |= ((long) b) << (j * 8); + } + } + output.add(new RuntimeScalar(Double.longBitsToDouble(bits))); + } + return; + } - // Switch to byte mode for numeric reading + boolean wasCharacterMode = state.isCharacterMode(); if (wasCharacterMode) { state.switchToByteMode(); } ByteBuffer buffer = state.getBuffer(); - for (int i = 0; i < count; i++) { - if (buffer.remaining() < 8) { - break; - } + if (buffer.remaining() < 8) break; output.add(new RuntimeScalar(buffer.getDouble())); } - // Restore original mode if (wasCharacterMode) { state.switchToCharacterMode(); } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java b/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java index 3bd0dbcf7..0830f1e37 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java @@ -72,7 +72,7 @@ public static RuntimeList add(RuntimeArray args, int ctx) { // Check for wide characters using the utility method StringParser.assertNoWideCharacters(dataStr, "add"); - byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8); + byte[] bytes = dataStr.getBytes(StandardCharsets.ISO_8859_1); md.update(bytes); updateBlockCount(self, bytes.length); } @@ -176,7 +176,7 @@ public static RuntimeList add_bits(RuntimeArray args, int ctx) { } int numBytes = nbits / 8; - byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); + byte[] dataBytes = data.getBytes(StandardCharsets.ISO_8859_1); byte[] truncatedBytes = new byte[Math.min(numBytes, dataBytes.length)]; System.arraycopy(dataBytes, 0, truncatedBytes, 0, truncatedBytes.length); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java b/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java index 34a3e1246..76a03f47e 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java @@ -101,7 +101,7 @@ public static RuntimeList add(RuntimeArray args, int ctx) { StringParser.assertNoWideCharacters(dataStr, "add"); if (data.type != RuntimeScalarType.UNDEF) { - md.update(dataStr.getBytes(StandardCharsets.UTF_8)); + md.update(dataStr.getBytes(StandardCharsets.ISO_8859_1)); } } @@ -199,7 +199,7 @@ public static RuntimeList add_bits(RuntimeArray args, int ctx) { int nbits = nbitsScalar.getInt(); // Convert data to bytes, but only use the specified number of bits - byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); + byte[] dataBytes = data.getBytes(StandardCharsets.ISO_8859_1); byte[] truncatedBytes = truncateToNBits(dataBytes, nbits); md.update(truncatedBytes); } From 18ebd7ce75136b459f487abfd9ff76302a64bb86 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 19:05:39 +0100 Subject: [PATCH 06/16] Fix Encode::is_utf8 returning inverted result The is_utf8() function was checking type == BYTE_STRING when it should check type != BYTE_STRING. In Perl 5, is_utf8() returns true when the string has the internal UTF-8 flag (character string), and false for byte strings. PerlOnJava BYTE_STRING type represents byte strings (no UTF-8 flag), so is_utf8 should return false for them. This caused ExifTool to incorrectly UTF-8 encode binary image data when reading from scalar references, because it uses is_utf8() to decide whether to re-encode the data. Fixes ExifTool tests: PPM_3, GIF_2, IPTC_4, IPTC_5 Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- src/main/java/org/perlonjava/runtime/perlmodule/Encode.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java b/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java index bb39ab3ab..ff1adcede 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java @@ -175,7 +175,7 @@ public static RuntimeList is_utf8(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for is_utf8"); } - return RuntimeScalarCache.getScalarBoolean(args.get(0).type == BYTE_STRING).getList(); + return RuntimeScalarCache.getScalarBoolean(args.get(0).type != BYTE_STRING).getList(); // // In PerlOnJava, strings are always internally Unicode (Java strings) // // So we'll check if the string contains any non-ASCII characters From 79109844b7d3a23860de2e7d0a3d26ee99b5922e Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 19:46:34 +0100 Subject: [PATCH 07/16] Fix BYTE_STRING propagation, sort SUBNAME LIST, foreach lexical restore, substr lvalue - joinInternal: use OR logic for BYTE_STRING (any byte element makes result bytes) - stringConcat/stringConcatWarnUninitialized: preserve BYTE_STRING in fallthrough when one operand is UNDEF/INTEGER - ParseMapGrepSort: detect sort SUBNAME LIST pattern before trying block parse - EmitForeach: save/restore pre-existing lexical variables used as loop variables - RuntimeSubstrLvalue.set(): preserve BYTE_STRING type after substr assignment Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../perlonjava/backend/jvm/EmitForeach.java | 23 ++++++++ .../frontend/parser/ParseMapGrepSort.java | 25 +++++++++ .../runtime/operators/StringOperators.java | 53 ++++++++++++++++--- .../runtimetypes/RuntimeSubstrLvalue.java | 7 ++- 4 files changed, 100 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java index 491a814aa..48bda77ee 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java @@ -263,6 +263,23 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { variableNode = actualVariable; } + // Save pre-existing lexical variable before the loop so we can restore after + int savedLexicalVarIndex = -1; + int lexicalVarIndex = -1; + if (!isReferenceAliasing && !loopVariableIsGlobal + && variableNode instanceof OperatorNode saveOp) { + String saveName = extractSimpleVariableName(saveOp); + if (saveName != null) { + int idx = emitterVisitor.ctx.symbolTable.getVariableIndex(saveName); + if (idx != -1) { + lexicalVarIndex = idx; + savedLexicalVarIndex = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); + mv.visitVarInsn(Opcodes.ALOAD, lexicalVarIndex); + mv.visitVarInsn(Opcodes.ASTORE, savedLexicalVarIndex); + } + } + } + // For global $_ as loop variable, we need to: // 1. Evaluate the list first (before any localization takes effect) // 2. For statement modifiers: localize $_ ourselves @@ -567,6 +584,12 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { mv.visitLabel(loopEnd); + // Restore pre-existing lexical variable after the loop + if (savedLexicalVarIndex != -1) { + mv.visitVarInsn(Opcodes.ALOAD, savedLexicalVarIndex); + mv.visitVarInsn(Opcodes.ASTORE, lexicalVarIndex); + } + // Restore the original value for reference aliasing: for \$x (...), for \@x (...), for \%x (...) if (isReferenceAliasing && savedValueIndex != -1) { if (actualVariable instanceof OperatorNode innerOp && innerOp.operand instanceof IdentifierNode) { diff --git a/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java b/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java index 3ed5de56c..1137246ca 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java @@ -2,6 +2,8 @@ import org.perlonjava.frontend.astnode.*; import org.perlonjava.frontend.lexer.LexerToken; +import org.perlonjava.frontend.lexer.LexerTokenType; +import org.perlonjava.runtime.runtimetypes.NameNormalizer; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import java.util.List; @@ -20,6 +22,29 @@ public class ParseMapGrepSort { static BinaryOperatorNode parseSort(Parser parser, LexerToken token) { ListNode operand; int currentIndex = parser.tokenIndex; + + LexerToken firstToken = peek(parser); + if (firstToken.type == LexerTokenType.IDENTIFIER + && !ParserTables.CORE_PROTOTYPES.containsKey(firstToken.text) + && !ParsePrimary.isIsQuoteLikeOperator(firstToken.text)) { + int savedIndex = parser.tokenIndex; + String subName = IdentifierParser.parseSubroutineIdentifier(parser); + if (subName != null && !subName.isEmpty()) { + LexerToken afterName = peek(parser); + if (!afterName.text.equals("(") && !afterName.text.equals("=>")) { + String fullName = subName.contains("::") + ? subName + : NameNormalizer.normalizeVariableName(subName, + parser.ctx.symbolTable.getCurrentPackage()); + operand = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false); + Node block = new OperatorNode("&", + new IdentifierNode(fullName, currentIndex), currentIndex); + return new BinaryOperatorNode(token.text, block, operand, parser.tokenIndex); + } + } + parser.tokenIndex = savedIndex; + } + try { // Handle 'sort' keyword as a Binary operator with a Code and List operands operand = ListParser.parseZeroOrMoreList(parser, 1, true, false, false, false); diff --git a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java index fac5b71d7..cee06757c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java @@ -313,7 +313,24 @@ public static RuntimeScalar stringConcat(RuntimeScalar runtimeScalar, RuntimeSca return new RuntimeScalar(out); } - return new RuntimeScalar(runtimeScalar + bStr); + if (runtimeScalar.type == BYTE_STRING || b.type == BYTE_STRING) { + boolean hasWide = false; + for (int i = 0; i < aStr.length() && !hasWide; i++) { + if (aStr.charAt(i) > 255) hasWide = true; + } + for (int i = 0; i < bStr.length() && !hasWide; i++) { + if (bStr.charAt(i) > 255) hasWide = true; + } + if (!hasWide) { + byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] out = new byte[aBytes.length + bBytes.length]; + System.arraycopy(aBytes, 0, out, 0, aBytes.length); + System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); + return new RuntimeScalar(out); + } + } + return new RuntimeScalar(aStr + bStr); } public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeScalar, RuntimeScalar b) { @@ -363,7 +380,24 @@ public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeS return new RuntimeScalar(out); } - return new RuntimeScalar(runtimeScalar + bStr); + if (runtimeScalar.type == BYTE_STRING || b.type == BYTE_STRING) { + boolean hasWide = false; + for (int i = 0; i < aStr.length() && !hasWide; i++) { + if (aStr.charAt(i) > 255) hasWide = true; + } + for (int i = 0; i < bStr.length() && !hasWide; i++) { + if (bStr.charAt(i) > 255) hasWide = true; + } + if (!hasWide) { + byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] out = new byte[aBytes.length + bBytes.length]; + System.arraycopy(aBytes, 0, out, 0, aBytes.length); + System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); + return new RuntimeScalar(out); + } + } + return new RuntimeScalar(aStr + bStr); } public static RuntimeScalar chompScalar(RuntimeScalar runtimeScalar) { @@ -550,7 +584,7 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa RuntimeScalarCache.scalarEmptyString); } - boolean isByteString = runtimeScalar.type == BYTE_STRING; + boolean anyIsByteString = runtimeScalar.type == BYTE_STRING; String delimiter = runtimeScalar.toString(); @@ -576,12 +610,19 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa RuntimeScalarCache.scalarEmptyString); } - isByteString = isByteString && scalar.type == BYTE_STRING; + anyIsByteString = anyIsByteString || scalar.type == BYTE_STRING; sb.append(scalar); } RuntimeScalar res = new RuntimeScalar(sb.toString()); - if (isByteString) { - res.type = BYTE_STRING; + if (anyIsByteString) { + String resultStr = sb.toString(); + boolean hasWide = false; + for (int i = 0; i < resultStr.length(); i++) { + if (resultStr.charAt(i) > 255) { hasWide = true; break; } + } + if (!hasWide) { + res.type = BYTE_STRING; + } } return res; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java index 85f898f3e..d4c057b95 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java @@ -96,8 +96,11 @@ public RuntimeScalar set(RuntimeScalar value) { updatedValue.replace(startIndex, endIndex, newValue); } - // Update the parent RuntimeScalar with the modified string - lvalue.set(new RuntimeScalar(updatedValue.toString())); + RuntimeScalar newVal = new RuntimeScalar(updatedValue.toString()); + if (lvalue.type == RuntimeScalarType.BYTE_STRING) { + newVal.type = RuntimeScalarType.BYTE_STRING; + } + lvalue.set(newVal); return this; } From c5210e7e706b5f4712dcea507e533b74f3d983df Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 20:35:47 +0100 Subject: [PATCH 08/16] Disable tryWholeBlockRefactoring to fix return inside loops tryWholeBlockRefactoring wraps large blocks in anonymous subs, breaking return semantics (return exits the anon sub, not the enclosing function). The interpreter fallback handles "Method too large" safely. Also adds return detection to ControlFlowDetectorVisitor as defense-in-depth, implements goto LABEL in bytecode compiler, and caches overload method resolution. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 44 ++++++++++++++++++- .../backend/bytecode/CompileOperator.java | 3 ++ .../jvm/astrefactor/LargeBlockRefactorer.java | 7 ++- .../analysis/ControlFlowDetectorVisitor.java | 6 +++ .../runtimetypes/DynamicVariableManager.java | 2 - .../runtime/runtimetypes/OverloadContext.java | 15 ++++--- 6 files changed, 65 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index ba8e7eac5..6fc14bfcf 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -69,6 +69,10 @@ private static class LoopInfo { } } + // goto LABEL support: track label positions and pending forward goto patches + private final Map gotoLabelPCs = new HashMap<>(); // label name → PC + private final Map> pendingGotoPatches = new HashMap<>(); // label name → list of PCs to patch + // Token index tracking for error reporting private final TreeMap pcToTokenIndex = new TreeMap<>(); int currentTokenIndex = -1; // Track current token for error reporting @@ -4483,7 +4487,17 @@ public void visit(TryNode node) { @Override public void visit(LabelNode node) { - // Labels are tracked in loops, standalone labels are no-ops + int labelPc = bytecode.size(); + gotoLabelPCs.put(node.label, labelPc); + + // Patch any pending forward gotos to this label + List patches = pendingGotoPatches.remove(node.label); + if (patches != null) { + for (int patchPc : patches) { + patchJump(patchPc, labelPc); + } + } + lastResultReg = -1; } @@ -4736,4 +4750,32 @@ void handleLoopControlOperator(OperatorNode node, String op) { targetLoop.redoPcs.add(patchPc); } } + + void handleGotoOperator(OperatorNode node) { + String labelStr = null; + if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { + Node arg = labelNode.elements.getFirst(); + if (arg instanceof IdentifierNode) { + labelStr = ((IdentifierNode) arg).name; + } + } + + if (labelStr == null) { + throwCompilerException("goto without label is not supported", node.getIndex()); + return; + } + + // Check if label was already seen (backward goto) + Integer targetPc = gotoLabelPCs.get(labelStr); + if (targetPc != null) { + emit(Opcodes.GOTO); + emitInt(targetPc); + } else { + // Forward goto: emit GOTO with placeholder, patch later + emit(Opcodes.GOTO); + int patchPc = bytecode.size(); + emitInt(0); + pendingGotoPatches.computeIfAbsent(labelStr, k -> new ArrayList<>()).add(patchPc); + } + } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index bc387b96a..1f32a5dfd 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -394,6 +394,9 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Loop control operators: last/next/redo [LABEL] bytecodeCompiler.handleLoopControlOperator(node, op); bytecodeCompiler.lastResultReg = -1; // No result after control flow + } else if (op.equals("goto")) { + bytecodeCompiler.handleGotoOperator(node); + bytecodeCompiler.lastResultReg = -1; } else if (op.equals("rand")) { // rand() or rand($max) // Calls Random.rand(max) where max defaults to 1 diff --git a/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java b/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java index c0b46f394..f6171d9af 100644 --- a/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java +++ b/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java @@ -157,10 +157,9 @@ public static boolean processBlock(EmitterVisitor emitterVisitor, BlockNode node return false; } - // Fallback: Try whole-block refactoring - return tryWholeBlockRefactoring(emitterVisitor, node); // Block was refactored and emitted - - // No refactoring was possible + // tryWholeBlockRefactoring is disabled: the interpreter fallback handles + // "Method too large" safely without breaking return/goto semantics. + return false; } /** diff --git a/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java b/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java index f383c2aa4..1fbacdd0f 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java +++ b/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java @@ -81,6 +81,12 @@ public void scan(Node root) { if (state == 0) { String oper = op.operator; + if ("return".equals(oper)) { + if (DEBUG) System.err.println("ControlFlowDetector(scan): UNSAFE return at tokenIndex=" + op.tokenIndex); + hasUnsafeControlFlow = true; + continue; + } + if ("goto".equals(oper)) { if (allowedGotoLabels != null && op.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { Node arg = labelNode.elements.getFirst(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java index 4df66404d..0840d9dfe 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java @@ -29,14 +29,12 @@ public static int getLocalLevel() { * @param variable the dynamic state to be pushed onto the stack. */ public static RuntimeBase pushLocalVariable(RuntimeBase variable) { - // Save the current state of the variable and push it onto the stack. variable.dynamicSaveState(); variableStack.push(variable); return variable; } public static RuntimeScalar pushLocalVariable(RuntimeScalar variable) { - // Save the current state of the variable and push it onto the stack. variable.dynamicSaveState(); variableStack.push(variable); return variable; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java index d77cf5931..46e7ee5bb 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java @@ -2,6 +2,8 @@ import org.perlonjava.runtime.mro.InheritanceResolver; +import java.util.HashMap; +import java.util.Map; import java.util.function.Function; import static org.perlonjava.runtime.runtimetypes.RuntimeContextType.SCALAR; @@ -63,6 +65,7 @@ public class OverloadContext { * The fallback method handler */ final RuntimeScalar methodFallback; + private final Map resolvedMethods = new HashMap<>(); /** * Private constructor to create an OverloadContext instance. @@ -218,12 +221,14 @@ public RuntimeScalar tryOverloadFallback(RuntimeScalar runtimeScalar, String... * @return RuntimeScalar result from method execution, or null if method not found */ public RuntimeScalar tryOverload(String methodName, RuntimeArray perlMethodArgs) { - // Look for method in class hierarchy - RuntimeScalar perlMethod = InheritanceResolver.findMethodInHierarchy(methodName, perlClassName, null, 0); - if (perlMethod == null) { - return null; + if (resolvedMethods.containsKey(methodName)) { + RuntimeScalar perlMethod = resolvedMethods.get(methodName); + if (perlMethod == null) return null; + return RuntimeCode.apply(perlMethod, perlMethodArgs, SCALAR).getFirst(); } - // Execute found method with provided arguments + RuntimeScalar perlMethod = InheritanceResolver.findMethodInHierarchy(methodName, perlClassName, null, 0); + resolvedMethods.put(methodName, perlMethod); + if (perlMethod == null) return null; return RuntimeCode.apply(perlMethod, perlMethodArgs, SCALAR).getFirst(); } } From 98bff286697ac7c982839dd67f03d95aba93ec42 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 20:45:54 +0100 Subject: [PATCH 09/16] Fix interpreter HASH_SET and NOT opcodes for non-scalar registers HASH_SET was casting value register to RuntimeScalar without checking type, causing ClassCastException when a RuntimeList was in the register. NOT opcode similarly hardened. Also improved bytecode context display to show full 16-bit opcode values. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 64fe66c62..a8abb79bb 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -899,8 +899,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int valueReg = bytecode[pc++]; RuntimeHash hash = (RuntimeHash) registers[hashReg]; RuntimeScalar key = (RuntimeScalar) registers[keyReg]; - RuntimeScalar val = (RuntimeScalar) registers[valueReg]; - hash.put(key.toString(), val); // Convert key to String + RuntimeBase rawVal = registers[valueReg]; + RuntimeScalar val = (rawVal instanceof RuntimeScalar) ? (RuntimeScalar) rawVal : rawVal.scalar(); + hash.put(key.toString(), val); break; } @@ -2205,7 +2206,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c if (i == errorPc) { bcContext.append(" >>>"); } - bcContext.append(String.format(" %02X", bytecode[i] & 0xFF)); + bcContext.append(String.format(" %04X", bytecode[i] & 0xFFFF)); if (i == errorPc) { bcContext.append("<<<"); } @@ -2677,9 +2678,9 @@ private static int executeComparisons(int opcode, int[] bytecode, int pc, // Logical NOT: rd = !rs int rd = bytecode[pc++]; int rs = bytecode[pc++]; - RuntimeScalar val = (RuntimeScalar) registers[rs]; - registers[rd] = val.getBoolean() ? - RuntimeScalarCache.scalarFalse : RuntimeScalarCache.scalarTrue; + RuntimeBase val = registers[rs]; + boolean b = (val instanceof RuntimeScalar) ? ((RuntimeScalar) val).getBoolean() : val.scalar().getBoolean(); + registers[rd] = b ? RuntimeScalarCache.scalarFalse : RuntimeScalarCache.scalarTrue; return pc; } From 01433fccf7fe73f5159f7395b8b0dbb64a4e9c45 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 21:55:44 +0100 Subject: [PATCH 10/16] Fix undef $scalar, HASH_SET read-only, print return value, MY_SCALAR opcode Three interpreter bugs fixed plus a runtime fix: 1. undef $scalar in interpreter: was creating a new undef value and ignoring the operand. Now emits UNDEFINE_SCALAR opcode that calls .undefine() on the variable register in-place. 2. HASH_SET opcode stored values directly in hash, allowing RuntimeScalarReadOnly cached integers to be stored. Later += modifications failed with read-only error. Now wraps values in new RuntimeScalar() to ensure mutability. 3. print/say/printf return value: was returning byte count instead of 1 on success (Perl spec). Fixed in CustomFileChannel and PipeOutputChannel write() methods. 4. MY_SCALAR superinstruction (opcode 352): combines LOAD_UNDEF + SET_SCALAR for lexical scalar assignment. Prevents aliasing to read-only cached values. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 28 ++++++++++++- .../backend/bytecode/CompileAssignment.java | 33 ++++++--------- .../backend/bytecode/CompileOperator.java | 30 +++++++++---- .../backend/bytecode/InterpretedCode.java | 9 ++++ .../perlonjava/backend/bytecode/Opcodes.java | 8 ++++ .../runtime/io/CustomFileChannel.java | 4 +- .../runtime/io/PipeOutputChannel.java | 2 +- .../runtime/operators/Operator.java | 38 ++++++++++++++--- .../runtimetypes/RuntimeScalarReadOnly.java | 3 +- src/main/perl/lib/Digest/SHA.pm | 42 +++++++++---------- 10 files changed, 137 insertions(+), 60 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index a8abb79bb..5ebc50efe 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -409,6 +409,23 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.MY_SCALAR: { + // Mutable scalar assignment: allocate fresh scalar, then copy value + // Superinstruction for LOAD_UNDEF rd + SET_SCALAR rd rs + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + RuntimeScalar target = new RuntimeScalar(); + registers[rs].addToScalar(target); + registers[rd] = target; + break; + } + + case Opcodes.UNDEFINE_SCALAR: { + int rd = bytecode[pc++]; + ((RuntimeScalar) registers[rd]).undefine(); + break; + } + // ================================================================= // ARITHMETIC OPERATORS // ================================================================= @@ -901,7 +918,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeScalar key = (RuntimeScalar) registers[keyReg]; RuntimeBase rawVal = registers[valueReg]; RuntimeScalar val = (rawVal instanceof RuntimeScalar) ? (RuntimeScalar) rawVal : rawVal.scalar(); - hash.put(key.toString(), val); + hash.put(key.toString(), new RuntimeScalar(val)); break; } @@ -2985,6 +3002,15 @@ private static String formatInterpreterError(InterpretedCode code, int errorPc, .append(" line ").append(lineNumber) .append(" (pc=").append(errorPc).append("): ") .append(e.getMessage()); + // Debug: show opcode at error PC + if (errorPc >= 0 && errorPc < code.bytecode.length) { + int opAtError = code.bytecode[errorPc]; + sb.append(" [opcode=0x").append(String.format("%04X", opAtError)); + if (errorPc + 1 < code.bytecode.length) sb.append(" arg1=").append(code.bytecode[errorPc + 1]); + if (errorPc + 2 < code.bytecode.length) sb.append(" arg2=").append(code.bytecode[errorPc + 2]); + if (errorPc + 3 < code.bytecode.length) sb.append(" arg3=").append(code.bytecode[errorPc + 3]); + sb.append("]"); + } } else if (tokenIndex != null) { // We have token index but no errorUtil sb.append("Interpreter error in ").append(code.sourceName) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 19ba6fbed..103f5552c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -90,8 +90,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - // Move to variable register - bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emit(Opcodes.MY_SCALAR); bytecodeCompiler.emitReg(reg); bytecodeCompiler.emitReg(valueReg); @@ -234,8 +233,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - // Move to variable register - bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emit(Opcodes.MY_SCALAR); bytecodeCompiler.emitReg(reg); bytecodeCompiler.emitReg(valueReg); @@ -342,8 +340,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(varReg); bytecodeCompiler.emitReg(elemReg); } else { - // Regular variable - use MOVE - bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emit(Opcodes.MY_SCALAR); bytecodeCompiler.emitReg(varReg); bytecodeCompiler.emitReg(elemReg); } @@ -744,16 +741,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); } else { - // Regular lexical - create a fresh RuntimeScalar, then copy the value into it. - // LOAD_UNDEF allocates a new mutable RuntimeScalar in the target register; - // SET_SCALAR copies the source value into it. - // This avoids two bugs: - // - MOVE aliases constants from the pool, corrupting them on later mutation - // - SET_SCALAR alone modifies the existing object in-place, which breaks - // 'local' variable restoration when the register was shared - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(targetReg); - bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emit(Opcodes.MY_SCALAR); bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); } @@ -914,9 +902,9 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rhsListReg); bytecodeCompiler.emitReg(indexReg); - // Assign to variable + // Assign to variable — our variables use SET_SCALAR to preserve global alias if (sigil.equals("$")) { - bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emit(Opcodes.SET_SCALAR); bytecodeCompiler.emitReg(varReg); bytecodeCompiler.emitReg(elemReg); } else if (sigil.equals("@")) { @@ -1048,9 +1036,12 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, String varName = ((IdentifierNode) node.left).name; if (bytecodeCompiler.hasVariable(varName)) { - // Lexical variable - copy to its register int targetReg = bytecodeCompiler.getVariableRegister(varName); - bytecodeCompiler.emit(Opcodes.MOVE); + if (bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName)) { + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + } else { + bytecodeCompiler.emit(Opcodes.MY_SCALAR); + } bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); bytecodeCompiler.lastResultReg = targetReg; @@ -1553,7 +1544,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(elementReg); } else { - bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emit(Opcodes.MY_SCALAR); bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(elementReg); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 1f32a5dfd..04fa9c8c6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -904,13 +904,29 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.lastResultReg = rd; } else if (op.equals("undef")) { - // undef operator - returns undefined value - // Can be used standalone: undef - // Or with an operand to undef a variable: undef $x (not implemented yet) - int undefReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(undefReg); - bytecodeCompiler.lastResultReg = undefReg; + if (node.operand instanceof ListNode listNode && !listNode.elements.isEmpty()) { + Node elem = listNode.elements.get(0); + if (elem instanceof OperatorNode opNode && opNode.operator.equals("$")) { + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + elem.accept(bytecodeCompiler); + bytecodeCompiler.currentCallContext = savedContext; + int varReg = bytecodeCompiler.lastResultReg; + bytecodeCompiler.emit(Opcodes.UNDEFINE_SCALAR); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.lastResultReg = varReg; + } else { + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; + } + } else { + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; + } } else if (op.equals("unaryMinus")) { // Unary minus: -$x // Compile operand in scalar context (negation always produces a scalar) diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index b644bc460..163cb219c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1163,6 +1163,15 @@ public String disassemble() { rs = bytecode[pc++]; sb.append("SET_SCALAR r").append(rd).append(".set(r").append(rs).append(")\n"); break; + case Opcodes.MY_SCALAR: + rd = bytecode[pc++]; + rs = bytecode[pc++]; + sb.append("MY_SCALAR r").append(rd).append(" = new Scalar(r").append(rs).append(")\n"); + break; + case Opcodes.UNDEFINE_SCALAR: + rd = bytecode[pc++]; + sb.append("UNDEFINE_SCALAR r").append(rd).append(".undefine()\n"); + break; case Opcodes.NOT: rd = bytecode[pc++]; rs = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 315919082..11059b59b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1157,5 +1157,13 @@ public class Opcodes { * Format: LSTAT_LASTHANDLE rd ctx */ public static final short LSTAT_LASTHANDLE = 351; + /** Mutable scalar assignment: rd = new RuntimeScalar(); rd.set(rs) + * Superinstruction combining LOAD_UNDEF + SET_SCALAR for lexical scalar assignment. + * Format: MY_SCALAR rd rs */ + public static final short MY_SCALAR = 352; + + /** Undefine a scalar variable in-place: rd.undefine(). Used by `undef $x`. */ + public static final short UNDEFINE_SCALAR = 353; + private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java index 1fda20e8a..5d790cd6f 100644 --- a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java @@ -185,8 +185,8 @@ public RuntimeScalar write(String string) { data[i] = (byte) string.charAt(i); } ByteBuffer byteBuffer = ByteBuffer.wrap(data); - int bytesWritten = fileChannel.write(byteBuffer); - return new RuntimeScalar(bytesWritten); + fileChannel.write(byteBuffer); + return scalarTrue; } catch (IOException e) { return handleIOException(e, "write failed"); } diff --git a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java index 117056ddf..229b24db6 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java @@ -211,7 +211,7 @@ public RuntimeScalar write(String string) { process.getOutputStream().write(bytes); process.getOutputStream().flush(); - return new RuntimeScalar(bytes.length); + return scalarTrue; } catch (IOException e) { return handleIOException(e, "Write to pipe failed"); } diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index 7160304ba..d5b9a442d 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -206,10 +206,10 @@ public static RuntimeList split(RuntimeScalar quotedRegex, RuntimeList args, int } } } else { - // Treat quotedRegex as a literal string - String literalPattern = quotedRegex.toString(); + // In Perl, split with a string variable compiles the string as a regex + String regexPattern = quotedRegex.toString(); - if (literalPattern.isEmpty()) { + if (regexPattern.isEmpty()) { // Special case: if the pattern is an empty string, split between characters if (limit > 0) { for (int i = 0; i < inputStr.length() && splitElements.size() < limit - 1; i++) { @@ -224,9 +224,35 @@ public static RuntimeList split(RuntimeScalar quotedRegex, RuntimeList args, int } } } else { - String[] parts = inputStr.split(Pattern.quote(literalPattern), limit); - for (String part : parts) { - splitElements.add(new RuntimeScalar(part)); + Pattern pattern = Pattern.compile(regexPattern); + Matcher matcher = pattern.matcher(inputStr); + int lastEnd = 0; + int splitCount = 0; + + while (matcher.find() && (limit <= 0 || splitCount < limit - 1)) { + if (lastEnd == 0 && matcher.end() == 0) { + // Zero-width match at start never produces an empty field + } else if (matcher.start() == matcher.end() && matcher.start() == lastEnd) { + continue; + } else { + splitElements.add(new RuntimeScalar(inputStr.substring(lastEnd, matcher.start()))); + } + for (int i = 1; i <= matcher.groupCount(); i++) { + String group = matcher.group(i); + splitElements.add(group != null ? new RuntimeScalar(group) : scalarUndef); + } + lastEnd = matcher.end(); + splitCount++; + } + + if (lastEnd <= inputStr.length()) { + splitElements.add(new RuntimeScalar(inputStr.substring(lastEnd))); + } + + if (limit == 0) { + while (!splitElements.isEmpty() && splitElements.getLast().toString().isEmpty()) { + splitElements.removeLast(); + } } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java index 55e577b8f..84708f9f7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java @@ -84,7 +84,8 @@ public RuntimeScalarReadOnly(String s) { */ @Override void vivify() { - throw new RuntimeException("Modification of a read-only value attempted"); + RuntimeException ex = new RuntimeException("Modification of a read-only value attempted (value=" + this.toString() + " type=" + this.type + ")"); + throw ex; } /** diff --git a/src/main/perl/lib/Digest/SHA.pm b/src/main/perl/lib/Digest/SHA.pm index da11f3e7e..3dd1527fd 100644 --- a/src/main/perl/lib/Digest/SHA.pm +++ b/src/main/perl/lib/Digest/SHA.pm @@ -99,147 +99,147 @@ sub base64digest { # Functional interface implementations sub sha1 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('1'); $sha->add($data); return $sha->digest; } sub sha1_hex { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('1'); $sha->add($data); return $sha->hexdigest; } sub sha1_base64 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('1'); $sha->add($data); return $sha->b64digest; } sub sha224 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('224'); $sha->add($data); return $sha->digest; } sub sha224_hex { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('224'); $sha->add($data); return $sha->hexdigest; } sub sha224_base64 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('224'); $sha->add($data); return $sha->b64digest; } sub sha256 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('256'); $sha->add($data); return $sha->digest; } sub sha256_hex { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('256'); $sha->add($data); return $sha->hexdigest; } sub sha256_base64 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('256'); $sha->add($data); return $sha->b64digest; } sub sha384 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('384'); $sha->add($data); return $sha->digest; } sub sha384_hex { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('384'); $sha->add($data); return $sha->hexdigest; } sub sha384_base64 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('384'); $sha->add($data); return $sha->b64digest; } sub sha512 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512'); $sha->add($data); return $sha->digest; } sub sha512_hex { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512'); $sha->add($data); return $sha->hexdigest; } sub sha512_base64 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512'); $sha->add($data); return $sha->b64digest; } sub sha512224 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512224'); $sha->add($data); return $sha->digest; } sub sha512224_hex { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512224'); $sha->add($data); return $sha->hexdigest; } sub sha512224_base64 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512224'); $sha->add($data); return $sha->b64digest; } sub sha512256 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512256'); $sha->add($data); return $sha->digest; } sub sha512256_hex { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512256'); $sha->add($data); return $sha->hexdigest; } sub sha512256_base64 { - my $data = shift; + my $data = join('', @_); my $sha = Digest::SHA->new('512256'); $sha->add($data); return $sha->b64digest; From 62fb4f566b16b6c1f05c543c353392455b270c63 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 27 Feb 2026 22:50:48 +0100 Subject: [PATCH 11/16] Fix eval BLOCK and eval STRING in bytecode interpreter - Fix eval BLOCK exception handling: jump to catch PC instead of returning from interpreter, so code after eval continues executing - Fix eval BLOCK compilation: skip CALL_SUB for inlined eval blocks (eval { BLOCK } is parsed as sub { ... }->() with useTryCatch flag, but visitEvalBlock() already inlines the body) - Fix eval STRING variable scope: per-eval-site variable registry snapshots prevent register collisions when same variable name is declared in multiple scopes - Fix local our $VAR compilation: handle 'our' as intermediate operand in local() declarations Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 41 +++++++++++++++++++ .../backend/bytecode/BytecodeInterpreter.java | 21 +++++----- .../bytecode/CompileBinaryOperator.java | 8 ++++ .../backend/bytecode/CompileOperator.java | 4 ++ .../backend/bytecode/EvalStringHandler.java | 35 ++++++++++++---- .../backend/bytecode/InterpretedCode.java | 6 ++- .../backend/bytecode/SlowOpcodeHandler.java | 33 +++++++-------- 7 files changed, 110 insertions(+), 38 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 6fc14bfcf..feda888ff 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -103,6 +103,9 @@ private static class LoopInfo { // This is needed because inner scopes get popped before variableRegistry is built final Map allDeclaredVariables = new HashMap<>(); + // Per-eval-site variable scope snapshots (for eval STRING variable capture) + final List> evalSiteRegistries = new ArrayList<>(); + // BEGIN support for named subroutine closures private int currentSubroutineBeginId = 0; // BEGIN ID for current named subroutine (0 = not in named sub) private Set currentSubroutineClosureVars = new HashSet<>(); // Variables captured from outer scope @@ -269,6 +272,16 @@ private void exitScope() { } } + int snapshotEvalSiteRegistry() { + Map snapshot = new HashMap<>(); + for (Map scope : variableScopes) { + snapshot.putAll(scope); + } + int index = evalSiteRegistries.size(); + evalSiteRegistries.add(snapshot); + return index; + } + /** * Helper: Get current package name for global variable resolution. * Uses symbolTable for proper package/class tracking. @@ -569,6 +582,9 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { ); result.containsRegex = FindDeclarationVisitor.findOperator(node, "matchRegex") != null || FindDeclarationVisitor.findOperator(node, "replaceRegex") != null; + if (!evalSiteRegistries.isEmpty()) { + result.evalSiteRegistries = evalSiteRegistries; + } return result; } @@ -2768,6 +2784,31 @@ void compileVariableDeclaration(OperatorNode node, String op) { return; } + // Handle local our $x - compile `our` declaration then localize + if (sigil.equals("our")) { + OperatorNode ourNode = new OperatorNode("our", sigilOp.operand, node.getIndex()); + compileVariableDeclaration(ourNode, "our"); + + if (sigilOp.operand instanceof OperatorNode innerSigilOp + && innerSigilOp.operand instanceof IdentifierNode idNode) { + String innerSigil = innerSigilOp.operator; + String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); + int nameIdx = addToStringPool(globalVarName); + int rd = allocateRegister(); + if (innerSigil.equals("$")) { + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + } else if (innerSigil.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + } else { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + } + emitReg(rd); + emit(nameIdx); + lastResultReg = rd; + } + return; + } + // Handle local \$x or local \\$x - backslash operator if (sigil.equals("\\")) { boolean isDeclaredReference = node.annotations != null && diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 5ebc50efe..c724000b7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -75,6 +75,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // When exception occurs, pop from stack and jump to catch PC java.util.Stack evalCatchStack = new java.util.Stack<>(); + try { + outer: + while (true) { try { // Main dispatch loop - JVM JIT optimizes switch to tableswitch (O(1) jump) while (pc < bytecode.length) { @@ -2208,9 +2211,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Special handling for ClassCastException to show which opcode is failing // Check if we're inside an eval block first if (!evalCatchStack.isEmpty()) { - evalCatchStack.pop(); + int catchPc = evalCatchStack.pop(); WarnDie.catchEval(e); - return new RuntimeList(); + pc = catchPc; + continue outer; } // Not in eval - show detailed error with bytecode context @@ -2235,15 +2239,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } catch (Throwable e) { // Check if we're inside an eval block if (!evalCatchStack.isEmpty()) { - // Inside eval block - catch the exception - evalCatchStack.pop(); // Pop the catch handler - - // Call WarnDie.catchEval() to set $@ + int catchPc = evalCatchStack.pop(); WarnDie.catchEval(e); - - // Eval block failed - return empty list - // (The result will be undef in scalar context, empty in list context) - return new RuntimeList(); + pc = catchPc; + continue outer; } // Not in eval block - propagate exception @@ -2264,6 +2263,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Wrap other exceptions with interpreter context including bytecode context String errorMessage = formatInterpreterError(code, pc, e); throw new RuntimeException(errorMessage, e); + } + } // end outer while } finally { if (regexLocalLevel >= 0) { DynamicVariableManager.popToLocalLevel(regexLocalLevel); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 576357ec7..b4a24b76c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -246,6 +246,14 @@ static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperato // Code reference call: $code->() or $code->(@args) // right is ListNode with arguments else if (node.right instanceof ListNode) { + // eval { BLOCK } is parsed as sub { ... }->() with useTryCatch. + // visitEvalBlock() already inlined the body and set lastResultReg + // to the block result, so skip the CALL_SUB. + if (node.left instanceof SubroutineNode && ((SubroutineNode) node.left).useTryCatch) { + node.left.accept(bytecodeCompiler); + return; + } + // This is a code reference call: $coderef->(args) // Compile the code reference in scalar context int savedContext = bytecodeCompiler.currentCallContext; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 04fa9c8c6..833a8d950 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -855,6 +855,9 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); + // Snapshot current variable scopes for this eval site + int evalSiteIdx = bytecodeCompiler.snapshotEvalSiteRegistry(); + // Emit direct opcode EVAL_STRING bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); @@ -863,6 +866,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // wantarray() inside the eval body and the eval return value follow // the correct context even when the surrounding sub is VOID. bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + bytecodeCompiler.emit(evalSiteIdx); bytecodeCompiler.lastResultReg = rd; } else { diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 6259c44b0..83dcffd95 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -61,7 +61,17 @@ public static RuntimeScalar evalString(String perlCode, String sourceName, int sourceLine, int callContext) { - return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext).scalar(); + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, null).scalar(); + } + + public static RuntimeScalar evalString(String perlCode, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext, + Map siteRegistry) { + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, siteRegistry).scalar(); } public static RuntimeList evalStringList(String perlCode, @@ -70,6 +80,16 @@ public static RuntimeList evalStringList(String perlCode, String sourceName, int sourceLine, int callContext) { + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, null); + } + + public static RuntimeList evalStringList(String perlCode, + InterpretedCode currentCode, + RuntimeBase[] registers, + String sourceName, + int sourceLine, + int callContext, + Map siteRegistry) { try { evalTrace("EvalStringHandler enter ctx=" + callContext + " srcName=" + sourceName + " srcLine=" + sourceLine + " codeLen=" + (perlCode != null ? perlCode.length() : -1)); @@ -128,20 +148,20 @@ public static RuntimeList evalStringList(String perlCode, RuntimeBase[] capturedVars = new RuntimeBase[0]; Map adjustedRegistry = null; - if (currentCode != null && currentCode.variableRegistry != null && registers != null) { + // Use per-eval-site registry if available (correct scoping), else fall back to global + Map sourceRegistry = (siteRegistry != null) ? siteRegistry + : (currentCode != null ? currentCode.variableRegistry : null); + + if (sourceRegistry != null && registers != null) { - // Sort parent variables by register index for consistent ordering List> sortedVars = new ArrayList<>( - currentCode.variableRegistry.entrySet() + sourceRegistry.entrySet() ); sortedVars.sort(Map.Entry.comparingByValue()); - // Build capturedVars array and adjusted registry - // Captured variables will be placed at registers 3+ in eval'd code List capturedList = new ArrayList<>(); adjustedRegistry = new HashMap<>(); - // Always include reserved registers in adjusted registry adjustedRegistry.put("this", 0); adjustedRegistry.put("@_", 1); adjustedRegistry.put("wantarray", 2); @@ -151,7 +171,6 @@ public static RuntimeList evalStringList(String perlCode, String varName = entry.getKey(); int parentRegIndex = entry.getValue(); - // Skip reserved registers (they're handled separately in interpreter) if (parentRegIndex < 3) { continue; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 163cb219c..d58face7e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -3,6 +3,7 @@ import org.perlonjava.runtime.runtimetypes.*; import java.util.BitSet; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -36,6 +37,7 @@ public class InterpretedCode extends RuntimeCode { public final String compilePackage; // Package at compile time (for eval STRING name resolution) public boolean containsRegex; // Whether this code contains regex ops (for match var scoping) + public List> evalSiteRegistries; // Per-eval-site variable snapshots // Debug information (optional) public final String sourceName; // Source file name (for stack traces) @@ -1323,7 +1325,9 @@ public String disassemble() { case Opcodes.EVAL_STRING: rd = bytecode[pc++]; rs = bytecode[pc++]; - sb.append("EVAL_STRING r").append(rd).append(" = eval(r").append(rs).append(")\n"); + int evalCtx = bytecode[pc++]; + int evalSiteId = bytecode[pc++]; + sb.append("EVAL_STRING r").append(rd).append(" = eval(r").append(rs).append(", ctx=").append(evalCtx).append(", site=").append(evalSiteId).append(")\n"); break; case Opcodes.SELECT_OP: rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index e2a553861..d23de55c1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -7,6 +7,8 @@ import org.perlonjava.runtime.operators.Time; import org.perlonjava.runtime.runtimetypes.*; +import java.util.Map; + /** * Handler for rarely-used operations called directly by BytecodeInterpreter. * @@ -271,12 +273,19 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; int evalCallContext = RuntimeContextType.SCALAR; - // Newer bytecode encodes the eval operator's own call context (VOID/SCALAR/LIST) - // so eval semantics are correct even when the surrounding statement is compiled - // in VOID context. if (pc < bytecode.length) { evalCallContext = bytecode[pc++]; } + int evalSiteIdx = -1; + if (pc < bytecode.length) { + evalSiteIdx = bytecode[pc++]; + } + + // Resolve per-eval-site variable registry if available + Map siteRegistry = null; + if (evalSiteIdx >= 0 && code.evalSiteRegistries != null && evalSiteIdx < code.evalSiteRegistries.size()) { + siteRegistry = code.evalSiteRegistries.get(evalSiteIdx); + } // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -305,28 +314,14 @@ public static int executeEvalString( } if (callContext == RuntimeContextType.LIST) { - // Return list context result RuntimeList result = EvalStringHandler.evalStringList( - perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access - code.sourceName, - code.sourceLine, - callContext - ); + perlCode, code, registers, code.sourceName, code.sourceLine, callContext, siteRegistry); registers[rd] = result; evalTrace("EVAL_STRING opcode exit LIST stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + " scalar=" + result.scalar().toString()); } else { - // Scalar/void context: return scalar result RuntimeScalar result = EvalStringHandler.evalString( - perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access - code.sourceName, - code.sourceLine, - callContext - ); + perlCode, code, registers, code.sourceName, code.sourceLine, callContext, siteRegistry); registers[rd] = result; evalTrace("EVAL_STRING opcode exit SCALAR/VOID stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + " val=" + result.toString() + " bool=" + result.getBoolean()); From 6b7091dc70221802a64ec21de711d22948c6c422 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 09:21:56 +0100 Subject: [PATCH 12/16] Add labeled block support for interpreter and fix split args casting - Emit PUSH_LABELED_BLOCK/POP_LABELED_BLOCK opcodes for labeled bare blocks (For3Node.isSimpleBlock with labelName) in BytecodeCompiler - Handle labeled block stack in interpreter for CALL_SUB and CALL_METHOD, matching RuntimeControlFlowList labels before propagating - Fix split args ClassCastException when args register contains RuntimeScalar - Add disassembly support for new opcodes - Merge master changes Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/import-perl5/config.yaml | 9 - .../backend/bytecode/BytecodeCompiler.java | 122 ++---- .../backend/bytecode/BytecodeInterpreter.java | 159 +++---- .../backend/bytecode/CompileAssignment.java | 33 +- .../bytecode/CompileBinaryOperator.java | 226 +++++----- .../backend/bytecode/CompileOperator.java | 79 +--- .../backend/bytecode/EvalStringHandler.java | 35 +- .../backend/bytecode/InterpretedCode.java | 26 +- .../bytecode/OpcodeHandlerExtended.java | 14 - .../perlonjava/backend/bytecode/Opcodes.java | 8 + .../backend/bytecode/SlowOpcodeHandler.java | 38 +- .../perlonjava/backend/jvm/Dereference.java | 13 +- .../perlonjava/backend/jvm/EmitForeach.java | 23 - .../perlonjava/backend/jvm/EmitOperator.java | 16 +- .../backend/jvm/EmitterMethodCreator.java | 15 - .../org/perlonjava/backend/jvm/Local.java | 38 +- .../jvm/astrefactor/LargeBlockRefactorer.java | 7 +- .../analysis/ControlFlowDetectorVisitor.java | 6 - .../frontend/parser/OperatorParser.java | 20 +- .../frontend/parser/ParseMapGrepSort.java | 25 -- .../frontend/parser/PrototypeArgs.java | 2 +- .../frontend/parser/SignatureParser.java | 4 +- .../runtime/io/CustomFileChannel.java | 14 +- .../runtime/io/InternalPipeHandle.java | 10 +- .../runtime/io/PipeInputChannel.java | 10 +- .../runtime/io/PipeOutputChannel.java | 2 +- .../org/perlonjava/runtime/io/StandardIO.java | 10 +- .../runtime/mro/InheritanceResolver.java | 22 +- .../runtime/operators/FileTestOperator.java | 399 +++++++++--------- .../runtime/operators/IOOperator.java | 18 +- .../runtime/operators/MathOperators.java | 3 - .../runtime/operators/Operator.java | 44 +- .../runtime/operators/Readline.java | 3 - .../perlonjava/runtime/operators/Stat.java | 113 +++-- .../runtime/operators/StringOperators.java | 127 +++--- .../perlonjava/runtime/operators/Time.java | 23 +- .../unpack/NumericFormatHandler.java | 54 +-- .../runtime/perlmodule/DigestMD5.java | 4 +- .../runtime/perlmodule/DigestSHA.java | 4 +- .../perlonjava/runtime/perlmodule/Encode.java | 2 +- .../regex/RegexPreprocessorHelper.java | 12 +- .../runtime/regex/RuntimeRegex.java | 37 +- .../runtimetypes/DynamicVariableManager.java | 7 +- .../runtime/runtimetypes/OverloadContext.java | 15 +- .../runtime/runtimetypes/RuntimeCode.java | 36 +- .../runtime/runtimetypes/RuntimeIO.java | 8 - .../runtime/runtimetypes/RuntimeList.java | 9 - .../runtimetypes/RuntimeRegexState.java | 25 -- .../runtime/runtimetypes/RuntimeScalar.java | 6 - .../runtimetypes/RuntimeScalarReadOnly.java | 3 +- .../runtimetypes/RuntimeSubstrLvalue.java | 9 +- .../runtimetypes/ScalarSpecialVariable.java | 10 +- src/main/perl/lib/Digest/SHA.pm | 42 +- src/main/perl/lib/Time/Local.pm | 162 +++---- 54 files changed, 866 insertions(+), 1295 deletions(-) diff --git a/dev/import-perl5/config.yaml b/dev/import-perl5/config.yaml index 61eca74ad..e8df5dd7e 100644 --- a/dev/import-perl5/config.yaml +++ b/dev/import-perl5/config.yaml @@ -400,15 +400,6 @@ 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/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index feda888ff..dc4c94669 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1,6 +1,5 @@ package org.perlonjava.backend.bytecode; -import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.analysis.Visitor; import org.perlonjava.backend.jvm.EmitterMethodCreator; import org.perlonjava.backend.jvm.EmitterContext; @@ -69,10 +68,6 @@ private static class LoopInfo { } } - // goto LABEL support: track label positions and pending forward goto patches - private final Map gotoLabelPCs = new HashMap<>(); // label name → PC - private final Map> pendingGotoPatches = new HashMap<>(); // label name → list of PCs to patch - // Token index tracking for error reporting private final TreeMap pcToTokenIndex = new TreeMap<>(); int currentTokenIndex = -1; // Track current token for error reporting @@ -103,9 +98,6 @@ private static class LoopInfo { // This is needed because inner scopes get popped before variableRegistry is built final Map allDeclaredVariables = new HashMap<>(); - // Per-eval-site variable scope snapshots (for eval STRING variable capture) - final List> evalSiteRegistries = new ArrayList<>(); - // BEGIN support for named subroutine closures private int currentSubroutineBeginId = 0; // BEGIN ID for current named subroutine (0 = not in named sub) private Set currentSubroutineClosureVars = new HashSet<>(); // Variables captured from outer scope @@ -272,16 +264,6 @@ private void exitScope() { } } - int snapshotEvalSiteRegistry() { - Map snapshot = new HashMap<>(); - for (Map scope : variableScopes) { - snapshot.putAll(scope); - } - int index = evalSiteRegistries.size(); - evalSiteRegistries.add(snapshot); - return index; - } - /** * Helper: Get current package name for global variable resolution. * Uses symbolTable for proper package/class tracking. @@ -511,7 +493,12 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Use the calling context from EmitterContext for top-level expressions // This is crucial for eval STRING to propagate context correctly currentCallContext = ctx.contextType; - + // Inherit package from the JVM compiler context so unqualified sub calls + // resolve in the correct package (not main) + if (ctx.symbolTable != null) { + symbolTable.setCurrentPackage(ctx.symbolTable.getCurrentPackage(), + ctx.symbolTable.currentPackageIsClass()); + } } // If we have captured variables, allocate registers for them @@ -564,7 +551,7 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { } // Build InterpretedCode - InterpretedCode result = new InterpretedCode( + return new InterpretedCode( toShortArray(), constants.toArray(), stringPool.toArray(new String[0]), @@ -580,12 +567,6 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { warningFlags, // Warning flags for eval STRING inheritance symbolTable.getCurrentPackage() // Compile-time package for eval STRING name resolution ); - result.containsRegex = FindDeclarationVisitor.findOperator(node, "matchRegex") != null - || FindDeclarationVisitor.findOperator(node, "replaceRegex") != null; - if (!evalSiteRegistries.isEmpty()) { - result.evalSiteRegistries = evalSiteRegistries; - } - return result; } // ========================================================================= @@ -2784,31 +2765,6 @@ void compileVariableDeclaration(OperatorNode node, String op) { return; } - // Handle local our $x - compile `our` declaration then localize - if (sigil.equals("our")) { - OperatorNode ourNode = new OperatorNode("our", sigilOp.operand, node.getIndex()); - compileVariableDeclaration(ourNode, "our"); - - if (sigilOp.operand instanceof OperatorNode innerSigilOp - && innerSigilOp.operand instanceof IdentifierNode idNode) { - String innerSigil = innerSigilOp.operator; - String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); - int nameIdx = addToStringPool(globalVarName); - int rd = allocateRegister(); - if (innerSigil.equals("$")) { - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - } else if (innerSigil.equals("@")) { - emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); - } else { - emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); - } - emitReg(rd); - emit(nameIdx); - lastResultReg = rd; - } - return; - } - // Handle local \$x or local \\$x - backslash operator if (sigil.equals("\\")) { boolean isDeclaredReference = node.annotations != null && @@ -4152,12 +4108,6 @@ public void visit(For1Node node) { variableScopes.peek().put(varName, varReg); allDeclaredVariables.put(varName, varReg); } - } else if (varOp.operator.equals("$") && varOp.operand instanceof IdentifierNode - && globalLoopVarName == null) { - String varName = "$" + ((IdentifierNode) varOp.operand).name; - if (hasVariable(varName)) { - variableScopes.peek().put(varName, varReg); - } } } @@ -4253,6 +4203,17 @@ public void visit(For3Node node) { if (currentCallContext != RuntimeContextType.VOID) { outerResultReg = allocateRegister(); } + + int labelIdx = -1; + int exitPcPlaceholder = -1; + if (node.labelName != null) { + labelIdx = addToStringPool(node.labelName); + emit(Opcodes.PUSH_LABELED_BLOCK); + emit(labelIdx); + exitPcPlaceholder = bytecode.size(); + emitInt(0); + } + enterScope(); try { // Just execute the body once, no loop @@ -4269,6 +4230,13 @@ public void visit(For3Node node) { // Exit scope to clean up lexical variables exitScope(); } + + if (node.labelName != null) { + emit(Opcodes.POP_LABELED_BLOCK); + int exitPc = bytecode.size(); + patchJump(exitPcPlaceholder, exitPc); + } + lastResultReg = outerResultReg; return; } @@ -4528,17 +4496,7 @@ public void visit(TryNode node) { @Override public void visit(LabelNode node) { - int labelPc = bytecode.size(); - gotoLabelPCs.put(node.label, labelPc); - - // Patch any pending forward gotos to this label - List patches = pendingGotoPatches.remove(node.label); - if (patches != null) { - for (int patchPc : patches) { - patchJump(patchPc, labelPc); - } - } - + // Labels are tracked in loops, standalone labels are no-ops lastResultReg = -1; } @@ -4791,32 +4749,4 @@ void handleLoopControlOperator(OperatorNode node, String op) { targetLoop.redoPcs.add(patchPc); } } - - void handleGotoOperator(OperatorNode node) { - String labelStr = null; - if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { - Node arg = labelNode.elements.getFirst(); - if (arg instanceof IdentifierNode) { - labelStr = ((IdentifierNode) arg).name; - } - } - - if (labelStr == null) { - throwCompilerException("goto without label is not supported", node.getIndex()); - return; - } - - // Check if label was already seen (backward goto) - Integer targetPc = gotoLabelPCs.get(labelStr); - if (targetPc != null) { - emit(Opcodes.GOTO); - emitInt(targetPc); - } else { - // Forward goto: emit GOTO with placeholder, patch later - emit(Opcodes.GOTO); - int patchPc = bytecode.size(); - emitInt(0); - pendingGotoPatches.computeIfAbsent(labelStr, k -> new ArrayList<>()).add(patchPc); - } - } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index c724000b7..5114103da 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -48,12 +48,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c String frameSubName = subroutineName != null ? subroutineName : (code.subName != null ? code.subName : "(eval)"); InterpreterState.push(code, framePackageName, frameSubName); - int regexLocalLevel = -1; - if (code.containsRegex) { - regexLocalLevel = DynamicVariableManager.getLocalLevel(); - RuntimeRegexState.pushLocal(); - } - // Pure register file (NOT stack-based - matches compiler for control flow correctness) RuntimeBase[] registers = new RuntimeBase[code.maxRegisters]; @@ -75,9 +69,12 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // When exception occurs, pop from stack and jump to catch PC java.util.Stack evalCatchStack = new java.util.Stack<>(); - try { - outer: - while (true) { + // Labeled block stack for non-local last/next/redo handling. + // When a function call returns a RuntimeControlFlowList, we check this stack + // to see if the label matches an enclosing labeled block. + java.util.Stack labeledBlockStack = new java.util.Stack<>(); + // Each entry is [labelStringPoolIdx, exitPc] + try { // Main dispatch loop - JVM JIT optimizes switch to tableswitch (O(1) jump) while (pc < bytecode.length) { @@ -97,16 +94,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; case Opcodes.RETURN: { + // Return from subroutine: return rd int retReg = bytecode[pc++]; RuntimeBase retVal = registers[retReg]; + if (retVal == null) { return new RuntimeList(); } - RuntimeList retList = retVal.getList(); - if (code.containsRegex) { - RuntimeList.resolveMatchProxies(retList); - } - return retList; + return retVal.getList(); } case Opcodes.GOTO: { @@ -412,23 +407,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } - case Opcodes.MY_SCALAR: { - // Mutable scalar assignment: allocate fresh scalar, then copy value - // Superinstruction for LOAD_UNDEF rd + SET_SCALAR rd rs - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - RuntimeScalar target = new RuntimeScalar(); - registers[rs].addToScalar(target); - registers[rd] = target; - break; - } - - case Opcodes.UNDEFINE_SCALAR: { - int rd = bytecode[pc++]; - ((RuntimeScalar) registers[rd]).undefine(); - break; - } - // ================================================================= // ARITHMETIC OPERATORS // ================================================================= @@ -919,9 +897,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int valueReg = bytecode[pc++]; RuntimeHash hash = (RuntimeHash) registers[hashReg]; RuntimeScalar key = (RuntimeScalar) registers[keyReg]; - RuntimeBase rawVal = registers[valueReg]; - RuntimeScalar val = (rawVal instanceof RuntimeScalar) ? (RuntimeScalar) rawVal : rawVal.scalar(); - hash.put(key.toString(), new RuntimeScalar(val)); + RuntimeScalar val = (RuntimeScalar) registers[valueReg]; + hash.put(key.toString(), val); // Convert key to String break; } @@ -1010,8 +987,25 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Check for control flow (last/next/redo/goto/tail-call) if (result.isNonLocalGoto()) { - // Propagate control flow up the call stack - return result; + RuntimeControlFlowList flow = (RuntimeControlFlowList) result; + // Check labeled block stack for a matching label + boolean handled = false; + for (int i = labeledBlockStack.size() - 1; i >= 0; i--) { + int[] entry = labeledBlockStack.get(i); + String blockLabel = code.stringPool[entry[0]]; + if (flow.matchesLabel(blockLabel)) { + // Pop entries down to and including the match + while (labeledBlockStack.size() > i) { + labeledBlockStack.pop(); + } + pc = entry[1]; // jump to block exit + handled = true; + break; + } + } + if (!handled) { + return result; + } } break; } @@ -1055,8 +1049,23 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Check for control flow (last/next/redo/goto/tail-call) if (result.isNonLocalGoto()) { - // Propagate control flow up the call stack - return result; + RuntimeControlFlowList flow = (RuntimeControlFlowList) result; + boolean handled = false; + for (int i = labeledBlockStack.size() - 1; i >= 0; i--) { + int[] entry = labeledBlockStack.get(i); + String blockLabel = code.stringPool[entry[0]]; + if (flow.matchesLabel(blockLabel)) { + while (labeledBlockStack.size() > i) { + labeledBlockStack.pop(); + } + pc = entry[1]; + handled = true; + break; + } + } + if (!handled) { + return result; + } } break; } @@ -1273,14 +1282,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = OpcodeHandlerExtended.executeLstat(bytecode, pc, registers); break; - case Opcodes.STAT_LASTHANDLE: - pc = OpcodeHandlerExtended.executeStatLastHandle(bytecode, pc, registers); - break; - - case Opcodes.LSTAT_LASTHANDLE: - pc = OpcodeHandlerExtended.executeLstatLastHandle(bytecode, pc, registers); - break; - // File test operations (opcodes 190-216) - delegated to handler case Opcodes.FILETEST_R: case Opcodes.FILETEST_W: @@ -1547,6 +1548,25 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + // ================================================================= + // LABELED BLOCK SUPPORT + // ================================================================= + + case Opcodes.PUSH_LABELED_BLOCK: { + int labelIdx = bytecode[pc++]; + int exitPc = readInt(bytecode, pc); + pc += 1; + labeledBlockStack.push(new int[]{labelIdx, exitPc}); + break; + } + + case Opcodes.POP_LABELED_BLOCK: { + if (!labeledBlockStack.isEmpty()) { + labeledBlockStack.pop(); + } + break; + } + // ================================================================= // LIST OPERATIONS // ================================================================= @@ -1739,7 +1759,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeList list = listBase.getList(); RuntimeScalar closure = (RuntimeScalar) registers[closureReg]; RuntimeList result = ListOperators.grep(list, closure, ctx); - registers[rd] = result; + + // In scalar context, return the count of elements + if (ctx == RuntimeContextType.SCALAR) { + registers[rd] = new RuntimeScalar(result.elements.size()); + } else { + registers[rd] = result; + } break; } @@ -2211,10 +2237,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Special handling for ClassCastException to show which opcode is failing // Check if we're inside an eval block first if (!evalCatchStack.isEmpty()) { - int catchPc = evalCatchStack.pop(); + evalCatchStack.pop(); WarnDie.catchEval(e); - pc = catchPc; - continue outer; + return new RuntimeList(); } // Not in eval - show detailed error with bytecode context @@ -2227,7 +2252,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c if (i == errorPc) { bcContext.append(" >>>"); } - bcContext.append(String.format(" %04X", bytecode[i] & 0xFFFF)); + bcContext.append(String.format(" %02X", bytecode[i] & 0xFF)); if (i == errorPc) { bcContext.append("<<<"); } @@ -2239,10 +2264,15 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } catch (Throwable e) { // Check if we're inside an eval block if (!evalCatchStack.isEmpty()) { - int catchPc = evalCatchStack.pop(); + // Inside eval block - catch the exception + evalCatchStack.pop(); // Pop the catch handler + + // Call WarnDie.catchEval() to set $@ WarnDie.catchEval(e); - pc = catchPc; - continue outer; + + // Eval block failed - return empty list + // (The result will be undef in scalar context, empty in list context) + return new RuntimeList(); } // Not in eval block - propagate exception @@ -2263,12 +2293,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Wrap other exceptions with interpreter context including bytecode context String errorMessage = formatInterpreterError(code, pc, e); throw new RuntimeException(errorMessage, e); - } - } // end outer while } finally { - if (regexLocalLevel >= 0) { - DynamicVariableManager.popToLocalLevel(regexLocalLevel); - } + // Always pop the interpreter state InterpreterState.pop(); } } @@ -2696,9 +2722,9 @@ private static int executeComparisons(int opcode, int[] bytecode, int pc, // Logical NOT: rd = !rs int rd = bytecode[pc++]; int rs = bytecode[pc++]; - RuntimeBase val = registers[rs]; - boolean b = (val instanceof RuntimeScalar) ? ((RuntimeScalar) val).getBoolean() : val.scalar().getBoolean(); - registers[rd] = b ? RuntimeScalarCache.scalarFalse : RuntimeScalarCache.scalarTrue; + RuntimeScalar val = (RuntimeScalar) registers[rs]; + registers[rd] = val.getBoolean() ? + RuntimeScalarCache.scalarFalse : RuntimeScalarCache.scalarTrue; return pc; } @@ -3003,15 +3029,6 @@ private static String formatInterpreterError(InterpretedCode code, int errorPc, .append(" line ").append(lineNumber) .append(" (pc=").append(errorPc).append("): ") .append(e.getMessage()); - // Debug: show opcode at error PC - if (errorPc >= 0 && errorPc < code.bytecode.length) { - int opAtError = code.bytecode[errorPc]; - sb.append(" [opcode=0x").append(String.format("%04X", opAtError)); - if (errorPc + 1 < code.bytecode.length) sb.append(" arg1=").append(code.bytecode[errorPc + 1]); - if (errorPc + 2 < code.bytecode.length) sb.append(" arg2=").append(code.bytecode[errorPc + 2]); - if (errorPc + 3 < code.bytecode.length) sb.append(" arg3=").append(code.bytecode[errorPc + 3]); - sb.append("]"); - } } else if (tokenIndex != null) { // We have token index but no errorUtil sb.append("Interpreter error in ").append(code.sourceName) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 103f5552c..19ba6fbed 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -90,7 +90,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - bytecodeCompiler.emit(Opcodes.MY_SCALAR); + // Move to variable register + bytecodeCompiler.emit(Opcodes.MOVE); bytecodeCompiler.emitReg(reg); bytecodeCompiler.emitReg(valueReg); @@ -233,7 +234,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - bytecodeCompiler.emit(Opcodes.MY_SCALAR); + // Move to variable register + bytecodeCompiler.emit(Opcodes.MOVE); bytecodeCompiler.emitReg(reg); bytecodeCompiler.emitReg(valueReg); @@ -340,7 +342,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(varReg); bytecodeCompiler.emitReg(elemReg); } else { - bytecodeCompiler.emit(Opcodes.MY_SCALAR); + // Regular variable - use MOVE + bytecodeCompiler.emit(Opcodes.MOVE); bytecodeCompiler.emitReg(varReg); bytecodeCompiler.emitReg(elemReg); } @@ -741,7 +744,16 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); } else { - bytecodeCompiler.emit(Opcodes.MY_SCALAR); + // Regular lexical - create a fresh RuntimeScalar, then copy the value into it. + // LOAD_UNDEF allocates a new mutable RuntimeScalar in the target register; + // SET_SCALAR copies the source value into it. + // This avoids two bugs: + // - MOVE aliases constants from the pool, corrupting them on later mutation + // - SET_SCALAR alone modifies the existing object in-place, which breaks + // 'local' variable restoration when the register was shared + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emit(Opcodes.SET_SCALAR); bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); } @@ -902,9 +914,9 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rhsListReg); bytecodeCompiler.emitReg(indexReg); - // Assign to variable — our variables use SET_SCALAR to preserve global alias + // Assign to variable if (sigil.equals("$")) { - bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emit(Opcodes.MOVE); bytecodeCompiler.emitReg(varReg); bytecodeCompiler.emitReg(elemReg); } else if (sigil.equals("@")) { @@ -1036,12 +1048,9 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, String varName = ((IdentifierNode) node.left).name; if (bytecodeCompiler.hasVariable(varName)) { + // Lexical variable - copy to its register int targetReg = bytecodeCompiler.getVariableRegister(varName); - if (bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName)) { - bytecodeCompiler.emit(Opcodes.SET_SCALAR); - } else { - bytecodeCompiler.emit(Opcodes.MY_SCALAR); - } + bytecodeCompiler.emit(Opcodes.MOVE); bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(valueReg); bytecodeCompiler.lastResultReg = targetReg; @@ -1544,7 +1553,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(elementReg); } else { - bytecodeCompiler.emit(Opcodes.MY_SCALAR); + bytecodeCompiler.emit(Opcodes.MOVE); bytecodeCompiler.emitReg(targetReg); bytecodeCompiler.emitReg(elementReg); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index b4a24b76c..98010eb1e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -1,44 +1,9 @@ package org.perlonjava.backend.bytecode; -import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.astnode.*; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; public class CompileBinaryOperator { - - private static class DeclRewrite { - final OperatorNode declaration; - final String savedOperator; - final Node savedOperand; - - DeclRewrite(OperatorNode declaration, String savedOperator, Node savedOperand) { - this.declaration = declaration; - this.savedOperator = savedOperator; - this.savedOperand = savedOperand; - } - - void restore() { - declaration.operator = savedOperator; - declaration.operand = savedOperand; - } - } - - private static DeclRewrite extractMyDeclaration(BytecodeCompiler bc, Node rightNode) { - OperatorNode decl = FindDeclarationVisitor.findOperator(rightNode, "my"); - if (decl != null && decl.operand instanceof OperatorNode innerOp) { - String savedOp = decl.operator; - Node savedOperand = decl.operand; - int savedCtx = bc.currentCallContext; - bc.currentCallContext = RuntimeContextType.VOID; - decl.accept(bc); - bc.currentCallContext = savedCtx; - decl.operator = innerOp.operator; - decl.operand = innerOp.operand; - return new DeclRewrite(decl, savedOp, savedOperand); - } - return null; - } - static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { // Track token index for error reporting bytecodeCompiler.currentTokenIndex = node.getIndex(); @@ -246,14 +211,6 @@ static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperato // Code reference call: $code->() or $code->(@args) // right is ListNode with arguments else if (node.right instanceof ListNode) { - // eval { BLOCK } is parsed as sub { ... }->() with useTryCatch. - // visitEvalBlock() already inlined the body and set lastResultReg - // to the block result, so skip the CALL_SUB. - if (node.left instanceof SubroutineNode && ((SubroutineNode) node.left).useTryCatch) { - node.left.accept(bytecodeCompiler); - return; - } - // This is a code reference call: $coderef->(args) // Compile the code reference in scalar context int savedContext = bytecodeCompiler.currentCallContext; @@ -473,115 +430,134 @@ else if (node.right instanceof BinaryOperatorNode) { // Handle short-circuit operators specially - don't compile right operand yet! if (node.operator.equals("&&") || node.operator.equals("and")) { - DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); - try { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; + // Logical AND with short-circuit evaluation + // Only evaluate right side if left side is true - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + // Compile left operand in scalar context (need boolean value) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitInt(0); + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + // Emit conditional jump: if (!rd) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + // NOW compile right operand (only executed if left was true) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.lastResultReg = rd; - } finally { - if (rewrite != null) rewrite.restore(); - } + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); + + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; return; } if (node.operator.equals("||") || node.operator.equals("or")) { - DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); - try { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; + // Logical OR with short-circuit evaluation + // Only evaluate right side if left side is false - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + // Compile left operand in scalar context (need boolean value) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitInt(0); + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + // Emit conditional jump: if (rd) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + + // NOW compile right operand (only executed if left was false) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); - bytecodeCompiler.lastResultReg = rd; - } finally { - if (rewrite != null) rewrite.restore(); - } + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; return; } if (node.operator.equals("//")) { - DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); - try { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; + // Defined-OR with short-circuit evaluation + // Only evaluate right side if left side is undefined - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + // Compile left operand in scalar context (need to test definedness) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - int definedReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.DEFINED); - bytecodeCompiler.emitReg(definedReg); - bytecodeCompiler.emitReg(rd); + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); - bytecodeCompiler.emitReg(definedReg); - bytecodeCompiler.emitInt(0); + // Check if left is defined + int definedReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.DEFINED); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitReg(rd); - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + // Emit conditional jump: if (defined) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + // NOW compile right operand (only executed if left was undefined) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.lastResultReg = rd; - } finally { - if (rewrite != null) rewrite.restore(); - } + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); + + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; return; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 833a8d950..4374be4fe 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -394,9 +394,6 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Loop control operators: last/next/redo [LABEL] bytecodeCompiler.handleLoopControlOperator(node, op); bytecodeCompiler.lastResultReg = -1; // No result after control flow - } else if (op.equals("goto")) { - bytecodeCompiler.handleGotoOperator(node); - bytecodeCompiler.lastResultReg = -1; } else if (op.equals("rand")) { // rand() or rand($max) // Calls Random.rand(max) where max defaults to 1 @@ -557,37 +554,21 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode } } else if (op.equals("stat") || op.equals("lstat")) { // stat FILE or lstat FILE - boolean isUnderscoreOperand = (node.operand instanceof IdentifierNode) - && ((IdentifierNode) node.operand).name.equals("_"); - - if (isUnderscoreOperand) { - int savedContext = bytecodeCompiler.currentCallContext; - try { - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT_LASTHANDLE : Opcodes.LSTAT_LASTHANDLE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emit(savedContext); - bytecodeCompiler.lastResultReg = rd; - } finally { - bytecodeCompiler.currentCallContext = savedContext; - } - } else { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(bytecodeCompiler); - int operandReg = bytecodeCompiler.lastResultReg; + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(operandReg); - bytecodeCompiler.emit(savedContext); + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + bytecodeCompiler.emit(savedContext); // Pass calling context - bytecodeCompiler.lastResultReg = rd; - } finally { - bytecodeCompiler.currentCallContext = savedContext; - } + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; } } else if (op.startsWith("-") && op.length() == 2) { // File test operators: -r, -w, -x, etc. @@ -855,9 +836,6 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Snapshot current variable scopes for this eval site - int evalSiteIdx = bytecodeCompiler.snapshotEvalSiteRegistry(); - // Emit direct opcode EVAL_STRING bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); @@ -866,7 +844,6 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // wantarray() inside the eval body and the eval return value follow // the correct context even when the surrounding sub is VOID. bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); - bytecodeCompiler.emit(evalSiteIdx); bytecodeCompiler.lastResultReg = rd; } else { @@ -908,29 +885,13 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.lastResultReg = rd; } else if (op.equals("undef")) { - if (node.operand instanceof ListNode listNode && !listNode.elements.isEmpty()) { - Node elem = listNode.elements.get(0); - if (elem instanceof OperatorNode opNode && opNode.operator.equals("$")) { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - elem.accept(bytecodeCompiler); - bytecodeCompiler.currentCallContext = savedContext; - int varReg = bytecodeCompiler.lastResultReg; - bytecodeCompiler.emit(Opcodes.UNDEFINE_SCALAR); - bytecodeCompiler.emitReg(varReg); - bytecodeCompiler.lastResultReg = varReg; - } else { - int undefReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(undefReg); - bytecodeCompiler.lastResultReg = undefReg; - } - } else { - int undefReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); - bytecodeCompiler.emitReg(undefReg); - bytecodeCompiler.lastResultReg = undefReg; - } + // undef operator - returns undefined value + // Can be used standalone: undef + // Or with an operand to undef a variable: undef $x (not implemented yet) + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; } else if (op.equals("unaryMinus")) { // Unary minus: -$x // Compile operand in scalar context (negation always produces a scalar) diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 83dcffd95..6259c44b0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -61,17 +61,7 @@ public static RuntimeScalar evalString(String perlCode, String sourceName, int sourceLine, int callContext) { - return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, null).scalar(); - } - - public static RuntimeScalar evalString(String perlCode, - InterpretedCode currentCode, - RuntimeBase[] registers, - String sourceName, - int sourceLine, - int callContext, - Map siteRegistry) { - return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, siteRegistry).scalar(); + return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext).scalar(); } public static RuntimeList evalStringList(String perlCode, @@ -80,16 +70,6 @@ public static RuntimeList evalStringList(String perlCode, String sourceName, int sourceLine, int callContext) { - return evalStringList(perlCode, currentCode, registers, sourceName, sourceLine, callContext, null); - } - - public static RuntimeList evalStringList(String perlCode, - InterpretedCode currentCode, - RuntimeBase[] registers, - String sourceName, - int sourceLine, - int callContext, - Map siteRegistry) { try { evalTrace("EvalStringHandler enter ctx=" + callContext + " srcName=" + sourceName + " srcLine=" + sourceLine + " codeLen=" + (perlCode != null ? perlCode.length() : -1)); @@ -148,20 +128,20 @@ public static RuntimeList evalStringList(String perlCode, RuntimeBase[] capturedVars = new RuntimeBase[0]; Map adjustedRegistry = null; - // Use per-eval-site registry if available (correct scoping), else fall back to global - Map sourceRegistry = (siteRegistry != null) ? siteRegistry - : (currentCode != null ? currentCode.variableRegistry : null); - - if (sourceRegistry != null && registers != null) { + if (currentCode != null && currentCode.variableRegistry != null && registers != null) { + // Sort parent variables by register index for consistent ordering List> sortedVars = new ArrayList<>( - sourceRegistry.entrySet() + currentCode.variableRegistry.entrySet() ); sortedVars.sort(Map.Entry.comparingByValue()); + // Build capturedVars array and adjusted registry + // Captured variables will be placed at registers 3+ in eval'd code List capturedList = new ArrayList<>(); adjustedRegistry = new HashMap<>(); + // Always include reserved registers in adjusted registry adjustedRegistry.put("this", 0); adjustedRegistry.put("@_", 1); adjustedRegistry.put("wantarray", 2); @@ -171,6 +151,7 @@ public static RuntimeList evalStringList(String perlCode, String varName = entry.getKey(); int parentRegIndex = entry.getValue(); + // Skip reserved registers (they're handled separately in interpreter) if (parentRegIndex < 3) { continue; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index d58face7e..b39ab962f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -3,7 +3,6 @@ import org.perlonjava.runtime.runtimetypes.*; import java.util.BitSet; -import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -36,9 +35,6 @@ public class InterpretedCode extends RuntimeCode { public final BitSet warningFlags; // Warning flags at compile time public final String compilePackage; // Package at compile time (for eval STRING name resolution) - public boolean containsRegex; // Whether this code contains regex ops (for match var scoping) - public List> evalSiteRegistries; // Per-eval-site variable snapshots - // Debug information (optional) public final String sourceName; // Source file name (for stack traces) public final int sourceLine; // Source line number @@ -1165,15 +1161,6 @@ public String disassemble() { rs = bytecode[pc++]; sb.append("SET_SCALAR r").append(rd).append(".set(r").append(rs).append(")\n"); break; - case Opcodes.MY_SCALAR: - rd = bytecode[pc++]; - rs = bytecode[pc++]; - sb.append("MY_SCALAR r").append(rd).append(" = new Scalar(r").append(rs).append(")\n"); - break; - case Opcodes.UNDEFINE_SCALAR: - rd = bytecode[pc++]; - sb.append("UNDEFINE_SCALAR r").append(rd).append(".undefine()\n"); - break; case Opcodes.NOT: rd = bytecode[pc++]; rs = bytecode[pc++]; @@ -1325,9 +1312,7 @@ public String disassemble() { case Opcodes.EVAL_STRING: rd = bytecode[pc++]; rs = bytecode[pc++]; - int evalCtx = bytecode[pc++]; - int evalSiteId = bytecode[pc++]; - sb.append("EVAL_STRING r").append(rd).append(" = eval(r").append(rs).append(", ctx=").append(evalCtx).append(", site=").append(evalSiteId).append(")\n"); + sb.append("EVAL_STRING r").append(rd).append(" = eval(r").append(rs).append(")\n"); break; case Opcodes.SELECT_OP: rd = bytecode[pc++]; @@ -1549,6 +1534,15 @@ public String disassemble() { case Opcodes.DO_FILE: sb.append("DO_FILE r").append(bytecode[pc++]).append(" = doFile(r").append(bytecode[pc++]).append(") ctx=").append(bytecode[pc++]).append("\n"); break; + case Opcodes.PUSH_LABELED_BLOCK: { + int labelIdx = bytecode[pc++]; + int exitPc = bytecode[pc++]; + sb.append("PUSH_LABELED_BLOCK \"").append(stringPool[labelIdx]).append("\" exitPc=").append(exitPc).append("\n"); + break; + } + case Opcodes.POP_LABELED_BLOCK: + sb.append("POP_LABELED_BLOCK\n"); + break; default: sb.append("UNKNOWN(").append(opcode).append(")\n"); break; diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 5b543dc6f..4849678ec 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -513,20 +513,6 @@ public static int executeLstat(int[] bytecode, int pc, RuntimeBase[] registers) return pc; } - public static int executeStatLastHandle(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int ctx = bytecode[pc++]; - registers[rd] = Stat.statLastHandle(ctx); - return pc; - } - - public static int executeLstatLastHandle(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int ctx = bytecode[pc++]; - registers[rd] = Stat.lstatLastHandle(ctx); - return pc; - } - /** * Execute print operation. * Format: PRINT contentReg filehandleReg diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 11059b59b..83b1f0062 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1165,5 +1165,13 @@ public class Opcodes { /** Undefine a scalar variable in-place: rd.undefine(). Used by `undef $x`. */ public static final short UNDEFINE_SCALAR = 353; + /** Push a labeled block entry for non-local last/next/redo handling. + * Format: PUSH_LABELED_BLOCK label_string_idx exit_pc(int) */ + public static final short PUSH_LABELED_BLOCK = 354; + + /** Pop a labeled block entry. + * Format: POP_LABELED_BLOCK */ + public static final short POP_LABELED_BLOCK = 355; + private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index d23de55c1..8430e8082 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -7,8 +7,6 @@ import org.perlonjava.runtime.operators.Time; import org.perlonjava.runtime.runtimetypes.*; -import java.util.Map; - /** * Handler for rarely-used operations called directly by BytecodeInterpreter. * @@ -273,19 +271,12 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; int evalCallContext = RuntimeContextType.SCALAR; + // Newer bytecode encodes the eval operator's own call context (VOID/SCALAR/LIST) + // so eval semantics are correct even when the surrounding statement is compiled + // in VOID context. if (pc < bytecode.length) { evalCallContext = bytecode[pc++]; } - int evalSiteIdx = -1; - if (pc < bytecode.length) { - evalSiteIdx = bytecode[pc++]; - } - - // Resolve per-eval-site variable registry if available - Map siteRegistry = null; - if (evalSiteIdx >= 0 && code.evalSiteRegistries != null && evalSiteIdx < code.evalSiteRegistries.size()) { - siteRegistry = code.evalSiteRegistries.get(evalSiteIdx); - } // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -314,14 +305,28 @@ public static int executeEvalString( } if (callContext == RuntimeContextType.LIST) { + // Return list context result RuntimeList result = EvalStringHandler.evalStringList( - perlCode, code, registers, code.sourceName, code.sourceLine, callContext, siteRegistry); + perlCode, + code, // Current InterpretedCode for context + registers, // Current registers for variable access + code.sourceName, + code.sourceLine, + callContext + ); registers[rd] = result; evalTrace("EVAL_STRING opcode exit LIST stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + " scalar=" + result.scalar().toString()); } else { + // Scalar/void context: return scalar result RuntimeScalar result = EvalStringHandler.evalString( - perlCode, code, registers, code.sourceName, code.sourceLine, callContext, siteRegistry); + perlCode, + code, // Current InterpretedCode for context + registers, // Current registers for variable access + code.sourceName, + code.sourceLine, + callContext + ); registers[rd] = result; evalTrace("EVAL_STRING opcode exit SCALAR/VOID stored=" + (registers[rd] != null ? registers[rd].getClass().getSimpleName() : "null") + " val=" + result.toString() + " bool=" + result.getBoolean()); @@ -725,7 +730,10 @@ public static int executeSplit( int ctx = bytecode[pc++]; RuntimeScalar pattern = (RuntimeScalar) registers[patternReg]; - RuntimeList args = (RuntimeList) registers[argsReg]; + RuntimeBase argsBase = registers[argsReg]; + RuntimeList args = (argsBase instanceof RuntimeList) + ? (RuntimeList) argsBase + : new RuntimeList(argsBase.scalar()); RuntimeList result = Operator.split(pattern, args, ctx); diff --git a/src/main/java/org/perlonjava/backend/jvm/Dereference.java b/src/main/java/org/perlonjava/backend/jvm/Dereference.java index 6d4131340..830fffabc 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Dereference.java +++ b/src/main/java/org/perlonjava/backend/jvm/Dereference.java @@ -794,11 +794,20 @@ 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); - - if (right.elements.size() == 1 && !isSingleRange) { + + // Only apply the fix to true array literals with range operators + if (right.elements.size() == 1 && !(isArrayLiteral && isSingleRange)) { // Single index: use get/delete/exists methods Node elem = right.elements.getFirst(); elem.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java index 48bda77ee..491a814aa 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java @@ -263,23 +263,6 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { variableNode = actualVariable; } - // Save pre-existing lexical variable before the loop so we can restore after - int savedLexicalVarIndex = -1; - int lexicalVarIndex = -1; - if (!isReferenceAliasing && !loopVariableIsGlobal - && variableNode instanceof OperatorNode saveOp) { - String saveName = extractSimpleVariableName(saveOp); - if (saveName != null) { - int idx = emitterVisitor.ctx.symbolTable.getVariableIndex(saveName); - if (idx != -1) { - lexicalVarIndex = idx; - savedLexicalVarIndex = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); - mv.visitVarInsn(Opcodes.ALOAD, lexicalVarIndex); - mv.visitVarInsn(Opcodes.ASTORE, savedLexicalVarIndex); - } - } - } - // For global $_ as loop variable, we need to: // 1. Evaluate the list first (before any localization takes effect) // 2. For statement modifiers: localize $_ ourselves @@ -584,12 +567,6 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { mv.visitLabel(loopEnd); - // Restore pre-existing lexical variable after the loop - if (savedLexicalVarIndex != -1) { - mv.visitVarInsn(Opcodes.ALOAD, savedLexicalVarIndex); - mv.visitVarInsn(Opcodes.ASTORE, lexicalVarIndex); - } - // Restore the original value for reference aliasing: for \$x (...), for \@x (...), for \%x (...) if (isReferenceAliasing && savedValueIndex != -1) { if (actualVariable instanceof OperatorNode innerOp && innerOp.operand instanceof IdentifierNode) { diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index c7b436290..f8cde803e 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -864,20 +864,24 @@ static void handleStatOperator(EmitterVisitor emitterVisitor, OperatorNode node, if (node.operand instanceof IdentifierNode identNode && identNode.name.equals("_")) { - // stat _ or lstat _ - use cached stat buffer with context - emitterVisitor.pushCallContext(); + // stat _ or lstat _ - still use the old methods since they don't take args emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/operators/Stat", operator + "LastHandle", - "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", + "()Lorg/perlonjava/runtime/runtimetypes/RuntimeList;", false); + // Handle context - treat as list that needs conversion if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { handleVoidContext(emitterVisitor); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { - emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeScalar"); - } else if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { - emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeList"); + // Convert with stat's special semantics + emitterVisitor.ctx.mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/runtimetypes/RuntimeList", + "statScalar", + "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); } } else { // stat EXPR or lstat EXPR - use context-aware methods diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 5226e0844..6151612e7 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -741,15 +741,6 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", "getList", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeList;", false); mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); - if (localRecord.containsRegex()) { - mv.visitVarInsn(Opcodes.ALOAD, returnListSlot); - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeList", - "resolveMatchProxies", - "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)V", - false); - } - // Phase 3: Check for control flow markers // RuntimeList is on stack after getList() @@ -1628,12 +1619,6 @@ private static InterpretedCode compileToInterpreter( ctx.errorUtil ); - // Inherit package from the JVM compiler context so unqualified sub calls - // resolve in the correct package (not main) in the interpreter fallback. - if (ctx.symbolTable != null) { - compiler.setCompilePackage(ctx.symbolTable.getCurrentPackage()); - } - // Compile AST to interpreter bytecode (pass ctx for package context and closure detection) InterpretedCode code = compiler.compile(ast, ctx); diff --git a/src/main/java/org/perlonjava/backend/jvm/Local.java b/src/main/java/org/perlonjava/backend/jvm/Local.java index 10065f9f9..e2fb232f5 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Local.java +++ b/src/main/java/org/perlonjava/backend/jvm/Local.java @@ -22,13 +22,13 @@ public class Local { * and the index of the dynamic variable stack. */ static localRecord localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { + // Check if the code contains a 'local' operator boolean containsLocalOperator = FindDeclarationVisitor.findOperator(ast, "local") != null; - boolean containsRegex = FindDeclarationVisitor.findOperator(ast, "matchRegex") != null - || FindDeclarationVisitor.findOperator(ast, "replaceRegex") != null; - boolean needsDynamicSave = containsLocalOperator || containsRegex; int dynamicIndex = -1; - if (needsDynamicSave) { + if (containsLocalOperator) { + // Allocate a local variable to store the dynamic variable stack index dynamicIndex = ctx.symbolTable.allocateLocalVariable(); + // Get the current level of the dynamic variable stack and store it mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", "getLocalLevel", @@ -36,18 +36,21 @@ static localRecord localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { false); mv.visitVarInsn(Opcodes.ISTORE, dynamicIndex); } - if (containsRegex) { - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeRegexState", - "pushLocal", - "()V", - false); - } - return new localRecord(needsDynamicSave, containsRegex, dynamicIndex); + return new localRecord(containsLocalOperator, dynamicIndex); } + /** + * Tears down the local variable setup by restoring the dynamic variable stack + * to its previous level if a 'local' operator was present. + * + * @param localRecord The record containing information about the 'local' operator + * and the dynamic variable stack index. + * @param mv The method visitor used to generate bytecode instructions. + */ static void localTeardown(localRecord localRecord, MethodVisitor mv) { - if (localRecord.needsDynamicSave()) { + // Add `local` teardown logic + if (localRecord.containsLocalOperator()) { + // Restore the dynamic variable stack to the recorded level mv.visitVarInsn(Opcodes.ILOAD, localRecord.dynamicIndex()); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", @@ -57,6 +60,13 @@ static void localTeardown(localRecord localRecord, MethodVisitor mv) { } } - record localRecord(boolean needsDynamicSave, boolean containsRegex, int dynamicIndex) { + /** + * A record to store information about the presence of a 'local' operator + * and the index of the dynamic variable stack. + * + * @param containsLocalOperator Indicates if a 'local' operator is present. + * @param dynamicIndex The index of the dynamic variable stack. + */ + record localRecord(boolean containsLocalOperator, int dynamicIndex) { } } diff --git a/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java b/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java index f6171d9af..c0b46f394 100644 --- a/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java +++ b/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java @@ -157,9 +157,10 @@ public static boolean processBlock(EmitterVisitor emitterVisitor, BlockNode node return false; } - // tryWholeBlockRefactoring is disabled: the interpreter fallback handles - // "Method too large" safely without breaking return/goto semantics. - return false; + // Fallback: Try whole-block refactoring + return tryWholeBlockRefactoring(emitterVisitor, node); // Block was refactored and emitted + + // No refactoring was possible } /** diff --git a/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java b/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java index 1fbacdd0f..f383c2aa4 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java +++ b/src/main/java/org/perlonjava/frontend/analysis/ControlFlowDetectorVisitor.java @@ -81,12 +81,6 @@ public void scan(Node root) { if (state == 0) { String oper = op.operator; - if ("return".equals(oper)) { - if (DEBUG) System.err.println("ControlFlowDetector(scan): UNSAFE return at tokenIndex=" + op.tokenIndex); - hasUnsafeControlFlow = true; - continue; - } - if ("goto".equals(oper)) { if (allowedGotoLabels != null && op.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { Node arg = labelNode.elements.getFirst(); diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index a5920c3c2..6a8a059de 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -694,22 +694,13 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex) paren = true; } - if (nextToken.text.equals("_")) { - // Handle `stat _` - TokenUtils.consume(parser); - if (paren) { - TokenUtils.consume(parser, OPERATOR, ")"); - } - return new OperatorNode(token.text, - new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex); - } - // stat/lstat: bareword filehandle (typically ALLCAPS) should be treated as a typeglob. // Consume it here, before generic expression parsing can turn it into a subroutine call. if (nextToken.type == IDENTIFIER) { String name = nextToken.text; if (name.matches("^[A-Z_][A-Z0-9_]*$")) { TokenUtils.consume(parser); + // autovivify filehandle and convert to globref GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, name)); Node fh = FileHandle.parseBarewordHandle(parser, name); Node operand = fh != null ? fh : new IdentifierNode(name, parser.tokenIndex); @@ -719,6 +710,15 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex) return new OperatorNode(token.text, operand, currentIndex); } } + if (nextToken.text.equals("_")) { + // Handle `stat _` + TokenUtils.consume(parser); + if (paren) { + TokenUtils.consume(parser, OPERATOR, ")"); + } + return new OperatorNode(token.text, + new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex); + } // Parse optional single argument (or default to $_) // If we've already consumed '(', we must parse a full expression up to ')'. diff --git a/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java b/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java index 1137246ca..3ed5de56c 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParseMapGrepSort.java @@ -2,8 +2,6 @@ import org.perlonjava.frontend.astnode.*; import org.perlonjava.frontend.lexer.LexerToken; -import org.perlonjava.frontend.lexer.LexerTokenType; -import org.perlonjava.runtime.runtimetypes.NameNormalizer; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import java.util.List; @@ -22,29 +20,6 @@ public class ParseMapGrepSort { static BinaryOperatorNode parseSort(Parser parser, LexerToken token) { ListNode operand; int currentIndex = parser.tokenIndex; - - LexerToken firstToken = peek(parser); - if (firstToken.type == LexerTokenType.IDENTIFIER - && !ParserTables.CORE_PROTOTYPES.containsKey(firstToken.text) - && !ParsePrimary.isIsQuoteLikeOperator(firstToken.text)) { - int savedIndex = parser.tokenIndex; - String subName = IdentifierParser.parseSubroutineIdentifier(parser); - if (subName != null && !subName.isEmpty()) { - LexerToken afterName = peek(parser); - if (!afterName.text.equals("(") && !afterName.text.equals("=>")) { - String fullName = subName.contains("::") - ? subName - : NameNormalizer.normalizeVariableName(subName, - parser.ctx.symbolTable.getCurrentPackage()); - operand = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false); - Node block = new OperatorNode("&", - new IdentifierNode(fullName, currentIndex), currentIndex); - return new BinaryOperatorNode(token.text, block, operand, parser.tokenIndex); - } - } - parser.tokenIndex = savedIndex; - } - try { // Handle 'sort' keyword as a Binary operator with a Code and List operands operand = ListParser.parseZeroOrMoreList(parser, 1, true, false, false, false); diff --git a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java index b3c5b424e..bb45a56c7 100644 --- a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java +++ b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java @@ -482,7 +482,7 @@ private static void handleListOrHashArgument(Parser parser, ListNode args, boole parser.tokenIndex = saveIndex; } - ListNode argList = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false); + ListNode argList = ListParser.parseZeroOrMoreList(parser, 0, false, true, false, false); // @ and % consume remaining arguments in LIST context // for (Node element : argList.elements) { // element.setAnnotation("context", "LIST"); diff --git a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java index 7f88d7934..409b12510 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java @@ -503,7 +503,7 @@ private Node generateTooFewArgsMessage() { new StringNode("Too few arguments for subroutine '" + fullName + "' (got ", parser.tokenIndex), argCount, parser.tokenIndex), - new StringNode(minParams == maxParams ? "; expected " : "; expected at least ", parser.tokenIndex), + new StringNode("; expected at least ", parser.tokenIndex), parser.tokenIndex), new NumberNode(Integer.toString(adjustedMin), parser.tokenIndex), parser.tokenIndex), @@ -539,7 +539,7 @@ private Node generateTooManyArgsMessage() { new StringNode("Too many arguments for subroutine '" + fullName + "' (got ", parser.tokenIndex), argCount, parser.tokenIndex), - new StringNode(minParams == maxParams ? "; expected " : "; expected at most ", parser.tokenIndex), + new StringNode("; expected at most ", parser.tokenIndex), parser.tokenIndex), new NumberNode(Integer.toString(adjustedMax), parser.tokenIndex), parser.tokenIndex), diff --git a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java index 5d790cd6f..cb9e8dc84 100644 --- a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java @@ -185,8 +185,8 @@ public RuntimeScalar write(String string) { data[i] = (byte) string.charAt(i); } ByteBuffer byteBuffer = ByteBuffer.wrap(data); - fileChannel.write(byteBuffer); - return scalarTrue; + int bytesWritten = fileChannel.write(byteBuffer); + return new RuntimeScalar(bytesWritten); } catch (IOException e) { return handleIOException(e, "write failed"); } @@ -354,10 +354,14 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } + // Convert bytes to string representation buffer.flip(); - byte[] readBytes = new byte[buffer.remaining()]; - buffer.get(readBytes); - return new RuntimeScalar(readBytes); + StringBuilder result = new StringBuilder(bytesRead); + while (buffer.hasRemaining()) { + result.append((char) (buffer.get() & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { getGlobalVariable("main::!").set(e.getMessage()); return new RuntimeScalar(); // undef diff --git a/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java b/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java index 47487f898..c2f176e90 100644 --- a/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java @@ -178,9 +178,13 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } - byte[] readBytes = new byte[bytesRead]; - System.arraycopy(buffer, 0, readBytes, 0, bytesRead); - return new RuntimeScalar(readBytes); + // Convert bytes to string representation + StringBuilder result = new StringBuilder(bytesRead); + for (int i = 0; i < bytesRead; i++) { + result.append((char) (buffer[i] & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { isEOF = true; getGlobalVariable("main::!").set(e.getMessage()); diff --git a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java index 05e1dbce2..5f10b345c 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java @@ -332,9 +332,13 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } - byte[] readBytes = new byte[bytesRead]; - System.arraycopy(buffer, 0, readBytes, 0, bytesRead); - return new RuntimeScalar(readBytes); + // Convert bytes to string representation + StringBuilder result = new StringBuilder(bytesRead); + for (int i = 0; i < bytesRead; i++) { + result.append((char) (buffer[i] & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { getGlobalVariable("main::!").set(e.getMessage()); return new RuntimeScalar(); // undef diff --git a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java index 229b24db6..117056ddf 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java @@ -211,7 +211,7 @@ public RuntimeScalar write(String string) { process.getOutputStream().write(bytes); process.getOutputStream().flush(); - return scalarTrue; + return new RuntimeScalar(bytes.length); } catch (IOException e) { return handleIOException(e, "Write to pipe failed"); } diff --git a/src/main/java/org/perlonjava/runtime/io/StandardIO.java b/src/main/java/org/perlonjava/runtime/io/StandardIO.java index 705a40fcf..a246558d3 100644 --- a/src/main/java/org/perlonjava/runtime/io/StandardIO.java +++ b/src/main/java/org/perlonjava/runtime/io/StandardIO.java @@ -285,9 +285,13 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } - byte[] readBytes = new byte[bytesRead]; - System.arraycopy(buffer, 0, readBytes, 0, bytesRead); - return new RuntimeScalar(readBytes); + // Convert bytes to string representation + StringBuilder result = new StringBuilder(bytesRead); + for (int i = 0; i < bytesRead; i++) { + result.append((char) (buffer[i] & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { getGlobalVariable("main::!").set(e.getMessage()); return new RuntimeScalar(); // undef diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index eccbb790a..dae5aa00b 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -318,18 +318,18 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl if (GlobalVariable.existsGlobalCodeRef(normalizedClassMethodName)) { RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(normalizedClassMethodName); // Perl method lookup should ignore undefined CODE slots (e.g. after `undef *pkg::method`). - // But don't skip to next class — fall through to AUTOLOAD check for this class. - if (codeRef.getDefinedBoolean()) { - // Cache the found method - cacheMethod(cacheKey, codeRef); - - if (TRACE_METHOD_RESOLUTION) { - System.err.println(" FOUND method!"); - System.err.flush(); - } - - return codeRef; + if (!codeRef.getDefinedBoolean()) { + continue; + } + // Cache the found method + cacheMethod(cacheKey, codeRef); + + if (TRACE_METHOD_RESOLUTION) { + System.err.println(" FOUND method!"); + System.err.flush(); } + + return codeRef; } // Method not found in current class, check AUTOLOAD diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index 635ab2770..5037fc64a 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -259,21 +259,7 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) return scalarUndef; } - // Try to use the stored file path for file test - if (fh.filePath != null) { - Path path = fh.filePath; - try { - boolean lstat = operator.equals("-l"); - statForFileTest(fileHandle, path, lstat); - return evaluateFileTest(operator, path, fileHandle); - } catch (Exception e) { - getGlobalVariable("main::!").set(5); - updateLastStat(fileHandle, false, 5); - return scalarUndef; - } - } - - // For non-file handles (stdin, in-memory, socket), return undef and set EBADF + // For file test operators on file handles, return undef and set EBADF getGlobalVariable("main::!").set(9); updateLastStat(fileHandle, false, 9); return scalarUndef; @@ -320,211 +306,232 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) try { boolean lstat = operator.equals("-l"); statForFileTest(fileHandle, path, lstat); - return evaluateFileTest(operator, path, fileHandle); - } catch (IOException e) { - getGlobalVariable("main::!").set(2); - updateLastStat(fileHandle, false, 2); - return scalarUndef; - } - } - - private static RuntimeScalar evaluateFileTest(String operator, Path path, RuntimeScalar fileHandle) throws IOException { - String filename = path.toString(); - return switch (operator) { - case "-r" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + return switch (operator) { + case "-r" -> { + // Check if file is readable + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isReadable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isReadable(path)); - } - case "-w" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-w" -> { + // Check if file is writable + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isWritable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isWritable(path)); - } - case "-x" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-x" -> { + // Check if file is executable + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isExecutable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isExecutable(path)); - } - case "-e" -> { - boolean exists = Files.exists(path); - if (!exists) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-e" -> { + // Check if file exists + boolean exists = Files.exists(path); + if (!exists) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield scalarTrue; } - getGlobalVariable("main::!").set(0); - yield scalarTrue; - } - case "-z" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-z" -> { + // Check if file is empty (zero size) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.size(path) == 0); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.size(path) == 0); - } - case "-s" -> { - if (!lastStatOk) { - yield scalarUndef; + case "-s" -> { + // Return file size if non-zero, otherwise return false + if (!lastStatOk) { + yield scalarUndef; + } + long size = lastBasicAttr.size(); + yield size > 0 ? new RuntimeScalar(size) : RuntimeScalarCache.scalarZero; } - long size = lastBasicAttr.size(); - yield size > 0 ? new RuntimeScalar(size) : RuntimeScalarCache.scalarZero; - } - case "-f" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-f" -> { + // Check if path is a regular file + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path)); - } - case "-d" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-d" -> { + // Check if path is a directory + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isDirectory(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isDirectory(path)); - } - case "-l" -> { - if (!lastStatOk) { - yield scalarUndef; + case "-l" -> { + // Check if path is a symbolic link + if (!lastStatOk) { + yield scalarUndef; + } + yield getScalarBoolean(lastBasicAttr.isSymbolicLink()); } - yield getScalarBoolean(lastBasicAttr.isSymbolicLink()); - } - case "-o" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-o" -> { + // Check if file is owned by the effective user id (approximate with current user) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + UserPrincipal owner = Files.getOwner(path); + UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() + .lookupPrincipalByName(System.getProperty("user.name")); + yield getScalarBoolean(owner.equals(currentUser)); } - getGlobalVariable("main::!").set(0); - UserPrincipal owner = Files.getOwner(path); - UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - yield getScalarBoolean(owner.equals(currentUser)); - } - case "-p" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-p" -> { + // Approximate check for named pipe (FIFO) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".fifo")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".fifo")); - } - case "-S" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-S" -> { + // Approximate check for socket + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".sock")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".sock")); - } - case "-b" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-b" -> { + // Approximate check for block special file + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); - } - case "-c" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-c" -> { + // Approximate check for character special file + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); - } - case "-u" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-u" -> { + // Check if setuid bit is set + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OWNER_EXECUTE))); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OWNER_EXECUTE))); - } - case "-g" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-g" -> { + // Check if setgid bit is set + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.GROUP_EXECUTE))); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.GROUP_EXECUTE))); - } - case "-k" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-k" -> { + // Approximate check for sticky bit (using others execute permission) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OTHERS_EXECUTE))); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OTHERS_EXECUTE))); - } - case "-T", "-B" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-T", "-B" -> { + // Check if file is text (-T) or binary (-B) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield isTextOrBinary(path, operator.equals("-T")); } - getGlobalVariable("main::!").set(0); - yield isTextOrBinary(path, operator.equals("-T")); - } - case "-M", "-A", "-C" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-M", "-A", "-C" -> { + // Get file time difference for modification (-M), access (-A), or creation (-C) time + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getFileTimeDifference(path, operator); } - getGlobalVariable("main::!").set(0); - yield getFileTimeDifference(path, operator); - } - case "-R" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-R" -> { + // Check if file is readable by the real user ID + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isReadable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isReadable(path)); - } - case "-W" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-W" -> { + // Check if file is writable by the real user ID + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isWritable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isWritable(path)); - } - case "-X" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-X" -> { + // Check if file is executable by the real user ID + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isExecutable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isExecutable(path)); - } - case "-O" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); + case "-O" -> { + // Check if file is owned by the current user + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + UserPrincipal owner = Files.getOwner(path); + UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() + .lookupPrincipalByName(System.getProperty("user.name")); + yield getScalarBoolean(owner.equals(currentUser)); + } + case "-t" -> { + // -t on a string filename is an error in Perl (expects a filehandle) + // Set $! = EBADF and return undef + getGlobalVariable("main::!").set(9); // EBADF yield scalarUndef; } - getGlobalVariable("main::!").set(0); - UserPrincipal owner = Files.getOwner(path); - UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - yield getScalarBoolean(owner.equals(currentUser)); - } - case "-t" -> { - getGlobalVariable("main::!").set(9); - yield scalarUndef; - } - default -> throw new UnsupportedOperationException("Unsupported file test operator: " + operator); - }; + default -> throw new UnsupportedOperationException("Unsupported file test operator: " + operator); + }; + } catch (IOException e) { + // Set error message in global variable and return false/undef + getGlobalVariable("main::!").set(2); // ENOENT for most file operations + updateLastStat(fileHandle, false, 2); + return scalarUndef; + } } public static RuntimeScalar chainedFileTest(String[] operators, RuntimeScalar fileHandle) { diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index a0f8e610a..78aeb24d1 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -726,9 +726,6 @@ public static RuntimeScalar sysread(int ctx, RuntimeBase... args) { newValue.append(data); target.set(newValue.toString()); - if (result.type == RuntimeScalarType.BYTE_STRING && target.type != RuntimeScalarType.TIED_SCALAR) { - target.type = RuntimeScalarType.BYTE_STRING; - } return new RuntimeScalar(bytesRead); } @@ -877,7 +874,7 @@ public static RuntimeScalar syswrite(int ctx, RuntimeBase... args) { /** * Checks if the handle has a :utf8 layer. */ - public static boolean hasUtf8Layer(RuntimeIO fh) { + private static boolean hasUtf8Layer(RuntimeIO fh) { IOHandle handle = fh.ioHandle; while (handle instanceof LayeredIOHandle layered) { String layers = layered.getCurrentLayers(); @@ -2142,18 +2139,7 @@ public static RuntimeScalar sysseek(int ctx, RuntimeBase... args) { } public static RuntimeScalar read(int ctx, RuntimeBase... args) { - RuntimeScalar result = sysread(ctx, args); - if (args.length >= 2) { - RuntimeScalar fileHandle = args[0].scalar(); - RuntimeIO fh = fileHandle.getRuntimeIO(); - if (fh != null && hasUtf8Layer(fh)) { - RuntimeScalar target = args[1].scalar().scalarDeref(); - if (target.type != RuntimeScalarType.TIED_SCALAR) { - target.type = RuntimeScalarType.STRING; - } - } - } - return result; + return sysread(ctx, args); } } diff --git a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java index e44b06f6c..31a189797 100644 --- a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java @@ -725,9 +725,6 @@ public static RuntimeScalar integer(RuntimeScalar arg1) { public static RuntimeScalar not(RuntimeScalar runtimeScalar) { - if (runtimeScalar instanceof ScalarSpecialVariable) { - return getScalarBoolean(!runtimeScalar.getBoolean()); - } return switch (runtimeScalar.type) { case INTEGER -> getScalarBoolean((int) runtimeScalar.value == 0); case DOUBLE -> getScalarBoolean((double) runtimeScalar.value == 0.0); diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index d5b9a442d..d6f8716ad 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -206,10 +206,10 @@ public static RuntimeList split(RuntimeScalar quotedRegex, RuntimeList args, int } } } else { - // In Perl, split with a string variable compiles the string as a regex - String regexPattern = quotedRegex.toString(); + // Treat quotedRegex as a literal string + String literalPattern = quotedRegex.toString(); - if (regexPattern.isEmpty()) { + if (literalPattern.isEmpty()) { // Special case: if the pattern is an empty string, split between characters if (limit > 0) { for (int i = 0; i < inputStr.length() && splitElements.size() < limit - 1; i++) { @@ -224,35 +224,9 @@ public static RuntimeList split(RuntimeScalar quotedRegex, RuntimeList args, int } } } else { - Pattern pattern = Pattern.compile(regexPattern); - Matcher matcher = pattern.matcher(inputStr); - int lastEnd = 0; - int splitCount = 0; - - while (matcher.find() && (limit <= 0 || splitCount < limit - 1)) { - if (lastEnd == 0 && matcher.end() == 0) { - // Zero-width match at start never produces an empty field - } else if (matcher.start() == matcher.end() && matcher.start() == lastEnd) { - continue; - } else { - splitElements.add(new RuntimeScalar(inputStr.substring(lastEnd, matcher.start()))); - } - for (int i = 1; i <= matcher.groupCount(); i++) { - String group = matcher.group(i); - splitElements.add(group != null ? new RuntimeScalar(group) : scalarUndef); - } - lastEnd = matcher.end(); - splitCount++; - } - - if (lastEnd <= inputStr.length()) { - splitElements.add(new RuntimeScalar(inputStr.substring(lastEnd))); - } - - if (limit == 0) { - while (!splitElements.isEmpty() && splitElements.getLast().toString().isEmpty()) { - splitElements.removeLast(); - } + String[] parts = inputStr.split(Pattern.quote(literalPattern), limit); + for (String part : parts) { + splitElements.add(new RuntimeScalar(part)); } } } @@ -316,11 +290,7 @@ public static RuntimeScalar substr(int ctx, RuntimeBase... args) { String extractedSubstring = result; lvalue.set(replacement); // Return the extracted substring, not the lvalue (which now contains the replacement) - RuntimeScalar extracted = new RuntimeScalar(extractedSubstring); - if (((RuntimeScalar) args[0]).type == RuntimeScalarType.BYTE_STRING) { - extracted.type = RuntimeScalarType.BYTE_STRING; - } - return extracted; + return new RuntimeScalar(extractedSubstring); } return lvalue; diff --git a/src/main/java/org/perlonjava/runtime/operators/Readline.java b/src/main/java/org/perlonjava/runtime/operators/Readline.java index 69da9d1cc..8d5b7ba65 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Readline.java +++ b/src/main/java/org/perlonjava/runtime/operators/Readline.java @@ -360,9 +360,6 @@ public static RuntimeScalar read(RuntimeList args) { // Update the scalar with the new value scalar.set(scalarValue.toString()); - if (!IOOperator.hasUtf8Layer(fh)) { - scalar.type = RuntimeScalarType.BYTE_STRING; - } // Return the number of characters read return new RuntimeScalar(charsRead); diff --git a/src/main/java/org/perlonjava/runtime/operators/Stat.java b/src/main/java/org/perlonjava/runtime/operators/Stat.java index 8dea5f768..cfce73aa6 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Stat.java +++ b/src/main/java/org/perlonjava/runtime/operators/Stat.java @@ -21,7 +21,6 @@ import static org.perlonjava.runtime.operators.FileTestOperator.lastBasicAttr; import static org.perlonjava.runtime.operators.FileTestOperator.lastFileHandle; import static org.perlonjava.runtime.operators.FileTestOperator.lastPosixAttr; -import static org.perlonjava.runtime.operators.FileTestOperator.lastStatOk; import static org.perlonjava.runtime.operators.FileTestOperator.updateLastStat; import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable; import static org.perlonjava.runtime.runtimetypes.RuntimeIO.resolvePath; @@ -64,38 +63,12 @@ private static int getPermissionsOctal(BasicFileAttributes basicAttr, PosixFileA return permissions; } - public static RuntimeBase statLastHandle(int ctx) { - if (!lastStatOk) { - getGlobalVariable("main::!").set(9); - RuntimeList empty = new RuntimeList(); - if (ctx == RuntimeContextType.SCALAR) { - return empty.statScalar(); - } - return empty; - } - RuntimeList res = new RuntimeList(); - statInternal(res, lastBasicAttr, lastPosixAttr); - if (ctx == RuntimeContextType.SCALAR) { - return res.statScalar(); - } - return res; + public static RuntimeList statLastHandle() { + return stat(lastFileHandle); } - public static RuntimeBase lstatLastHandle(int ctx) { - if (!lastStatOk) { - getGlobalVariable("main::!").set(9); - RuntimeList empty = new RuntimeList(); - if (ctx == RuntimeContextType.SCALAR) { - return empty.statScalar(); - } - return empty; - } - RuntimeList res = new RuntimeList(); - statInternal(res, lastBasicAttr, lastPosixAttr); - if (ctx == RuntimeContextType.SCALAR) { - return res.statScalar(); - } - return res; + public static RuntimeList lstatLastHandle() { + return lstat(lastFileHandle); } /** @@ -153,12 +126,8 @@ public static RuntimeList stat(RuntimeScalar arg) { return res; // Return empty list } - // Try to stat via the stored file path - if (fh.filePath != null) { - return statPath(arg, fh.filePath, res, false); - } - // For in-memory file handles (like PerlIO::scalar), we can't stat them + // They should return EBADF getGlobalVariable("main::!").set(9); updateLastStat(arg, false, 9, false); return res; @@ -166,34 +135,33 @@ public static RuntimeList stat(RuntimeScalar arg) { // Handle string arguments String filename = arg.toString(); - Path path = resolvePath(filename); - return statPath(arg, path, res, false); - } - private static RuntimeList statPath(RuntimeScalar arg, Path path, RuntimeList res, boolean lstat) { + // Handle regular filenames try { - BasicFileAttributes basicAttr; - PosixFileAttributes posixAttr; - if (lstat) { - basicAttr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - posixAttr = Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - } else { - basicAttr = Files.readAttributes(path, BasicFileAttributes.class); - posixAttr = Files.readAttributes(path, PosixFileAttributes.class); - } + Path path = resolvePath(filename); + + // Basic file attributes (similar to some Perl stat fields) + BasicFileAttributes basicAttr = Files.readAttributes(path, BasicFileAttributes.class); + + // POSIX file attributes (for Unix-like systems) + PosixFileAttributes posixAttr = Files.readAttributes(path, PosixFileAttributes.class); lastBasicAttr = basicAttr; lastPosixAttr = posixAttr; statInternal(res, basicAttr, posixAttr); + // Clear $! on success getGlobalVariable("main::!").set(0); - updateLastStat(arg, true, 0, lstat); + updateLastStat(arg, true, 0, false); } catch (NoSuchFileException e) { + // Set $! to ENOENT (No such file or directory) = 2 getGlobalVariable("main::!").set(2); - updateLastStat(arg, false, 2, lstat); + updateLastStat(arg, false, 2, false); } catch (IOException e) { - getGlobalVariable("main::!").set(5); - updateLastStat(arg, false, 5, lstat); + // Returns the empty list if "stat" fails. + // Set a generic error code for other IO errors + getGlobalVariable("main::!").set(5); // EIO (Input/output error) + updateLastStat(arg, false, 5, false); } return res; } @@ -223,12 +191,8 @@ public static RuntimeList lstat(RuntimeScalar arg) { return res; // Return empty list } - // Try to lstat via the stored file path - if (fh.filePath != null) { - return statPath(arg, fh.filePath, res, true); - } - // For in-memory file handles (like PerlIO::scalar), we can't lstat them + // They should return EBADF getGlobalVariable("main::!").set(9); updateLastStat(arg, false, 9, true); return res; @@ -236,8 +200,37 @@ public static RuntimeList lstat(RuntimeScalar arg) { // Handle string arguments String filename = arg.toString(); - Path path = resolvePath(filename); - return statPath(arg, path, res, true); + + // Handle regular filenames + try { + Path path = resolvePath(filename); + + // Basic attributes without following symlink + BasicFileAttributes basicAttr = Files.readAttributes(path, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + + // POSIX attributes without following symlink + PosixFileAttributes posixAttr = Files.readAttributes(path, + PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + + lastBasicAttr = basicAttr; + lastPosixAttr = posixAttr; + + statInternal(res, basicAttr, posixAttr); + // Clear $! on success + getGlobalVariable("main::!").set(0); + updateLastStat(arg, true, 0, true); + } catch (NoSuchFileException e) { + // Set $! to ENOENT (No such file or directory) = 2 + getGlobalVariable("main::!").set(2); + updateLastStat(arg, false, 2, true); + } catch (IOException e) { + // Returns the empty list if "lstat" fails. + // Set a generic error code for other IO errors + getGlobalVariable("main::!").set(5); // EIO (Input/output error) + updateLastStat(arg, false, 5, true); + } + return res; } public static void statInternal(RuntimeList res, BasicFileAttributes basicAttr, PosixFileAttributes posixAttr) { diff --git a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java index cee06757c..40cd65af4 100644 --- a/src/main/java/org/perlonjava/runtime/operators/StringOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/StringOperators.java @@ -41,14 +41,18 @@ public static RuntimeScalar length(RuntimeScalar runtimeScalar) { * @return a {@link RuntimeScalar} containing the byte length of the input */ public static RuntimeScalar lengthBytes(RuntimeScalar runtimeScalar) { + // If the scalar is undefined, return undef if (!runtimeScalar.getDefinedBoolean()) { return RuntimeScalarCache.scalarUndef; } + // Convert the RuntimeScalar to a string and return its byte length String str = runtimeScalar.toString(); - if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING) { - return getScalarInt(str.length()); + try { + return getScalarInt(str.getBytes(StandardCharsets.UTF_8).length); + } catch (Exception e) { + // If UTF-8 encoding fails, fall back to character count + return getScalarInt(str.codePointCount(0, str.length())); } - return getScalarInt(str.getBytes(StandardCharsets.UTF_8).length); } /** @@ -277,34 +281,37 @@ public static RuntimeScalar stringConcat(RuntimeScalar runtimeScalar, RuntimeSca boolean aIsString = runtimeScalar.type == RuntimeScalarType.STRING || runtimeScalar.type == RuntimeScalarType.BYTE_STRING; boolean bIsString = b.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.BYTE_STRING; + // Preserve Perl-like UTF-8 flag semantics only for string scalars. + // For other types, keep legacy behavior to avoid wide behavioral changes. if (aIsString && bIsString) { + // If either operand is explicitly STRING type, return STRING + if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { + return new RuntimeScalar(aStr + bStr); + } + + // Both are BYTE_STRING - check if they actually contain only bytes 0-255 boolean hasUnicode = false; for (int i = 0; i < aStr.length(); i++) { - if (aStr.charAt(i) > 255) { hasUnicode = true; break; } + if (aStr.charAt(i) > 255) { + hasUnicode = true; + break; + } } if (!hasUnicode) { for (int i = 0; i < bStr.length(); i++) { - if (bStr.charAt(i) > 255) { hasUnicode = true; break; } + if (bStr.charAt(i) > 255) { + hasUnicode = true; + break; + } } } + // If Unicode present, upgrade to STRING to preserve characters if (hasUnicode) { return new RuntimeScalar(aStr + bStr); } - if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING || b.type == RuntimeScalarType.BYTE_STRING) { - byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] out = new byte[aBytes.length + bBytes.length]; - System.arraycopy(aBytes, 0, out, 0, aBytes.length); - System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); - return new RuntimeScalar(out); - } - - if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { - return new RuntimeScalar(aStr + bStr); - } - + // Pure byte strings - concatenate as bytes byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); byte[] out = new byte[aBytes.length + bBytes.length]; @@ -313,24 +320,7 @@ public static RuntimeScalar stringConcat(RuntimeScalar runtimeScalar, RuntimeSca return new RuntimeScalar(out); } - if (runtimeScalar.type == BYTE_STRING || b.type == BYTE_STRING) { - boolean hasWide = false; - for (int i = 0; i < aStr.length() && !hasWide; i++) { - if (aStr.charAt(i) > 255) hasWide = true; - } - for (int i = 0; i < bStr.length() && !hasWide; i++) { - if (bStr.charAt(i) > 255) hasWide = true; - } - if (!hasWide) { - byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] out = new byte[aBytes.length + bBytes.length]; - System.arraycopy(aBytes, 0, out, 0, aBytes.length); - System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); - return new RuntimeScalar(out); - } - } - return new RuntimeScalar(aStr + bStr); + return new RuntimeScalar(runtimeScalar + bStr); } public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeScalar, RuntimeScalar b) { @@ -345,33 +335,34 @@ public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeS boolean bIsString = b.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.BYTE_STRING; if (aIsString && bIsString) { + // If either operand is explicitly STRING type, return STRING + if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { + return new RuntimeScalar(aStr + bStr); + } + + // Both are BYTE_STRING - check if they actually contain only bytes 0-255 boolean hasUnicode = false; for (int i = 0; i < aStr.length(); i++) { - if (aStr.charAt(i) > 255) { hasUnicode = true; break; } + if (aStr.charAt(i) > 255) { + hasUnicode = true; + break; + } } if (!hasUnicode) { for (int i = 0; i < bStr.length(); i++) { - if (bStr.charAt(i) > 255) { hasUnicode = true; break; } + if (bStr.charAt(i) > 255) { + hasUnicode = true; + break; + } } } + // If Unicode present, upgrade to STRING to preserve characters if (hasUnicode) { return new RuntimeScalar(aStr + bStr); } - if (runtimeScalar.type == RuntimeScalarType.BYTE_STRING || b.type == RuntimeScalarType.BYTE_STRING) { - byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] out = new byte[aBytes.length + bBytes.length]; - System.arraycopy(aBytes, 0, out, 0, aBytes.length); - System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); - return new RuntimeScalar(out); - } - - if (runtimeScalar.type == RuntimeScalarType.STRING || b.type == RuntimeScalarType.STRING) { - return new RuntimeScalar(aStr + bStr); - } - + // Pure byte strings - concatenate as bytes byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); byte[] out = new byte[aBytes.length + bBytes.length]; @@ -380,24 +371,7 @@ public static RuntimeScalar stringConcatWarnUninitialized(RuntimeScalar runtimeS return new RuntimeScalar(out); } - if (runtimeScalar.type == BYTE_STRING || b.type == BYTE_STRING) { - boolean hasWide = false; - for (int i = 0; i < aStr.length() && !hasWide; i++) { - if (aStr.charAt(i) > 255) hasWide = true; - } - for (int i = 0; i < bStr.length() && !hasWide; i++) { - if (bStr.charAt(i) > 255) hasWide = true; - } - if (!hasWide) { - byte[] aBytes = aStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] bBytes = bStr.getBytes(StandardCharsets.ISO_8859_1); - byte[] out = new byte[aBytes.length + bBytes.length]; - System.arraycopy(aBytes, 0, out, 0, aBytes.length); - System.arraycopy(bBytes, 0, out, aBytes.length, bBytes.length); - return new RuntimeScalar(out); - } - } - return new RuntimeScalar(aStr + bStr); + return new RuntimeScalar(runtimeScalar + bStr); } public static RuntimeScalar chompScalar(RuntimeScalar runtimeScalar) { @@ -584,7 +558,7 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa RuntimeScalarCache.scalarEmptyString); } - boolean anyIsByteString = runtimeScalar.type == BYTE_STRING; + boolean isByteString = runtimeScalar.type == BYTE_STRING; String delimiter = runtimeScalar.toString(); @@ -610,19 +584,12 @@ private static RuntimeScalar joinInternal(RuntimeScalar runtimeScalar, RuntimeBa RuntimeScalarCache.scalarEmptyString); } - anyIsByteString = anyIsByteString || scalar.type == BYTE_STRING; + isByteString = isByteString && scalar.type == BYTE_STRING; sb.append(scalar); } RuntimeScalar res = new RuntimeScalar(sb.toString()); - if (anyIsByteString) { - String resultStr = sb.toString(); - boolean hasWide = false; - for (int i = 0; i < resultStr.length(); i++) { - if (resultStr.charAt(i) > 255) { hasWide = true; break; } - } - if (!hasWide) { - res.type = BYTE_STRING; - } + if (isByteString) { + res.type = BYTE_STRING; } return res; } diff --git a/src/main/java/org/perlonjava/runtime/operators/Time.java b/src/main/java/org/perlonjava/runtime/operators/Time.java index c4a7dd499..85b5931da 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 org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import java.time.format.DateTimeFormatter; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -89,8 +89,8 @@ public static RuntimeList localtime(RuntimeList args, int ctx) { if (args.isEmpty()) { date = ZonedDateTime.now(); } else { - long epoch = (long) Math.floor(args.getFirst().getDouble()); - date = Instant.ofEpochSecond(epoch).atZone(ZoneId.systemDefault()); + long arg = args.getFirst().getInt(); + date = Instant.ofEpochSecond(arg).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 epoch = (long) Math.floor(args.getFirst().getDouble()); - date = Instant.ofEpochSecond(epoch).atZone(ZoneId.of("UTC")); + long arg = args.getFirst().getInt(); + date = Instant.ofEpochSecond(arg).atZone(ZoneId.of("UTC")); } return getTimeComponents(ctx, date); } @@ -116,15 +116,7 @@ public static RuntimeList gmtime(RuntimeList args, int ctx) { private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) { RuntimeList res = new RuntimeList(); if (ctx == RuntimeContextType.SCALAR) { - 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()))); + res.add(date.format(DateTimeFormatter.RFC_1123_DATE_TIME)); return res; } // 0 1 2 3 4 5 6 7 8 @@ -135,8 +127,7 @@ private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) { res.add(date.getDayOfMonth()); res.add(date.getMonth().getValue() - 1); res.add(date.getYear() - 1900); - int dow = date.getDayOfWeek().getValue(); // Mon=1..Sun=7 - res.add(dow == 7 ? 0 : dow); // Sun=0, Mon=1..Sat=6 + res.add(date.getDayOfWeek().getValue()); res.add(date.getDayOfYear() - 1); res.add(date.getZone().getRules().isDaylightSavings(date.toInstant()) ? 1 : 0); return res; diff --git a/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java b/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java index dc271478a..ed9e56fbd 100644 --- a/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java +++ b/src/main/java/org/perlonjava/runtime/operators/unpack/NumericFormatHandler.java @@ -614,34 +614,24 @@ public int getFormatSize() { public static class FloatHandler extends NumericFormatHandler { @Override public void unpack(UnpackState state, List output, int count, boolean isStarCount) { - if (state.isUTF8Data() && state.isCharacterMode()) { - ByteBuffer buffer = state.getBuffer(); - boolean isBigEndian = (buffer.order() == java.nio.ByteOrder.BIG_ENDIAN); - for (int i = 0; i < count; i++) { - if (state.remainingCodePoints() < 4) break; - int b1 = state.nextCodePoint() & 0xFF; - int b2 = state.nextCodePoint() & 0xFF; - int b3 = state.nextCodePoint() & 0xFF; - int b4 = state.nextCodePoint() & 0xFF; - int intBits = isBigEndian - ? (b1 << 24) | (b2 << 16) | (b3 << 8) | b4 - : b1 | (b2 << 8) | (b3 << 16) | (b4 << 24); - output.add(new RuntimeScalar((double) Float.intBitsToFloat(intBits))); - } - return; - } - + // Save current mode boolean wasCharacterMode = state.isCharacterMode(); + + // Switch to byte mode for numeric reading if (wasCharacterMode) { state.switchToByteMode(); } ByteBuffer buffer = state.getBuffer(); + for (int i = 0; i < count; i++) { - if (buffer.remaining() < 4) break; + if (buffer.remaining() < 4) { + break; + } output.add(new RuntimeScalar(buffer.getFloat())); } + // Restore original mode if (wasCharacterMode) { state.switchToCharacterMode(); } @@ -656,36 +646,24 @@ public int getFormatSize() { public static class DoubleHandler extends NumericFormatHandler { @Override public void unpack(UnpackState state, List output, int count, boolean isStarCount) { - if (state.isUTF8Data() && state.isCharacterMode()) { - ByteBuffer buffer = state.getBuffer(); - boolean isBigEndian = (buffer.order() == java.nio.ByteOrder.BIG_ENDIAN); - for (int i = 0; i < count; i++) { - if (state.remainingCodePoints() < 8) break; - long bits = 0; - for (int j = 0; j < 8; j++) { - int b = state.nextCodePoint() & 0xFF; - if (isBigEndian) { - bits = (bits << 8) | b; - } else { - bits |= ((long) b) << (j * 8); - } - } - output.add(new RuntimeScalar(Double.longBitsToDouble(bits))); - } - return; - } - + // Save current mode boolean wasCharacterMode = state.isCharacterMode(); + + // Switch to byte mode for numeric reading if (wasCharacterMode) { state.switchToByteMode(); } ByteBuffer buffer = state.getBuffer(); + for (int i = 0; i < count; i++) { - if (buffer.remaining() < 8) break; + if (buffer.remaining() < 8) { + break; + } output.add(new RuntimeScalar(buffer.getDouble())); } + // Restore original mode if (wasCharacterMode) { state.switchToCharacterMode(); } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java b/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java index 0830f1e37..3bd0dbcf7 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DigestMD5.java @@ -72,7 +72,7 @@ public static RuntimeList add(RuntimeArray args, int ctx) { // Check for wide characters using the utility method StringParser.assertNoWideCharacters(dataStr, "add"); - byte[] bytes = dataStr.getBytes(StandardCharsets.ISO_8859_1); + byte[] bytes = dataStr.getBytes(StandardCharsets.UTF_8); md.update(bytes); updateBlockCount(self, bytes.length); } @@ -176,7 +176,7 @@ public static RuntimeList add_bits(RuntimeArray args, int ctx) { } int numBytes = nbits / 8; - byte[] dataBytes = data.getBytes(StandardCharsets.ISO_8859_1); + byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); byte[] truncatedBytes = new byte[Math.min(numBytes, dataBytes.length)]; System.arraycopy(dataBytes, 0, truncatedBytes, 0, truncatedBytes.length); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java b/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java index 76a03f47e..34a3e1246 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DigestSHA.java @@ -101,7 +101,7 @@ public static RuntimeList add(RuntimeArray args, int ctx) { StringParser.assertNoWideCharacters(dataStr, "add"); if (data.type != RuntimeScalarType.UNDEF) { - md.update(dataStr.getBytes(StandardCharsets.ISO_8859_1)); + md.update(dataStr.getBytes(StandardCharsets.UTF_8)); } } @@ -199,7 +199,7 @@ public static RuntimeList add_bits(RuntimeArray args, int ctx) { int nbits = nbitsScalar.getInt(); // Convert data to bytes, but only use the specified number of bits - byte[] dataBytes = data.getBytes(StandardCharsets.ISO_8859_1); + byte[] dataBytes = data.getBytes(StandardCharsets.UTF_8); byte[] truncatedBytes = truncateToNBits(dataBytes, nbits); md.update(truncatedBytes); } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java b/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java index ff1adcede..bb39ab3ab 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Encode.java @@ -175,7 +175,7 @@ public static RuntimeList is_utf8(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for is_utf8"); } - return RuntimeScalarCache.getScalarBoolean(args.get(0).type != BYTE_STRING).getList(); + return RuntimeScalarCache.getScalarBoolean(args.get(0).type == BYTE_STRING).getList(); // // In PerlOnJava, strings are always internally Unicode (Java strings) // // So we'll check if the string contains any non-ASCII characters diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java index 18ed138f5..761e905ff 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java @@ -626,15 +626,9 @@ static int handleRegexCharacterClassEscape(int offset, String s, StringBuilder s sb.append(Character.toChars(c2)); lastChar = octalValue; } else { - // Short octal (1-2 digits) inside character class - // In character classes, \1-\7 are always octals, not backrefs - // Java requires leading zero: \4 → \04, \12 → \012 - sb.append('0'); - for (int i = 0; i < octalLength; i++) { - sb.append(s.charAt(offset + i)); - } - offset += octalLength - 1; - lastChar = octalValue; + // Short octal or single digit, pass through + sb.append(Character.toChars(c2)); + lastChar = c2; } } else if (c2 == '8' || c2 == '9') { // \8 and \9 are not valid octals - treat as literal digits diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index eea7b0899..48cf2a194 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -56,31 +56,8 @@ protected boolean removeEldestEntry(Map.Entry eldest) { public static String lastSuccessfulMatchString = null; // ${^LAST_SUCCESSFUL_PATTERN} public static RuntimeRegex lastSuccessfulPattern = null; - - public static Object[] saveMatchState() { - return new Object[]{ - globalMatcher, globalMatchString, - lastMatchedString, lastMatchStart, lastMatchEnd, - lastSuccessfulMatchedString, lastSuccessfulMatchStart, lastSuccessfulMatchEnd, - lastSuccessfulMatchString, lastSuccessfulPattern - }; - } - - public static void restoreMatchState(Object[] state) { - globalMatcher = (Matcher) state[0]; - globalMatchString = (String) state[1]; - lastMatchedString = (String) state[2]; - lastMatchStart = (Integer) state[3]; - lastMatchEnd = (Integer) state[4]; - lastSuccessfulMatchedString = (String) state[5]; - lastSuccessfulMatchStart = (Integer) state[6]; - lastSuccessfulMatchEnd = (Integer) state[7]; - lastSuccessfulMatchString = (String) state[8]; - lastSuccessfulPattern = (RuntimeRegex) state[9]; - } - // Indicates if \G assertion is used - boolean useGAssertion = false; + private final boolean useGAssertion = false; // Compiled regex pattern public Pattern pattern; int patternFlags; @@ -133,7 +110,6 @@ public static RuntimeRegex compile(String patternString, String modifiers) { regex.regexFlags = fromModifiers(modifiers, patternString); regex.patternFlags = regex.regexFlags.toPatternFlags(); - regex.useGAssertion = regex.regexFlags.useGAssertion(); String javaPattern = null; try { @@ -284,7 +260,6 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternString = originalRegex.patternString; regex.regexFlags = mergeRegexFlags(originalRegex.regexFlags, modifierStr, originalRegex.patternString); regex.patternFlags = regex.regexFlags.toPatternFlags(); - regex.useGAssertion = regex.regexFlags.useGAssertion(); return new RuntimeScalar(regex); } @@ -310,7 +285,6 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternString = originalRegex.patternString; regex.regexFlags = mergeRegexFlags(originalRegex.regexFlags, modifierStr, originalRegex.patternString); regex.patternFlags = regex.regexFlags.toPatternFlags(); - regex.useGAssertion = regex.regexFlags.useGAssertion(); return new RuntimeScalar(regex); } @@ -376,7 +350,6 @@ public static RuntimeScalar getReplacementRegex(RuntimeScalar patternString, Run } } - regex.useGAssertion = regex.regexFlags != null && regex.regexFlags.useGAssertion(); regex.replacement = replacement; return new RuntimeScalar(regex); } @@ -442,15 +415,13 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc // hexPrinter(inputStr); // Use RuntimePosLvalue to get the current position - // In Perl, pos() only affects /g matches; non-/g matches always start from position 0 RuntimeScalar posScalar = RuntimePosLvalue.pos(string); - boolean isGlobal = regex.regexFlags.isGlobalMatch(); - boolean isPosDefined = isGlobal && posScalar.getDefinedBoolean(); + boolean isPosDefined = posScalar.getDefinedBoolean(); int startPos = isPosDefined ? posScalar.getInt() : 0; // Check if previous call had zero-length match at this position (for SCALAR context) // This prevents infinite loops in: while ($str =~ /pat/g) - if (isGlobal && ctx == RuntimeContextType.SCALAR) { + if (regex.regexFlags.isGlobalMatch() && ctx == RuntimeContextType.SCALAR) { String patternKey = regex.patternString; if (RuntimePosLvalue.hadZeroLengthMatchAt(string, startPos, patternKey)) { // Previous match was zero-length at this position - fail to break loop @@ -459,7 +430,7 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc } } - // Start matching from the current position if defined (only for /g matches) + // Start matching from the current position if defined if (isPosDefined) { matcher.region(startPos, inputStr.length()); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java index 0840d9dfe..b46d02d90 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java @@ -29,12 +29,14 @@ public static int getLocalLevel() { * @param variable the dynamic state to be pushed onto the stack. */ public static RuntimeBase pushLocalVariable(RuntimeBase variable) { + // Save the current state of the variable and push it onto the stack. variable.dynamicSaveState(); variableStack.push(variable); return variable; } public static RuntimeScalar pushLocalVariable(RuntimeScalar variable) { + // Save the current state of the variable and push it onto the stack. variable.dynamicSaveState(); variableStack.push(variable); return variable; @@ -47,11 +49,6 @@ public static RuntimeGlob pushLocalVariable(RuntimeGlob variable) { return variable; } - public static void pushLocalDynamicState(DynamicState state) { - state.dynamicSaveState(); - variableStack.push(state); - } - /** * Pops dynamic variables from the stack until the stack size matches the specified target local level. * This is useful for restoring the stack to a previous state by removing any variables added after that state. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java index 46e7ee5bb..d77cf5931 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java @@ -2,8 +2,6 @@ import org.perlonjava.runtime.mro.InheritanceResolver; -import java.util.HashMap; -import java.util.Map; import java.util.function.Function; import static org.perlonjava.runtime.runtimetypes.RuntimeContextType.SCALAR; @@ -65,7 +63,6 @@ public class OverloadContext { * The fallback method handler */ final RuntimeScalar methodFallback; - private final Map resolvedMethods = new HashMap<>(); /** * Private constructor to create an OverloadContext instance. @@ -221,14 +218,12 @@ public RuntimeScalar tryOverloadFallback(RuntimeScalar runtimeScalar, String... * @return RuntimeScalar result from method execution, or null if method not found */ public RuntimeScalar tryOverload(String methodName, RuntimeArray perlMethodArgs) { - if (resolvedMethods.containsKey(methodName)) { - RuntimeScalar perlMethod = resolvedMethods.get(methodName); - if (perlMethod == null) return null; - return RuntimeCode.apply(perlMethod, perlMethodArgs, SCALAR).getFirst(); - } + // Look for method in class hierarchy RuntimeScalar perlMethod = InheritanceResolver.findMethodInHierarchy(methodName, perlClassName, null, 0); - resolvedMethods.put(methodName, perlMethod); - if (perlMethod == null) return null; + if (perlMethod == null) { + return null; + } + // Execute found method with provided arguments return RuntimeCode.apply(perlMethod, perlMethodArgs, SCALAR).getFirst(); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 950cf708e..d45043e7b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -17,7 +17,6 @@ import org.perlonjava.backend.bytecode.BytecodeCompiler; import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.backend.bytecode.InterpreterState; -import org.perlonjava.runtime.regex.RuntimeRegex; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -415,7 +414,6 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // Then when "say @arr" is parsed in the BEGIN, it resolves to BEGIN_PKG_x::@arr // which is aliased to the runtime array with values (a, b). Map capturedVars = capturedSymbolTable.getAllVisibleVariables(); - List evalAliasKeys = new ArrayList<>(); for (SymbolTable.SymbolEntry entry : capturedVars.values()) { if (!entry.name().equals("@_") && !entry.decl().isEmpty() && !entry.name().startsWith("&")) { if (!entry.decl().equals("our")) { @@ -435,7 +433,6 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // entry.name() is "@arr" but the key should be "packageName::arr" String varNameWithoutSigil = entry.name().substring(1); // Remove the sigil String fullName = packageName + "::" + varNameWithoutSigil; - evalAliasKeys.add(fullName); // Alias the global to the runtime value if (runtimeValue instanceof RuntimeArray) { @@ -568,17 +565,6 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje setCurrentScope(capturedSymbolTable); - // Clean up eval STRING aliases from global namespace. - // These aliases were created before parsing so BEGIN blocks inside the eval - // could access outer lexicals. After compilation, they are no longer needed. - // Leaving them would cause `my` re-declarations in loops to pick up stale - // values via retrieveBeginScalar instead of creating fresh objects. - for (String key : evalAliasKeys) { - GlobalVariable.globalVariables.remove(key); - GlobalVariable.globalArrays.remove(key); - GlobalVariable.globalHashes.remove(key); - } - // Store source lines in symbol table if $^P flags are set // Do this on both success and failure paths when flags require retention // Use the original evalString and actualFileName; AST may be null on failure @@ -757,7 +743,6 @@ public static RuntimeList evalStringWithInterpreter( // Save dynamic variable level to restore after eval int dynamicVarLevel = DynamicVariableManager.getLocalLevel(); - List evalAliasKeys = new ArrayList<>(); try { String evalString = code.toString(); @@ -806,7 +791,6 @@ public static RuntimeList evalStringWithInterpreter( String packageName = PersistentVariable.beginPackage(operatorAst.id); String varNameWithoutSigil = entry.name().substring(1); String fullName = packageName + "::" + varNameWithoutSigil; - evalAliasKeys.add(fullName); if (runtimeValue instanceof RuntimeArray) { GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); @@ -1034,13 +1018,6 @@ public static RuntimeList evalStringWithInterpreter( // Restore dynamic variables (local) to their state before eval DynamicVariableManager.popToLocalLevel(dynamicVarLevel); - // Clean up eval STRING aliases from global namespace - for (String key : evalAliasKeys) { - GlobalVariable.globalVariables.remove(key); - GlobalVariable.globalArrays.remove(key); - GlobalVariable.globalHashes.remove(key); - } - // Store source lines in debugger symbol table if $^P flags are set // Do this on both success and failure paths when flags require retention // ast and tokens may be null if parsing failed early, but storeSourceLines handles that @@ -1714,11 +1691,13 @@ public boolean defined() { */ public RuntimeList apply(RuntimeArray a, int callContext) { if (constantValue != null) { + // Alternative way to create constants like: `$constant::{_CAN_PCS} = \$const` return new RuntimeList(constantValue); } try { + // Wait for the compilerThread to finish if it exists if (this.compilerSupplier != null) { - this.compilerSupplier.get(); + this.compilerSupplier.get(); // Wait for the task to finish } if (isStatic) { @@ -1727,13 +1706,17 @@ public RuntimeList apply(RuntimeArray a, int callContext) { return (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); } } catch (NullPointerException e) { + if (this.methodHandle == null) { throw new PerlCompilerException("Subroutine exists but has null method handle (possible compilation or registration error) at "); } else if (this.codeObject == null && !isStatic) { throw new PerlCompilerException("Subroutine exists but has null code object at "); } else { + // Original NPE from somewhere else throw new PerlCompilerException("Null pointer exception in subroutine call: " + e.getMessage() + " at "); } + + //throw new PerlCompilerException("Undefined subroutine called at "); } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); if (!(targetException instanceof RuntimeException)) { @@ -1747,11 +1730,14 @@ public RuntimeList apply(RuntimeArray a, int callContext) { public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) { if (constantValue != null) { + // Alternative way to create constants like: `$constant::{_CAN_PCS} = \$const` return new RuntimeList(constantValue); } try { + // Wait for the compilerThread to finish if it exists if (this.compilerSupplier != null) { - this.compilerSupplier.get(); + // System.out.println("Waiting for compiler thread to finish..."); + this.compilerSupplier.get(); // Wait for the task to finish } if (isStatic) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index 4021d2bbf..1d8d006ff 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -154,13 +154,6 @@ protected boolean removeEldestEntry(Map.Entry eldest) { */ public DirectoryIO directoryIO; - /** - * The file system path associated with this I/O handle, if opened from a file. - * Used by stat() and file test operators (-f, -s, etc.) on filehandles. - * Null for non-file handles (STDIN/STDOUT/STDERR, pipes, sockets, in-memory). - */ - public Path filePath; - /** * The name of the glob that owns this IO handle (e.g., "main::STDOUT"). * Used for stringification when the filehandle is used in string context. @@ -380,7 +373,6 @@ public static RuntimeIO open(String fileName, String mode) { // Initialize ioHandle with CustomFileChannel fh.ioHandle = new CustomFileChannel(filePath, options); - fh.filePath = filePath; // Add the handle to the LRU cache addHandle(fh.ioHandle); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index bc6383058..9c020d76e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -240,15 +240,6 @@ public RuntimeList getList() { return this; } - public static void resolveMatchProxies(RuntimeList list) { - for (int i = 0; i < list.elements.size(); i++) { - RuntimeBase elem = list.elements.get(i); - if (elem instanceof ScalarSpecialVariable ssv) { - list.elements.set(i, ssv.getValueAsScalar()); - } - } - } - /** * Evaluates the boolean representation of the list. * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java index ad6806052..e69de29bb 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java @@ -1,25 +0,0 @@ -package org.perlonjava.runtime.runtimetypes; - -import org.perlonjava.runtime.regex.RuntimeRegex; - -public class RuntimeRegexState implements DynamicState { - - private Object[] savedState; - - public static void pushLocal() { - DynamicVariableManager.pushLocalDynamicState(new RuntimeRegexState()); - } - - @Override - public void dynamicSaveState() { - savedState = RuntimeRegex.saveMatchState(); - } - - @Override - public void dynamicRestoreState() { - if (savedState != null) { - RuntimeRegex.restoreMatchState(savedState); - savedState = null; - } - } -} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 67c4942f0..bf054c56e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -109,9 +109,6 @@ public RuntimeScalar(RuntimeScalar scalar) { if (scalar.type == TIED_SCALAR) { scalar = scalar.tiedFetch(); } - if (scalar instanceof ScalarSpecialVariable ssv) { - scalar = ssv.getValueAsScalar(); - } this.type = scalar.type; this.value = scalar.value; } @@ -639,9 +636,6 @@ public RuntimeScalar set(RuntimeScalar value) { if (value.type == TIED_SCALAR) { return set(value.tiedFetch()); } - if (value instanceof ScalarSpecialVariable ssv) { - return set(ssv.getValueAsScalar()); - } if (this.type == TIED_SCALAR) { return this.tiedStore(value); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java index 84708f9f7..55e577b8f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalarReadOnly.java @@ -84,8 +84,7 @@ public RuntimeScalarReadOnly(String s) { */ @Override void vivify() { - RuntimeException ex = new RuntimeException("Modification of a read-only value attempted (value=" + this.toString() + " type=" + this.type + ")"); - throw ex; + throw new RuntimeException("Modification of a read-only value attempted"); } /** diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java index d4c057b95..5fe125bc3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java @@ -28,7 +28,7 @@ public RuntimeSubstrLvalue(RuntimeScalar parent, String str, int offset, int len this.offset = offset; this.length = length; - this.type = (parent.type == RuntimeScalarType.BYTE_STRING) ? RuntimeScalarType.BYTE_STRING : RuntimeScalarType.STRING; + this.type = RuntimeScalarType.STRING; this.value = str; } @@ -96,11 +96,8 @@ public RuntimeScalar set(RuntimeScalar value) { updatedValue.replace(startIndex, endIndex, newValue); } - RuntimeScalar newVal = new RuntimeScalar(updatedValue.toString()); - if (lvalue.type == RuntimeScalarType.BYTE_STRING) { - newVal.type = RuntimeScalarType.BYTE_STRING; - } - lvalue.set(newVal); + // Update the parent RuntimeScalar with the modified string + lvalue.set(new RuntimeScalar(updatedValue.toString())); return this; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index d7ff69f65..cfea89ab4 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -88,13 +88,7 @@ public RuntimeScalar set(RuntimeScalar value) { return super.set(value); } - @Override - public RuntimeList getList() { - RuntimeList list = new RuntimeList(); - this.addToList(list); - return list; - } - + // Add itself to a RuntimeArray. public void addToArray(RuntimeArray array) { array.elements.add(new RuntimeScalar(this.getValueAsScalar())); } @@ -114,7 +108,7 @@ public RuntimeScalar addToScalar(RuntimeScalar var) { * * @return The RuntimeScalar value of the special variable, or null if not available. */ - RuntimeScalar getValueAsScalar() { + private RuntimeScalar getValueAsScalar() { try { RuntimeScalar result = switch (variableId) { case CAPTURE -> { diff --git a/src/main/perl/lib/Digest/SHA.pm b/src/main/perl/lib/Digest/SHA.pm index 3dd1527fd..da11f3e7e 100644 --- a/src/main/perl/lib/Digest/SHA.pm +++ b/src/main/perl/lib/Digest/SHA.pm @@ -99,147 +99,147 @@ sub base64digest { # Functional interface implementations sub sha1 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('1'); $sha->add($data); return $sha->digest; } sub sha1_hex { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('1'); $sha->add($data); return $sha->hexdigest; } sub sha1_base64 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('1'); $sha->add($data); return $sha->b64digest; } sub sha224 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('224'); $sha->add($data); return $sha->digest; } sub sha224_hex { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('224'); $sha->add($data); return $sha->hexdigest; } sub sha224_base64 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('224'); $sha->add($data); return $sha->b64digest; } sub sha256 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('256'); $sha->add($data); return $sha->digest; } sub sha256_hex { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('256'); $sha->add($data); return $sha->hexdigest; } sub sha256_base64 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('256'); $sha->add($data); return $sha->b64digest; } sub sha384 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('384'); $sha->add($data); return $sha->digest; } sub sha384_hex { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('384'); $sha->add($data); return $sha->hexdigest; } sub sha384_base64 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('384'); $sha->add($data); return $sha->b64digest; } sub sha512 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512'); $sha->add($data); return $sha->digest; } sub sha512_hex { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512'); $sha->add($data); return $sha->hexdigest; } sub sha512_base64 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512'); $sha->add($data); return $sha->b64digest; } sub sha512224 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512224'); $sha->add($data); return $sha->digest; } sub sha512224_hex { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512224'); $sha->add($data); return $sha->hexdigest; } sub sha512224_base64 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512224'); $sha->add($data); return $sha->b64digest; } sub sha512256 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512256'); $sha->add($data); return $sha->digest; } sub sha512256_hex { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512256'); $sha->add($data); return $sha->hexdigest; } sub sha512256_base64 { - my $data = join('', @_); + my $data = shift; my $sha = Digest::SHA->new('512256'); $sha->add($data); return $sha->b64digest; diff --git a/src/main/perl/lib/Time/Local.pm b/src/main/perl/lib/Time/Local.pm index 7cd8c5204..773414f72 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.35'; +our $VERSION = '1.30'; use parent 'Exporter'; @@ -58,18 +58,6 @@ 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 @@ -92,7 +80,7 @@ else { $Epoc = _daygm( gmtime(0) ); } -%Cheat = (); # clear the cache as epoc has changed +%Cheat = (); # clear the cache as epoc has changed sub _daygm { @@ -107,8 +95,7 @@ sub _daygm { + int( $year / 4 ) - int( $year / 100 ) + int( $year / 400 ) - + int( ( ( $month * 306 ) + 5 ) / 10 ) ) - - $Epoc; + + int( ( ( $month * 306 ) + 5 ) / 10 ) ) - $Epoc; } ); } @@ -124,8 +111,6 @@ 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; @@ -160,8 +145,9 @@ sub timegm { my $days = _daygm( undef, undef, undef, $mday, $month, $year ); - if ( abs($days) > $MaxDay && !$Options{no_range_check} ) { - my $msg = "Day too big - abs($days) > $MaxDay\n"; + unless ( $Options{no_range_check} or abs($days) < $MaxDay ) { + my $msg = q{}; + $msg .= "Day too big - $days > $MaxDay\n" if $days > $MaxDay; $year += 1900; $msg @@ -170,16 +156,11 @@ sub timegm { Carp::croak($msg); } - # 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 - ); + return + $sec + $SecOff + + ( SECS_PER_MINUTE * $min ) + + ( SECS_PER_HOUR * $hour ) + + ( SECS_PER_DAY * $days ); } sub _is_leap_year { @@ -206,16 +187,11 @@ 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 + $subsec; + or return $loc_for_ref_t; # Adjust for timezone my $loc_t = $ref_t - $zone_off; @@ -231,20 +207,20 @@ sub timelocal { && ( ( $ref_t - SECS_PER_HOUR ) - _timegm( localtime( $loc_t - SECS_PER_HOUR ) ) < 0 ) ) { - return ( $loc_t - SECS_PER_HOUR ) + $subsec; + return $loc_t - SECS_PER_HOUR; } # Adjust for DST change $loc_t += $dst_off; - return $loc_t + $subsec if $dst_off > 0; + return $loc_t 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 + $subsec; + return $loc_t; } sub timelocal_nocheck { @@ -278,7 +254,7 @@ Time::Local - Efficiently compute time from local and GMT time =head1 VERSION -version 1.35 +version 1.30 =head1 SYNOPSIS @@ -305,8 +281,6 @@ 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 @@ -319,9 +293,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 @@ -333,8 +307,6 @@ 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 @@ -344,26 +316,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 @@ -371,8 +343,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 @@ -386,16 +358,17 @@ 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 * @@ -406,11 +379,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 @@ -441,15 +414,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. @@ -472,20 +445,21 @@ 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. @@ -498,6 +472,8 @@ 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. @@ -508,7 +484,7 @@ Dave Rolsky =head1 CONTRIBUTORS -=for stopwords Florian Ragwitz Gregory Oschwald J. Nick Koston Tom Wyant Unknown +=for stopwords Florian Ragwitz J. Nick Koston Unknown =over 4 @@ -518,25 +494,17 @@ 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 - 2023 by Graham Barr & Dave Rolsky. +This software is copyright (c) 1997 - 2020 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 b147d77cd56fca972b88b89062d0bdd790c50e58 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 09:37:16 +0100 Subject: [PATCH 13/16] Fix eval BLOCK exception handling after merge conflict Restore outer: while(true) loop structure with continue outer for eval catch handling - stash pop lost the proper structure. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeInterpreter.java | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 5114103da..f327efb6d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -75,6 +75,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c java.util.Stack labeledBlockStack = new java.util.Stack<>(); // Each entry is [labelStringPoolIdx, exitPc] + try { + outer: + while (true) { try { // Main dispatch loop - JVM JIT optimizes switch to tableswitch (O(1) jump) while (pc < bytecode.length) { @@ -2237,9 +2240,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Special handling for ClassCastException to show which opcode is failing // Check if we're inside an eval block first if (!evalCatchStack.isEmpty()) { - evalCatchStack.pop(); + int catchPc = evalCatchStack.pop(); WarnDie.catchEval(e); - return new RuntimeList(); + pc = catchPc; + continue outer; } // Not in eval - show detailed error with bytecode context @@ -2265,14 +2269,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Check if we're inside an eval block if (!evalCatchStack.isEmpty()) { // Inside eval block - catch the exception - evalCatchStack.pop(); // Pop the catch handler + int catchPc = evalCatchStack.pop(); // Pop the catch handler // Call WarnDie.catchEval() to set $@ WarnDie.catchEval(e); - // Eval block failed - return empty list - // (The result will be undef in scalar context, empty in list context) - return new RuntimeList(); + pc = catchPc; + continue outer; } // Not in eval block - propagate exception @@ -2293,6 +2296,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Wrap other exceptions with interpreter context including bytecode context String errorMessage = formatInterpreterError(code, pc, e); throw new RuntimeException(errorMessage, e); + } + } // end outer while } finally { // Always pop the interpreter state InterpreterState.pop(); From c6b4dc0e8948e112b6ebe805d4a269e1311d070f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 12:01:21 +0100 Subject: [PATCH 14/16] Scope eval's currentPackage via DynamicVariableManager to prevent leaking SET_PACKAGE inside eval STRING was leaking into the caller's InterpreterState.currentPackage (used only by caller()). This caused signatures.t to regress from 601 to 446 passing tests. Fix: push/pop currentPackage around eval execution in both EvalStringHandler and RuntimeCode, using the existing DynamicVariableManager mechanism. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/EvalStringHandler.java | 21 ++++++++++++++++--- .../backend/bytecode/InterpreterState.java | 1 + .../runtime/runtimetypes/RuntimeCode.java | 4 +++- 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 6259c44b0..5a2623301 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -218,8 +218,17 @@ public static RuntimeList evalStringList(String perlCode, } // Step 6: Execute the compiled code + // Scope currentPackage (used only by caller()) so SET_PACKAGE + // inside the eval doesn't leak into the caller's state. + int pkgLevel = DynamicVariableManager.getLocalLevel(); + DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); RuntimeArray args = new RuntimeArray(); // Empty @_ - RuntimeList result = evalCode.apply(args, callContext); + RuntimeList result; + try { + result = evalCode.apply(args, callContext); + } finally { + DynamicVariableManager.popToLocalLevel(pkgLevel); + } evalTrace("EvalStringHandler exec ok ctx=" + callContext + " resultScalar=" + (result != null ? result.scalar().toString() : "null") + " resultBool=" + (result != null && result.scalar() != null ? result.scalar().getBoolean() : false) + @@ -295,9 +304,15 @@ public static RuntimeScalar evalString(String perlCode, // Attach captured variables evalCode = evalCode.withCapturedVars(capturedVars); - // Execute + int pkgLevel = DynamicVariableManager.getLocalLevel(); + DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); RuntimeArray args = new RuntimeArray(); - RuntimeList result = evalCode.apply(args, RuntimeContextType.SCALAR); + RuntimeList result; + try { + result = evalCode.apply(args, RuntimeContextType.SCALAR); + } finally { + DynamicVariableManager.popToLocalLevel(pkgLevel); + } return result.scalar(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java index 60e102066..b38e8ecf1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java @@ -114,4 +114,5 @@ public static List getStack() { public static List getPcStack() { return new ArrayList<>(pcStack.get()); } + } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index d45043e7b..ed6580ef3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -741,8 +741,10 @@ public static RuntimeList evalStringWithInterpreter( Node ast = null; List tokens = null; - // Save dynamic variable level to restore after eval + // Save dynamic variable level to restore after eval. + // Push currentPackage (used by caller()) so SET_PACKAGE inside eval doesn't leak. int dynamicVarLevel = DynamicVariableManager.getLocalLevel(); + DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); try { String evalString = code.toString(); From bdaafc9fb8abc6eaad4eb919770ec0f60ae68b1f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 12:06:50 +0100 Subject: [PATCH 15/16] Add design comments documenting package scoping in eval Document the compile-time vs runtime distinction for package handling across InterpreterState, BytecodeCompiler, EvalStringHandler, and RuntimeCode to prevent recurrence of the eval package leak regression. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 10 +++++-- .../backend/bytecode/EvalStringHandler.java | 12 ++++++-- .../backend/bytecode/InterpreterState.java | 30 ++++++++++++++----- .../runtime/runtimetypes/RuntimeCode.java | 7 ++++- 4 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index dc4c94669..b8c8fe0c6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -493,8 +493,14 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Use the calling context from EmitterContext for top-level expressions // This is crucial for eval STRING to propagate context correctly currentCallContext = ctx.contextType; - // Inherit package from the JVM compiler context so unqualified sub calls - // resolve in the correct package (not main) + // Inherit package from the JVM compiler context so that eval STRING + // compiles unqualified names in the caller's package, not "main". + // This is compile-time package propagation — it sets the symbol table's + // current package so the parser/emitter qualify names correctly. + // Note: the *runtime* package (InterpreterState.currentPackage) is a + // separate concern used only by caller(); it must be scoped via + // DynamicVariableManager around eval execution to prevent leaking. + // See InterpreterState.currentPackage javadoc for details. if (ctx.symbolTable != null) { symbolTable.setCurrentPackage(ctx.symbolTable.getCurrentPackage(), ctx.symbolTable.currentPackageIsClass()); diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 5a2623301..9a9ee441f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -217,9 +217,14 @@ public static RuntimeList evalStringList(String perlCode, evalCode = evalCode.withCapturedVars(currentCode.capturedVars); } - // Step 6: Execute the compiled code - // Scope currentPackage (used only by caller()) so SET_PACKAGE - // inside the eval doesn't leak into the caller's state. + // Step 6: Execute the compiled code. + // IMPORTANT: Scope InterpreterState.currentPackage around eval execution. + // currentPackage is a runtime-only field used by caller() — it does NOT + // affect name resolution (which is fully compile-time). However, if the + // eval contains SET_PACKAGE opcodes (e.g. "package Foo;"), those would + // permanently mutate the caller's currentPackage without this scoping. + // We use DynamicVariableManager (same mechanism as PUSH_PACKAGE/POP_LOCAL_LEVEL) + // to save and restore it automatically. int pkgLevel = DynamicVariableManager.getLocalLevel(); DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); RuntimeArray args = new RuntimeArray(); // Empty @_ @@ -304,6 +309,7 @@ public static RuntimeScalar evalString(String perlCode, // Attach captured variables evalCode = evalCode.withCapturedVars(capturedVars); + // Scope currentPackage around eval — see Step 6 comment in evalStringHelper above. int pkgLevel = DynamicVariableManager.getLocalLevel(); DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); RuntimeArray args = new RuntimeArray(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java index b38e8ecf1..2e1494914 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java @@ -24,15 +24,29 @@ public class InterpreterState { /** * Thread-local RuntimeScalar holding the runtime current package name. * - * This is the single source of truth for the current package at runtime. - * It is used by: - * - caller() to return the correct calling package - * - eval STRING to compile code in the right package - * - SET_PACKAGE opcode (non-scoped: package Foo;) — sets it directly - * - PUSH_PACKAGE opcode (scoped: package Foo { }) — saves via DynamicVariableManager then sets + *

Design principle: Package is a compile-time concept for name + * resolution. All variable and subroutine names are fully qualified at compile time + * by the ScopedSymbolTable / BytecodeCompiler. This field exists only for + * runtime introspection — it does NOT affect name resolution.

* - * Scoped package blocks are automatically restored when the scope exits via - * the existing POP_LOCAL_LEVEL opcode (DynamicVariableManager.popToLocalLevel). + *

Used by:

+ *
    + *
  • {@code caller()} — to return the correct calling package
  • + *
  • {@code eval STRING} — to compile code in the right package (via + * BytecodeCompiler inheriting from ctx.symbolTable)
  • + *
  • {@code SET_PACKAGE} opcode ({@code package Foo;}) — sets it directly
  • + *
  • {@code PUSH_PACKAGE} opcode ({@code package Foo { }}) — saves via + * DynamicVariableManager then sets
  • + *
+ * + *

Eval scoping: Both eval STRING paths (EvalStringHandler for JVM bytecode, + * RuntimeCode for interpreter) must push/pop this field via DynamicVariableManager + * around eval execution. Without this, SET_PACKAGE opcodes inside the eval leak + * into the caller's package state, breaking caller() and subsequent eval compilations. + * This was the root cause of the signatures.t regression (601→446).

+ * + *

Scoped package blocks ({@code package Foo { }}) are automatically restored + * when the scope exits via POP_LOCAL_LEVEL (DynamicVariableManager.popToLocalLevel).

*/ public static final ThreadLocal currentPackage = ThreadLocal.withInitial(() -> new RuntimeScalar("main")); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index ed6580ef3..6371af1f6 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -742,7 +742,12 @@ public static RuntimeList evalStringWithInterpreter( List tokens = null; // Save dynamic variable level to restore after eval. - // Push currentPackage (used by caller()) so SET_PACKAGE inside eval doesn't leak. + // IMPORTANT: Scope InterpreterState.currentPackage around eval execution. + // This is the interpreter-side equivalent of the same scoping done in + // EvalStringHandler for the JVM bytecode path. Without this, SET_PACKAGE + // opcodes inside the eval permanently mutate the caller's package state, + // breaking caller() and subsequent eval compilations. + // See InterpreterState.currentPackage javadoc for the full design rationale. int dynamicVarLevel = DynamicVariableManager.getLocalLevel(); DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); From 7a82c54f9274c7ce490d3d5bc82c6bb51eca0150 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Sat, 28 Feb 2026 13:28:17 +0100 Subject: [PATCH 16/16] Fix eval package scoping: restore currentPackage after DynamicVariableManager push DynamicVariableManager.pushLocalVariable() calls dynamicSaveState() which clears the variable to UNDEF (Perl local semantics). For currentPackage scoping around eval, we need to preserve the current value - only restoring it when the eval exits. Save the package name before push, then re-set it immediately after. Fixes regressions: package_block.t (2->4), eval.t (141->152), caller.t (34->37), lex.t (99->102). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/backend/bytecode/EvalStringHandler.java | 4 ++++ .../java/org/perlonjava/frontend/parser/SignatureParser.java | 4 ++-- .../java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java | 2 ++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 9a9ee441f..1c5c619f8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -226,7 +226,9 @@ public static RuntimeList evalStringList(String perlCode, // We use DynamicVariableManager (same mechanism as PUSH_PACKAGE/POP_LOCAL_LEVEL) // to save and restore it automatically. int pkgLevel = DynamicVariableManager.getLocalLevel(); + String savedPkg = InterpreterState.currentPackage.get().toString(); DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); + InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); // Empty @_ RuntimeList result; try { @@ -311,7 +313,9 @@ public static RuntimeScalar evalString(String perlCode, // Scope currentPackage around eval — see Step 6 comment in evalStringHelper above. int pkgLevel = DynamicVariableManager.getLocalLevel(); + String savedPkg = InterpreterState.currentPackage.get().toString(); DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); + InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); RuntimeList result; try { diff --git a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java index 409b12510..7f88d7934 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java @@ -503,7 +503,7 @@ private Node generateTooFewArgsMessage() { new StringNode("Too few arguments for subroutine '" + fullName + "' (got ", parser.tokenIndex), argCount, parser.tokenIndex), - new StringNode("; expected at least ", parser.tokenIndex), + new StringNode(minParams == maxParams ? "; expected " : "; expected at least ", parser.tokenIndex), parser.tokenIndex), new NumberNode(Integer.toString(adjustedMin), parser.tokenIndex), parser.tokenIndex), @@ -539,7 +539,7 @@ private Node generateTooManyArgsMessage() { new StringNode("Too many arguments for subroutine '" + fullName + "' (got ", parser.tokenIndex), argCount, parser.tokenIndex), - new StringNode("; expected at most ", parser.tokenIndex), + new StringNode(minParams == maxParams ? "; expected " : "; expected at most ", parser.tokenIndex), parser.tokenIndex), new NumberNode(Integer.toString(adjustedMax), parser.tokenIndex), parser.tokenIndex), diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 6371af1f6..43f16e70b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -749,7 +749,9 @@ public static RuntimeList evalStringWithInterpreter( // breaking caller() and subsequent eval compilations. // See InterpreterState.currentPackage javadoc for the full design rationale. int dynamicVarLevel = DynamicVariableManager.getLocalLevel(); + String savedPkg = InterpreterState.currentPackage.get().toString(); DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); + InterpreterState.currentPackage.get().set(savedPkg); try { String evalString = code.toString();