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)) { 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; 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")); + } +}