From 84381ff2ab60beb0609102cb5aec6e8bbd1744c4 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 14:53:26 +0200 Subject: [PATCH 1/3] wip(regex): snapshot exact MARK publication work Preserve the JVM-complete MARK shorthand and exact-package publication delta before changing the interpreter match-package mechanism. Standard Perl and JVM pass 53/53; the interpreter currently passes 45/53. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- .../runtime/regex/JoniRegexPattern.java | 17 +- .../runtime/regex/RuntimeRegex.java | 21 +-- .../runtime/regex/JoniRegexPatternTest.java | 4 +- .../unit/regex/regex_mark_shorthand.t | 163 ++++++++++++++++++ third_party/joni/src/org/joni/Parser.java | 5 +- .../org/joni/test/TestPerlMarkShorthand.java | 87 ++++++++++ 6 files changed, 270 insertions(+), 27 deletions(-) create mode 100644 src/test/resources/unit/regex/regex_mark_shorthand.t create mode 100644 third_party/joni/test/org/joni/test/TestPerlMarkShorthand.java diff --git a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java index 38cb0a6f67..37e63c0669 100644 --- a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java +++ b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java @@ -408,13 +408,6 @@ private static String translatePattern(String pattern, RegexFlags flags, out.append(ch); continue; } - if (!inClass && pattern.startsWith("(*:", i)) { - // Perl's abbreviated MARK form is (*:NAME). Joni accepts the - // equivalent long spelling and publishes the mark normally. - out.append("(*MARK:"); - i += 2; - continue; - } if (inClass && flags.isExtendedWhitespace() && Character.isWhitespace(ch)) { continue; } @@ -865,7 +858,6 @@ private boolean find(int option, boolean anchored) { if (nextStart > regionEnd) { matched = false; committedLastClosedCapture = -1; - if (hasControlVerbState) RuntimeRegex.updateControlVerbVariables(null, null); return false; } matcher = regex.matcher(bytes); @@ -890,7 +882,7 @@ private boolean find(int option, boolean anchored) { throw failure; } matched = result >= 0; - if (hasControlVerbState || matcher.hasEncounteredControlVerb()) { + if (matcher.hasEncounteredControlVerb() || (hasControlVerbState && matched)) { RuntimeRegex.updateControlVerbVariables( matcher.getControlMark(), matcher.getControlError()); } @@ -1205,7 +1197,7 @@ private Evaluation evaluate(RuntimeRegexCallback callback, MatchView match) { CaptureSnapshot priorDynamicView = previousDynamicView; MatchView provisional = callback.kind == RuntimeRegexCallback.Kind.DYNAMIC ? dynamicCaptureView(match, priorDynamicView) : match; - publishProvisional(provisional); + publishProvisional(provisional, callback.lexicalPackage); if (callback.kind == RuntimeRegexCallback.Kind.DYNAMIC) { previousDynamicView = CaptureSnapshot.of(match); } @@ -1377,7 +1369,7 @@ private static void rejectEscapedControlFlow(RuntimeRegexCallback callback, throw new PerlCompilerException(marker.buildErrorMessage() + ".\n"); } - private void publishProvisional(MatchView match) { + private void publishProvisional(MatchView match, String lexicalPackage) { RuntimeRegexState state = PerlRuntime.current().regexState; state.lastParenMatchOverrideActive = false; state.lastParenMatchOverride = null; @@ -1412,7 +1404,8 @@ private void publishProvisional(MatchView match) { state.lastClosedCapture = lastClosed > 0 && lastClosed <= count ? state.lastCaptureGroups[lastClosed - 1] : null; if (publishesControlVerbState || match.controlMark() != null) { - RuntimeRegex.updateControlVerbVariables(match.controlMark(), null); + RuntimeRegex.updateControlVerbVariables( + lexicalPackage, match.controlMark(), null); } } diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index bec8aab452..073a075615 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -151,22 +151,19 @@ private static RuntimeRegexState state() { } static void updateControlVerbVariables(String mark, String error) { + updateControlVerbVariables( + InterpreterState.currentPackage.get().toString(), mark, error); + } + + static void updateControlVerbVariables(String packageName, String mark, String error) { RuntimeScalar markValue = mark == null ? RuntimeScalarCache.scalarEmptyString : new RuntimeScalar(mark); RuntimeScalar errorValue = error == null ? RuntimeScalarCache.scalarEmptyString : new RuntimeScalar(error); - // Perl activates these otherwise ordinary package variables through - // local(). The interpreter does not keep its runtime current-package - // facade synchronized with every lexical package statement, so use the - // localized scalar identities rather than guessing one package name. - for (Map.Entry entry - : DynamicVariableManager.activeLocalizedGlobalScalars().entrySet()) { - if (entry.getKey().endsWith("::REGMARK")) { - entry.getValue().set(markValue); - } else if (entry.getKey().endsWith("::REGERROR")) { - entry.getValue().set(errorValue); - } - } + String owner = packageName == null || packageName.isEmpty() ? "main" : packageName; + String separator = owner.endsWith("::") ? "" : "::"; + GlobalVariable.getGlobalVariable(owner + separator + "REGMARK").set(markValue); + GlobalVariable.getGlobalVariable(owner + separator + "REGERROR").set(errorValue); } // Compiled regex pattern (for byte strings - ASCII-only \w, \d) public Pattern pattern; diff --git a/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java b/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java index d6d5c65fe8..f4eff664c3 100644 --- a/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java +++ b/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java @@ -54,9 +54,9 @@ void translatesPerlInlineModifierSemantics() { } @Test - void translatesAbbreviatedMarkControlVerb() { + void routesAbbreviatedMarkControlVerbWithoutRewriting() { assertTrue(JoniRegexPattern.requiresJoniBackend("(*:B)A")); - assertEquals("(*MARK:B)A", JoniRegexPattern.translatePattern("(*:B)A")); + assertEquals("(*:B)A", JoniRegexPattern.translatePattern("(*:B)A")); } @Test diff --git a/src/test/resources/unit/regex/regex_mark_shorthand.t b/src/test/resources/unit/regex/regex_mark_shorthand.t new file mode 100644 index 0000000000..d527ed964a --- /dev/null +++ b/src/test/resources/unit/regex/regex_mark_shorthand.t @@ -0,0 +1,163 @@ +use strict; +use warnings; +use Test::More; + +our ($REGMARK, $REGERROR); +local $REGMARK; +local $REGERROR; + +my $empty = eval q{ qr/(*:)/; 1 }; +ok(!$empty, 'empty MARK shorthand label is rejected'); +like($@, qr/(?:mandatory argument|unrecognized|unknown|group option)/i, + 'empty shorthand reports a MARK or group diagnostic'); + +my $unterminated = eval q{ qr/(*:unterminated/; 1 }; +ok(!$unterminated, 'unterminated MARK shorthand label is rejected'); +like($@, qr/(?:unterminated|not terminated|unmatched|end pattern|group option)/i, + 'unterminated shorthand reports a structural diagnostic'); + +ok('ac' =~ /a(*:first)b|a(*:second)c/, + 'MARK shorthand allows an ordinary match through a later branch'); +is($REGMARK, 'second', 'successful branch publishes its shorthand label'); +is($REGERROR, '', 'successful shorthand match clears REGERROR'); + +ok('ac' !~ /a(*:failed)(*FAIL)/, 'shorthand MARK path can fail'); +is($REGMARK, '', 'failed shorthand match clears REGMARK'); +is($REGERROR, 'failed', 'failed shorthand path publishes REGERROR'); + +ok('aab' =~ /a(*:after-a)b(*SKIP:after-a)(*FAIL)|b/, + 'named SKIP finds a shorthand MARK'); +is($-[0], 2, 'named SKIP resumes at the shorthand MARK position'); + +my $single = 'A'; +is($single =~ s/(*:B)A/$REGMARK/, 1, + 'shorthand MARK is visible to a substitution replacement'); +is($single, 'B', 'single substitution uses the shorthand label'); + +my $global = 'CCCCBAA'; +is($global =~ s/(*:X)A+|(*:Y)B+|(*:Z)C+/$REGMARK/g, 3, + 'global substitution selects three shorthand branches'); +is($global, 'ZYX', 'global substitution publishes each shorthand label'); + +my $long = 'CCCCBAA'; +is($long =~ s/(*:X)A+|(*:YYYYYYYYYYYYYYYY)B+|(*:Z)C+/$REGMARK/g, 3, + 'global substitution accepts a long shorthand label'); +is($long, 'ZYYYYYYYYYYYYYYYYX', + 'long shorthand label survives replacement exactly'); + +our @callback_marks; +ok('foo' =~ /foo(*:callback)(?{ push @callback_marks, $REGMARK })/, + 'shorthand MARK remains visible inside a regex callback'); +is_deeply(\@callback_marks, ['callback'], + 'callback observes the provisional shorthand REGMARK'); + +our @nested_marks; +my $inner_mark = qr/(*:inner)i/; +ok('outer' =~ /(*:outer)o(?{ + 'i' =~ $inner_mark; + push @nested_marks, $REGMARK; + })uter/x, + 'nested regex with shorthand MARK succeeds inside a callback'); +is_deeply(\@nested_marks, ['inner'], + 'nested regex publishes its own shorthand MARK during the callback'); +is($REGMARK, 'outer', + 'outer shorthand MARK is restored when the outer match completes'); + +my $iterator = 'a'; +ok($iterator =~ /a(*:once)/g, 'global shorthand iterator succeeds once'); +is($REGMARK, 'once', 'successful iterator publishes its shorthand MARK'); +ok(!($iterator =~ /a(*:once)/g), 'global shorthand iterator exhausts'); +is($REGMARK, 'once', + 'exhausted shorthand iterator preserves the last successful REGMARK'); +is($REGERROR, '', 'exhausted shorthand iterator clears REGERROR'); + +ok('a' =~ /a(*:prior-success)/, + 'state-preservation setup match succeeds'); +is($REGMARK, 'prior-success', 'setup publishes the prior successful MARK'); +ok('b' !~ /a(*:unreached)/, + 'later failure occurs before reaching its shorthand MARK'); +is($REGMARK, 'prior-success', + 'failed attempt before MARK preserves the prior successful REGMARK'); + +{ + package RegexMarkShorthandOther; + our ($REGMARK, $REGERROR); + local $REGMARK; + local $REGERROR; + ::ok('p' =~ /(*:package)p/, 'shorthand MARK works in another package'); + ::is($REGMARK, 'package', + 'shorthand REGMARK publication remains package-local'); + ::is($REGERROR, '', 'package-local shorthand success clears REGERROR'); +} + +{ + package RegexMarkShorthandUnlocalized; + our ($REGMARK, $REGERROR); + + my $single = 'A'; + ::is($single =~ s/(*:B)A/$REGMARK/, 1, + 'unlocalized shorthand replacement matches once'); + ::is($single, 'B', + 'unlocalized package REGMARK is visible to replacement'); + + my $global = 'CCCCBAA'; + ::is($global =~ s/(*:X)A+|(*:Y)B+|(*:Z)C+/$REGMARK/g, 3, + 'unlocalized global replacement selects three branches'); + ::is($global, 'ZYX', + 'unlocalized global replacement publishes each label'); + + my $long = 'CCCCBAA'; + ::is($long =~ s/(*:X)A+|(*:YYYYYYYYYYYYYYYY)B+|(*:Z)C+/$REGMARK/g, 3, + 'unlocalized replacement accepts a long label'); + ::is($long, 'ZYYYYYYYYYYYYYYYYX', + 'unlocalized long label survives replacement exactly'); +} + +{ + package RegexMarkPublicationA; + our ($REGMARK, $REGERROR) = ('A-before', 'AE-before'); + + package RegexMarkPublicationB; + our ($REGMARK, $REGERROR) = ('B-before', 'BE-before'); + sub match_in_b { 'b' =~ /(*:only-b)b/ } + + ::ok(match_in_b(), 'match executes in its lexical package'); + ::is($REGMARK, 'only-b', 'executing package receives REGMARK'); + ::is($RegexMarkPublicationA::REGMARK, 'A-before', + 'unrelated package REGMARK is not broadcast'); + ::ok('b' !~ /(*:failed-b)(*FAIL)/, + 'failed match publishes state in the executing package'); + ::is($REGERROR, 'failed-b', 'executing package receives REGERROR'); + ::is($RegexMarkPublicationA::REGERROR, 'AE-before', + 'unrelated package REGERROR is not broadcast'); +} + +$REGMARK = 'main-sentinel'; +{ + local $RegexMarkPublicationA::REGMARK = 'A-local'; + RegexMarkPublicationB::match_in_b(); + is($RegexMarkPublicationA::REGMARK, 'A-local', + 'unrelated localized REGMARK remains unchanged'); + is($REGMARK, 'main-sentinel', + 'active caller localization is not globally broadcast'); +} + +{ + package RegexMarkCallbackA; + our ($REGMARK, $REGERROR) = ('callback-A-before', 'callback-AE-before'); + our @SEEN; + our $QR = qr/(*:cross-package)(?{ push @SEEN, $REGMARK })A/; + + package RegexMarkCallbackB; + our ($REGMARK, $REGERROR) = ('callback-B-before', 'callback-BE-before'); + ::ok('A' =~ $RegexMarkCallbackA::QR, + 'cross-package qr callback match succeeds'); + ::is_deeply(\@RegexMarkCallbackA::SEEN, ['cross-package'], + 'callback lexical package sees provisional REGMARK'); + ::is($RegexMarkCallbackA::REGMARK, 'cross-package', + 'callback lexical package retains provisional REGMARK'); + ::is($REGMARK, 'cross-package', + 'outer executing package receives final REGMARK'); +} + +done_testing; diff --git a/third_party/joni/src/org/joni/Parser.java b/third_party/joni/src/org/joni/Parser.java index 247be8edce..3817616f24 100644 --- a/third_party/joni/src/org/joni/Parser.java +++ b/third_party/joni/src/org/joni/Parser.java @@ -898,7 +898,10 @@ private Node parseControlVerb() { final ControlVerbNode.Kind kind; final String verb; - if (startsWith("ACCEPT)")) { + if (startsWith(":")) { + kind = ControlVerbNode.Kind.MARK; + verb = ""; + } else if (startsWith("ACCEPT)")) { kind = ControlVerbNode.Kind.ACCEPT; verb = "ACCEPT"; } else if (startsWith("FAIL)")) { diff --git a/third_party/joni/test/org/joni/test/TestPerlMarkShorthand.java b/third_party/joni/test/org/joni/test/TestPerlMarkShorthand.java new file mode 100644 index 0000000000..0b22bfa569 --- /dev/null +++ b/third_party/joni/test/org/joni/test/TestPerlMarkShorthand.java @@ -0,0 +1,87 @@ +/* + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to + * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + * of the Software, and to permit persons to whom the Software is furnished to do + * so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.joni.test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertThrows; + +import java.nio.charset.StandardCharsets; + +import org.jcodings.specific.ASCIIEncoding; +import org.joni.Matcher; +import org.joni.Option; +import org.joni.Regex; +import org.joni.Syntax; +import org.joni.exception.SyntaxException; +import org.junit.Test; + +public class TestPerlMarkShorthand { + private static Regex regex(String pattern) { + byte[] bytes = pattern.getBytes(StandardCharsets.US_ASCII); + return new Regex(bytes, 0, bytes.length, Option.NONE, + ASCIIEncoding.INSTANCE, Syntax.RUBY); + } + + private static Matcher matcher(String pattern, String input) { + return regex(pattern).matcher(input.getBytes(StandardCharsets.US_ASCII)); + } + + @Test + public void shorthandReportsTheSuccessfulBranchAndRestoresOnBacktrack() { + Matcher matcher = matcher("a(*:first)b|a(*:second)c", "ac"); + + assertEquals(0, matcher.search(0, 2, Option.NONE)); + assertEquals(2, matcher.getEnd()); + assertEquals("second", matcher.getControlMark()); + assertNull(matcher.getControlError()); + } + + @Test + public void failedShorthandPathPublishesItsLabelAsTheControlError() { + Matcher matcher = matcher("a(*:failed)(*FAIL)", "ac"); + + assertEquals(-1, matcher.search(0, 2, Option.NONE)); + assertNull(matcher.getControlMark()); + assertEquals("failed", matcher.getControlError()); + } + + @Test + public void namedSkipFindsAShorthandMark() { + Matcher matcher = matcher("a(*:after-a)b(*SKIP:after-a)(*FAIL)|b", "aab"); + + assertEquals(2, matcher.search(0, 3, Option.NONE)); + assertEquals(3, matcher.getEnd()); + } + + @Test + public void shorthandRetainsALongLabelExactly() { + Matcher matcher = matcher("(*:YYYYYYYYYYYYYYYY)B", "B"); + + assertEquals(0, matcher.search(0, 1, Option.NONE)); + assertEquals("YYYYYYYYYYYYYYYY", matcher.getControlMark()); + } + + @Test + public void shorthandRejectsEmptyAndUnterminatedLabels() { + assertThrows(SyntaxException.class, () -> regex("(*:)")); + assertThrows(SyntaxException.class, () -> regex("(*:unterminated")); + } +} From 6593c5d9dcac8b67a06bc6c45cbe805a3774f3b0 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 14:54:37 +0200 Subject: [PATCH 2/3] fix(regex): carry interpreter match package explicitly Encode the lexical package on MATCH_REGEX and MATCH_REGEX_NOT bytecodes and scope InterpreterState.currentPackage around runtime matching. This gives MARK and REGERROR an exact executing-package owner while preserving callback lexical package overrides and nested-match restoration. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- .../backend/bytecode/BytecodeInterpreter.java | 7 +-- .../bytecode/CompileBinaryOperatorHelper.java | 4 ++ .../backend/bytecode/CompileOperator.java | 2 + .../backend/bytecode/Disassemble.java | 6 ++- .../bytecode/OpcodeHandlerExtended.java | 47 ++++++++++++------- .../perlonjava/backend/bytecode/Opcodes.java | 4 +- 6 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index ef5d117bd2..4b1bc62755 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2096,14 +2096,15 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { case Opcodes.MATCH_REGEX -> { // Match regex - // Format: MATCH_REGEX rd stringReg regexReg ctx bytesMode targetNameIndex + // Format: MATCH_REGEX rd stringReg regexReg ctx bytesMode targetNameIndex packageNameIndex pc = OpcodeHandlerExtended.executeMatchRegex(bytecode, pc, registers, code); } case Opcodes.MATCH_REGEX_NOT -> { // Negated regex match - // Format: MATCH_REGEX_NOT rd stringReg regexReg ctx - pc = OpcodeHandlerExtended.executeMatchRegexNot(bytecode, pc, registers); + // Format: MATCH_REGEX_NOT rd stringReg regexReg ctx packageNameIndex + pc = OpcodeHandlerExtended.executeMatchRegexNot( + bytecode, pc, registers, code); } case Opcodes.CHOMP -> { diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java index 456c9b96bc..6b4a005eaf 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java @@ -382,6 +382,8 @@ private static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); bytecodeCompiler.emit(0); bytecodeCompiler.emit(-1); + bytecodeCompiler.emit(bytecodeCompiler.addToStringPool( + bytecodeCompiler.getCurrentPackage())); } case "!~" -> { // $string !~ /pattern/ - negated regex match @@ -392,6 +394,8 @@ private static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + bytecodeCompiler.emit(bytecodeCompiler.addToStringPool( + bytecodeCompiler.getCurrentPackage())); } case "&" -> { // Numeric bitwise AND (default): rs1 & rs2 diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 12e0835bfe..cfba6100a2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -359,6 +359,7 @@ private static void visitMatchRegex(BytecodeCompiler bc, OperatorNode node) { bc.emit(bc.isBytesEnabled() ? 1 : 0); bc.emit(regexTargetNameIndex(bc, args.elements.size() > 2 ? args.elements.get(2) : null)); + bc.emit(bc.addToStringPool(bc.getCurrentPackage())); bc.lastResultReg = rd; } @@ -402,6 +403,7 @@ private static void visitReplaceRegex(BytecodeCompiler bc, OperatorNode node) { bc.emit(bc.isBytesEnabled() ? 1 : 0); bc.emit(regexTargetNameIndex(bc, args.elements.size() > 3 ? args.elements.get(3) : null)); + bc.emit(bc.addToStringPool(bc.getCurrentPackage())); bc.lastResultReg = rd; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index 29f96d5f0e..dd649366d5 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -845,14 +845,16 @@ public static String disassemble(InterpretedCode interpretedCode) { int matchCtx = interpretedCode.bytecode[pc++]; int bytesMode = interpretedCode.bytecode[pc++]; int targetNameIndex = interpretedCode.bytecode[pc++]; - sb.append("MATCH_REGEX r").append(rd).append(" = r").append(strReg).append(" =~ r").append(regReg).append(" (ctx=").append(matchCtx).append(", bytes=").append(bytesMode).append(", targetName=").append(targetNameIndex).append(")\n"); + int matchPackageIndex = interpretedCode.bytecode[pc++]; + sb.append("MATCH_REGEX r").append(rd).append(" = r").append(strReg).append(" =~ r").append(regReg).append(" (ctx=").append(matchCtx).append(", bytes=").append(bytesMode).append(", targetName=").append(targetNameIndex).append(", package=").append(interpretedCode.stringPool[matchPackageIndex]).append(")\n"); break; case Opcodes.MATCH_REGEX_NOT: rd = interpretedCode.bytecode[pc++]; strReg = interpretedCode.bytecode[pc++]; regReg = interpretedCode.bytecode[pc++]; matchCtx = interpretedCode.bytecode[pc++]; - sb.append("MATCH_REGEX_NOT r").append(rd).append(" = r").append(strReg).append(" !~ r").append(regReg).append(" (ctx=").append(matchCtx).append(")\n"); + matchPackageIndex = interpretedCode.bytecode[pc++]; + sb.append("MATCH_REGEX_NOT r").append(rd).append(" = r").append(strReg).append(" !~ r").append(regReg).append(" (ctx=").append(matchCtx).append(", package=").append(interpretedCode.stringPool[matchPackageIndex]).append(")\n"); break; case Opcodes.CHOMP: rd = interpretedCode.bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index c0b1bc23d2..74066fc890 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -908,7 +908,7 @@ public static int executeReadline(int[] bytecode, int pc, RuntimeBase[] register /** * Execute match regex operation. - * Format: MATCH_REGEX rd stringReg regexReg ctx bytesMode targetNameIndex + * Format: MATCH_REGEX rd stringReg regexReg ctx bytesMode targetNameIndex packageNameIndex */ public static int executeMatchRegex(int[] bytecode, int pc, RuntimeBase[] registers, InterpretedCode code) { @@ -918,6 +918,7 @@ public static int executeMatchRegex(int[] bytecode, int pc, RuntimeBase[] regist int ctx = bytecode[pc++]; boolean bytesMode = bytecode[pc++] != 0; int targetNameIndex = bytecode[pc++]; + int packageNameIndex = bytecode[pc++]; RegexQuoteMeta.setMatchTargetName(targetNameIndex >= 0 && targetNameIndex < code.stringPool.length @@ -926,37 +927,47 @@ public static int executeMatchRegex(int[] bytecode, int pc, RuntimeBase[] regist if (ctx == RuntimeContextType.RUNTIME) ctx = ((RuntimeScalar) registers[2]).getInt(); RuntimeScalar regex = registers[regexReg].scalar(); RuntimeScalar string = registers[stringReg].scalar(); - if (bytesMode) { - registers[rd] = RuntimeRegex.matchRegexBytes( - regex, - string, - ctx); - } else { - registers[rd] = RuntimeRegex.matchRegex( - regex, - string, - ctx); + RuntimeScalar currentPackage = InterpreterState.currentPackage.get(); + String savedPackage = currentPackage.toString(); + currentPackage.set(code.stringPool[packageNameIndex]); + try { + if (bytesMode) { + registers[rd] = RuntimeRegex.matchRegexBytes(regex, string, ctx); + } else { + registers[rd] = RuntimeRegex.matchRegex(regex, string, ctx); + } + } finally { + currentPackage.set(savedPackage); } return pc; } /** * Execute negated match regex operation. - * Format: MATCH_REGEX_NOT rd stringReg regexReg ctx + * Format: MATCH_REGEX_NOT rd stringReg regexReg ctx packageNameIndex */ - public static int executeMatchRegexNot(int[] bytecode, int pc, RuntimeBase[] registers) { + public static int executeMatchRegexNot(int[] bytecode, int pc, RuntimeBase[] registers, + InterpretedCode code) { int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; int regexReg = bytecode[pc++]; int ctx = bytecode[pc++]; + int packageNameIndex = bytecode[pc++]; RegexQuoteMeta.setMatchTargetName(null); if (ctx == RuntimeContextType.RUNTIME) ctx = ((RuntimeScalar) registers[2]).getInt(); - RuntimeBase matchResult = RuntimeRegex.matchRegex( - (RuntimeScalar) registers[regexReg], - (RuntimeScalar) registers[stringReg], - ctx - ); + RuntimeScalar currentPackage = InterpreterState.currentPackage.get(); + String savedPackage = currentPackage.toString(); + RuntimeBase matchResult; + currentPackage.set(code.stringPool[packageNameIndex]); + try { + matchResult = RuntimeRegex.matchRegex( + (RuntimeScalar) registers[regexReg], + (RuntimeScalar) registers[stringReg], + ctx); + } finally { + currentPackage.set(savedPackage); + } // Negate the boolean result registers[rd] = new RuntimeScalar(matchResult.scalar().getBoolean() ? 0 : 1); return pc; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 08ccb49c8a..dc967c4496 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1003,7 +1003,7 @@ public class Opcodes { /** * Match regex: rd = RuntimeRegex.matchRegex(string, regex, ctx) - * Format: MATCH_REGEX rd stringReg regexReg ctx bytesMode target_name_index + * Format: MATCH_REGEX rd stringReg regexReg ctx bytesMode target_name_index package_name_index */ public static final short MATCH_REGEX = 167; @@ -1255,7 +1255,7 @@ public class Opcodes { /** * Match regex (negated): rd = !RuntimeRegex.matchRegex(string, regex, ctx) - * Format: MATCH_REGEX_NOT rd stringReg regexReg ctx + * Format: MATCH_REGEX_NOT rd stringReg regexReg ctx package_name_index */ public static final short MATCH_REGEX_NOT = 217; From 47453947a9e9adf1c459c020040ee4268be268ce Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 15:05:42 +0200 Subject: [PATCH 3/3] fix(interpreter): refresh list-form our package aliases Apply the existing cross-package our-alias refresh to list declarations as well as scalar declarations. Later bare reads now resolve to the package of the most recent lexical our declaration instead of the preceding package. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- .../backend/bytecode/BytecodeCompiler.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index c36c33db27..78be47497f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3926,6 +3926,22 @@ void compileVariableDeclaration(OperatorNode node, String op) { if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; + // Match the single-variable declaration path: a later + // list-form `our` under another package establishes a + // new lexical alias for the same bare name. Reusing the + // old entry would make subsequent reads keep loading the + // preceding package's global even though this declaration + // itself loaded the correct package variable. + if (hasVariable(varName) && isOurVariable(varName)) { + SymbolTable.SymbolEntry entry = + symbolTable.getSymbolEntry(varName); + if (entry != null + && !getCurrentPackage().equals( + entry.perlPackage())) { + addVariable(varName, "our"); + } + } + int reg; // Check if already declared in current scope if (hasVariable(varName)) {