diff --git a/dev/design/phase36-regex-parity.md b/dev/design/phase36-regex-parity.md index 9beb32958..752169ff2 100644 --- a/dev/design/phase36-regex-parity.md +++ b/dev/design/phase36-regex-parity.md @@ -92,13 +92,10 @@ affected corpus before taking another slice. - `master` contains the validated named-character diagnostics plus native `(?(DEFINE)...)`, ordinary lookbehind, branch reset, and plain `\N`; its exact head passed warning-free local, Ubuntu, and Windows gates. -- The successor integration batch carries byte/Unicode provenance and fold - policy, dotted-U+ diagnostics, the ordinary-pattern Joni default, a dynamic- - pattern edge contract, and the first obsolete import retirement. Its fold, - property, and resolver-cache residuals are closed; the exact semantic head - passes a warning-free 17-task `make`. The prospective PR head also includes - three independent runtime-diagnostic corrections and passes the same full - combined gate. +- Byte/Unicode provenance and fold policy, dotted-U+ diagnostics, the ordinary- + pattern Joni default, a dynamic-pattern edge contract, and the first obsolete + import retirement are integrated. Fold, property, resolver-cache, and the + classified runtime-diagnostic residuals are closed. - Native DEFINE, ordinary lookbehind, and branch reset now route through Joni; their feature-specific Java rewrites and branch-reset capture-map adapter are deleted. Plain Perl `\N` is a native Joni non-line-feed atom, including @@ -118,6 +115,16 @@ affected corpus before taking another slice. evidence. - Exact `/aa` routing/folding gates pass on native Joni, and the Java `/aa` workaround is removed. +- Perl grouped nested-quantifier semantics and extended-mode quantifier + modifiers are native Joni behavior; the exact `regexp.t` differential removes + seven failures with no introductions. +- Named `(*ACCEPT:NAME)`, `(*FAIL:NAME)`, and `(*F:NAME)` carry control state + through native Joni bytecode and publish Perl-compatible `$REGMARK` and + `$REGERROR`; the exact `regexp.t` differential removes three failures with no + introductions. +- Negative lookbehind accepts capture enclosures and uses ACCEPT-aware width + analysis in native Joni; named cut errors remain authoritative before an + unnamed FAIL. The combined exact `regexp.t` differential has no introductions. ## Execution Phases @@ -229,8 +236,9 @@ behavior. ## Ordered Next Steps -1. Open the validated successor review PR against `master` and require - exact-head Ubuntu/Windows CI. +1. Run one warning-free full build and affected-corpus differential on the + integrated nested-quantifier, named-control-verb, and negative-lookbehind + batch, then open its review PR and require exact-head Ubuntu/Windows CI. 2. Complete byte/Unicode pattern provenance through runtime interpolation and template composition, then finish `/d`/`u`/`a`/`aa` forward/reverse literal and backreference folding from generated data. Require direct Joni plus @@ -240,8 +248,8 @@ behavior. unwind, backtracking re-evaluation, and recursion safety. Route every embedded closure to Joni and delete constant inlining, progressive errors, and the dynamic Java adapter as their gates pass. -4. Carry lexical `use re 'strict'` policy through regex compilation and close - the unescaped-brace/non-hex diagnostic families. Refresh complete +4. Finish the remaining lexical `use re 'strict'`, unescaped-brace, and non-hex + diagnostic families. Refresh complete `reg_mesg.t`, `pat.t`, and `pat_advanced.t` maps after each combined batch. 5. Repeat the four-leg 80-file Java/Joni × JVM/interpreter matrix on the exact successor artifact and compare every file with the PR 958 log. Resolve every diff --git a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java index 51ed7f54b..66065f597 100644 --- a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java +++ b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java @@ -519,6 +519,10 @@ static boolean requiresJoniBackend(String pattern, RegexFlags flags) { || pattern.contains("(?{=DYNAMIC:") || containsNamedCharacterEscape(pattern) || pattern.contains("(*ACCEPT)") + || pattern.contains("(*ACCEPT:") + || pattern.contains("(*FAIL") + || pattern.contains("(*F)") + || pattern.contains("(*F:") || pattern.contains("(*PRUNE") || pattern.contains("(*SKIP") || pattern.contains("(*THEN") @@ -719,6 +723,8 @@ private static PerlSyntaxFeatures analyzePerlSyntax(String pattern, boolean exte private static boolean hasControlVerbState(String pattern) { return pattern.contains("(*MARK") || pattern.contains("(*:") + || pattern.contains("(*ACCEPT") || pattern.contains("(*FAIL") + || pattern.contains("(*F)") || pattern.contains("(*F:") || pattern.contains("(*PRUNE") || pattern.contains("(*SKIP") || pattern.contains("(*THEN") || pattern.contains("(*COMMIT"); @@ -1077,7 +1083,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); @@ -1103,9 +1108,12 @@ private boolean find(int option, boolean anchored) { throw failure; } matched = result >= 0; - if (hasControlVerbState || matcher.hasEncounteredControlVerb()) { + boolean encounteredControlVerb = matcher.hasEncounteredControlVerb(); + if ((matched && hasControlVerbState) || encounteredControlVerb) { + String mark = matcher.getControlMark(); + if (matched && mark == null) mark = "1"; RuntimeRegex.updateControlVerbVariables( - matcher.getControlMark(), matcher.getControlError()); + mark, matcher.getControlError()); } if (calloutHandler != null) calloutHandler.finish(matched); if (!matched) { diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 733637924..fbeeb788a 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -158,10 +158,14 @@ static void updateControlVerbVariables(String mark, String error) { ? RuntimeScalarCache.scalarEmptyString : new RuntimeScalar(mark); RuntimeScalar errorValue = error == null ? RuntimeScalarCache.scalarEmptyString : new RuntimeScalar(error); + String currentPackage = InterpreterState.currentPackage.get().toString(); + if (currentPackage == null || currentPackage.isEmpty()) currentPackage = "main"; + GlobalVariable.getGlobalVariable(currentPackage + "::REGMARK").set(markValue); + GlobalVariable.getGlobalVariable(currentPackage + "::REGERROR").set(errorValue); // 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. + // local(). Also update localized scalar identities directly because the + // interpreter does not keep its runtime current-package facade + // synchronized with every lexical package statement. for (Map.Entry entry : DynamicVariableManager.activeLocalizedGlobalScalars().entrySet()) { if (entry.getKey().endsWith("::REGMARK")) { diff --git a/src/test/java/org/perlonjava/runtime/regex/JoniNestedQuantifierPatternTest.java b/src/test/java/org/perlonjava/runtime/regex/JoniNestedQuantifierPatternTest.java new file mode 100644 index 000000000..37c46dbfc --- /dev/null +++ b/src/test/java/org/perlonjava/runtime/regex/JoniNestedQuantifierPatternTest.java @@ -0,0 +1,18 @@ +package org.perlonjava.runtime.regex; + +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.joni.exception.SyntaxException; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("unit") +class JoniNestedQuantifierPatternTest { + @Test + void rejectsNestedIntervalModifiersBeforeRuntimeWrapping() { + assertThrows(SyntaxException.class, () -> new JoniRegexPattern( + ".{1}??", RegexFlags.fromModifiers("", ".{1}??"))); + assertThrows(SyntaxException.class, () -> new JoniRegexPattern( + ".{1}?+", RegexFlags.fromModifiers("", ".{1}?+"))); + } +} diff --git a/src/test/resources/unit/regex/named_accept_fail_control_verbs.t b/src/test/resources/unit/regex/named_accept_fail_control_verbs.t new file mode 100644 index 000000000..d7334d677 --- /dev/null +++ b/src/test/resources/unit/regex/named_accept_fail_control_verbs.t @@ -0,0 +1,36 @@ +use strict; +use warnings; +use Test::More; + +our ($REGMARK, $REGERROR); +$REGMARK = undef; +$REGERROR = undef; + +ok('ab' =~ /a(*ACCEPT:accepted)z/, 'named ACCEPT ends the current match'); +is($&, 'a', 'named ACCEPT preserves its match boundary'); +is($REGMARK, 'accepted', 'named ACCEPT publishes its argument'); +is($REGERROR, '', 'named ACCEPT clears REGERROR'); + +ok('ac' !~ /a(*FAIL:blocked)c/, 'named FAIL rejects the match'); +is($REGMARK, '', 'named FAIL clears REGMARK after failure'); +is($REGERROR, 'blocked', 'named FAIL publishes its argument'); + +ok('ac' !~ /a(*F:short)c/, 'named F shorthand rejects the match'); +is($REGERROR, 'short', 'named F publishes its argument'); + +ok('ab' =~ /a(*FAIL:first)b|ab/, 'named FAIL can backtrack to a successful branch'); +is($REGMARK, '1', 'successful controlled match without a mark publishes true'); +is($REGERROR, '', 'success clears a backtracked named FAIL argument'); + +ok('ab' =~ /(?=(a(*ACCEPT:inner)z))ab/, + 'named ACCEPT respects a nested assertion boundary'); +is($1, 'a', 'nested named ACCEPT closes its active capture'); +is($REGMARK, 'inner', 'nested named ACCEPT publishes its argument'); + +$REGMARK = 'sentinel mark'; +$REGERROR = 'sentinel error'; +ok('x' !~ /z|a(*FAIL:unreached)/, 'pattern can fail before reaching named FAIL'); +is($REGMARK, 'sentinel mark', 'unreached control verb leaves REGMARK untouched on failure'); +is($REGERROR, 'sentinel error', 'unreached control verb leaves REGERROR untouched on failure'); + +done_testing; diff --git a/src/test/resources/unit/regex/negative_lookbehind_capture_accept.t b/src/test/resources/unit/regex/negative_lookbehind_capture_accept.t new file mode 100644 index 000000000..c4bdb2dd6 --- /dev/null +++ b/src/test/resources/unit/regex/negative_lookbehind_capture_accept.t @@ -0,0 +1,18 @@ +use strict; +use warnings; +use Test::More; + +my $accept = qr/(? OPSize.ACCEPT; - case FAIL -> OPSize.FAIL; + case FAIL -> OPSize.CONTROL_FAIL; case PRUNE -> OPSize.PRUNE; case SKIP -> OPSize.SKIP; case THEN -> OPSize.THEN; diff --git a/third_party/joni/src/org/joni/ByteCodeMachine.java b/third_party/joni/src/org/joni/ByteCodeMachine.java index f38475ab5..8219c88f5 100644 --- a/third_party/joni/src/org/joni/ByteCodeMachine.java +++ b/third_party/joni/src/org/joni/ByteCodeMachine.java @@ -389,6 +389,7 @@ private final int execute(final boolean checkThreadInterrupt) throws Interrupted case OPCode.CHECK_LOOK_BEHIND_END: opCheckLookBehindEnd(); continue; case OPCode.FINISH: return finish(); case OPCode.FAIL: opFail(); continue; + case OPCode.CONTROL_FAIL: opControlFail(); continue; case OPCode.CALLOUT: opCallout(); continue; case OPCode.CALLOUT_CONDITION: opCalloutCondition(); continue; case OPCode.DYNAMIC_CALLOUT: opDynamicCallout(); continue; @@ -559,6 +560,7 @@ private final int executeSb(final boolean checkThreadInterrupt) throws Interrupt case OPCode.CHECK_LOOK_BEHIND_END: opCheckLookBehindEnd(); continue; case OPCode.FINISH: return finish(); case OPCode.FAIL: opFail(); continue; + case OPCode.CONTROL_FAIL: opControlFail(); continue; case OPCode.CALLOUT: opCallout(); continue; case OPCode.CALLOUT_CONDITION: opCalloutCondition(); continue; case OPCode.DYNAMIC_CALLOUT: opDynamicCallout(); continue; @@ -2949,6 +2951,9 @@ private void opDynamicCallout() { * are not boundaries; calls and assertions are, matching Perl's behavior. */ private boolean opAccept() { + controlVerbEncountered = true; + String name = controlVerbName(code[ip++]); + if (name != null) controlMark = name; int callDepth = 0; for (int i = stk - 1; i >= 0; i--) { StackEntry entry = stack[i]; @@ -2989,6 +2994,18 @@ private boolean opAccept() { return opEnd(); } + private void opControlFail() { + controlVerbEncountered = true; + String name = controlVerbName(code[ip++]); + // An unnamed FAIL terminates the current path without replacing a + // more specific PRUNE/SKIP/THEN/COMMIT error already encountered on + // that path. A named FAIL remains authoritative. + if (name != null || controlError == null) { + controlError = name == null ? "1" : name; + } + opFail(); + } + private void opPrune() { controlVerbEncountered = true; String name = controlVerbName(code[ip++]); diff --git a/third_party/joni/src/org/joni/Lexer.java b/third_party/joni/src/org/joni/Lexer.java index 0596502fe..f0599750e 100644 --- a/third_party/joni/src/org/joni/Lexer.java +++ b/third_party/joni/src/org/joni/Lexer.java @@ -20,6 +20,7 @@ package org.joni; import static org.joni.Option.isAsciiRange; +import static org.joni.Option.isExtend; import static org.joni.Option.isSingleline; import static org.joni.Option.isWordBoundAllRange; import static org.joni.ast.QuantifierNode.isRepeatInfinite; @@ -1272,7 +1273,7 @@ private void fetchTokenFor_openBrace() { greedyCheck(); break; case 2: - if (syntax.fixedIntervalIsGreedyOnly()) { + if (syntax.fixedIntervalIsGreedyOnly() && !syntax.op2OptionPerl()) { possessiveCheck(); } else { greedyCheck(); @@ -2032,18 +2033,20 @@ private boolean fetchTokenForPerlBoundary(boolean negated) { } private void greedyCheck() { + skipPerlExtendedQuantifierSpace(); if (left() && peekIs('?') && syntax.opQMarkNonGreedy()) { - fetch(); token.setRepeatGreedy(false); token.setRepeatPossessive(false); + rejectPerlNestedQuantifierModifier(); } else { possessiveCheck(); } } private void possessiveCheck() { + skipPerlExtendedQuantifierSpace(); if (left() && peekIs('+') && (syntax.op2PlusPossessiveRepeat() && token.type != TokenType.INTERVAL || syntax.op2PlusPossessiveInterval() && token.type == TokenType.INTERVAL)) { @@ -2052,12 +2055,38 @@ private void possessiveCheck() { token.setRepeatGreedy(true); token.setRepeatPossessive(true); + rejectPerlNestedQuantifierModifier(); } else { token.setRepeatGreedy(true); token.setRepeatPossessive(false); } } + private void rejectPerlNestedQuantifierModifier() { + if (!env.usesPerlDiagnostics() || !left()) return; + int next = peek(); + if (next == '?' || next == '*' || next == '+') { + newSyntaxException(PERL_NESTED_QUANTIFIERS); + } + } + + private void skipPerlExtendedQuantifierSpace() { + if (!syntax.op2OptionPerl() || !isExtend(env.option)) return; + while (left()) { + int next = peek(); + if (next == ' ' || next == '\t' || next == '\n' + || next == '\r' || next == '\f') { + inc(); + continue; + } + if (next != '#') return; + while (left()) { + fetch(); + if (enc.isNewLine(c)) break; + } + } + } + protected static final class CharProperty { final int ctype; final int[] ranges; diff --git a/third_party/joni/src/org/joni/Parser.java b/third_party/joni/src/org/joni/Parser.java index 070c8f4d8..6044be6b2 100644 --- a/third_party/joni/src/org/joni/Parser.java +++ b/third_party/joni/src/org/joni/Parser.java @@ -1202,13 +1202,13 @@ private Node parseControlVerb() { final ControlVerbNode.Kind kind; final String verb; - if (startsWith("ACCEPT)")) { + if (startsWith("ACCEPT)") || startsWith("ACCEPT:")) { kind = ControlVerbNode.Kind.ACCEPT; verb = "ACCEPT"; - } else if (startsWith("FAIL)")) { + } else if (startsWith("FAIL)") || startsWith("FAIL:")) { kind = ControlVerbNode.Kind.FAIL; verb = "FAIL"; - } else if (startsWith("F)")) { + } else if (startsWith("F)") || startsWith("F:")) { kind = ControlVerbNode.Kind.FAIL; verb = "F"; } else if (startsWith("PRUNE)") || startsWith("PRUNE:")) { diff --git a/third_party/joni/src/org/joni/ast/QuantifierNode.java b/third_party/joni/src/org/joni/ast/QuantifierNode.java index ed9e80ed8..8b1a655f1 100644 --- a/third_party/joni/src/org/joni/ast/QuantifierNode.java +++ b/third_party/joni/src/org/joni/ast/QuantifierNode.java @@ -29,6 +29,8 @@ import org.joni.Config; import org.joni.ScanEnvironment; import org.joni.constants.internal.TargetInfo; +import org.joni.exception.ErrorMessages; +import org.joni.exception.ValueException; public final class QuantifierNode extends StateNode { public static final int REPEAT_INFINITE = -1; @@ -229,6 +231,16 @@ public int setQuantifier(Node tgt, boolean group, ScanEnvironment env, byte[]byt /* check redundant double repeat. */ /* verbose warn (?:.?)? etc... but not warn (.?)? etc... */ QuantifierNode qnt = (QuantifierNode)tgt; + if (env.usesPerlDiagnostics()) { + if (!group) { + throw new ValueException(ErrorMessages.PERL_NESTED_QUANTIFIERS); + } + // Parentheses are a semantic boundary in Perl. Keep both + // quantifiers instead of applying Oniguruma's reduction table + // or warning about a legal grouped repeat. + setTarget(qnt); + return 0; + } int nestQNum = popularNum(); int targetQNum = qnt.popularNum(); diff --git a/third_party/joni/src/org/joni/constants/internal/OPCode.java b/third_party/joni/src/org/joni/constants/internal/OPCode.java index 3e0b47f50..15d182e88 100644 --- a/third_party/joni/src/org/joni/constants/internal/OPCode.java +++ b/third_party/joni/src/org/joni/constants/internal/OPCode.java @@ -167,6 +167,7 @@ public interface OPCode { int WIDE_SCALAR = 123; int WIDE_SCALAR_CLASS = 124; int PUSH_BRANCH = 125; /* push a syntactic alternation branch */ + int CONTROL_FAIL = 126; /* explicit Perl (*FAIL[:name]) verb */ String[] OpCodeNames = Config.DEBUG_COMPILE ? new String[] { "finish", /*OP_FINISH*/ @@ -296,6 +297,7 @@ public interface OPCode { "wide-scalar", "wide-scalar-class", "push-branch", + "control-fail", } : null; int[] OpCodeArgTypes = Config.DEBUG_COMPILE ? new int[] { @@ -426,5 +428,6 @@ public interface OPCode { Arguments.SPECIAL, /*OP_WIDE_SCALAR*/ Arguments.MEMNUM, /*OP_WIDE_SCALAR_CLASS*/ Arguments.RELADDR, /*OP_PUSH_BRANCH*/ + Arguments.MEMNUM, /*OP_CONTROL_FAIL*/ } : null; } diff --git a/third_party/joni/src/org/joni/constants/internal/OPSize.java b/third_party/joni/src/org/joni/constants/internal/OPSize.java index ba1de3dc7..3b17b0e9a 100644 --- a/third_party/joni/src/org/joni/constants/internal/OPSize.java +++ b/third_party/joni/src/org/joni/constants/internal/OPSize.java @@ -74,7 +74,8 @@ public interface OPSize { int CALLOUT = (OPCODE + MEMNUM); int CALLOUT_CONDITION = (OPCODE + MEMNUM + RELADDR); int DYNAMIC_CALLOUT = (OPCODE + MEMNUM); - int ACCEPT = OPCODE; + int ACCEPT = (OPCODE + INDEX); + int CONTROL_FAIL = (OPCODE + INDEX); int PRUNE = (OPCODE + INDEX); int SKIP = (OPCODE + INDEX); int THEN = (OPCODE + INDEX); diff --git a/third_party/joni/src/org/joni/exception/ErrorMessages.java b/third_party/joni/src/org/joni/exception/ErrorMessages.java index 80d5f29ab..9956d6c9b 100644 --- a/third_party/joni/src/org/joni/exception/ErrorMessages.java +++ b/third_party/joni/src/org/joni/exception/ErrorMessages.java @@ -48,6 +48,7 @@ public interface ErrorMessages extends org.jcodings.exception.ErrorMessages { String UNMATCHED_RANGE_SPECIFIER_IN_CHAR_CLASS = "unmatched range specifier in char-class"; String TARGET_OF_REPEAT_OPERATOR_NOT_SPECIFIED = "target of repeat operator is not specified"; String PERL_QUANTIFIER_FOLLOWS_NOTHING = "Quantifier follows nothing"; + String PERL_NESTED_QUANTIFIERS = "Nested quantifiers"; String TARGET_OF_REPEAT_OPERATOR_INVALID = "target of repeat operator is invalid"; String NESTED_REPEAT_NOT_ALLOWED = "nested repeat is not allowed"; String NESTED_REPEAT_OPERATOR = "nested repeat operator"; diff --git a/third_party/joni/test/org/joni/test/TestPerlControlFailPrecedence.java b/third_party/joni/test/org/joni/test/TestPerlControlFailPrecedence.java new file mode 100644 index 000000000..6dc849190 --- /dev/null +++ b/third_party/joni/test/org/joni/test/TestPerlControlFailPrecedence.java @@ -0,0 +1,55 @@ +/* + * 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 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.junit.Test; + +public class TestPerlControlFailPrecedence { + private static Matcher match(String pattern, String input) { + byte[] patternBytes = pattern.getBytes(StandardCharsets.US_ASCII); + byte[] inputBytes = input.getBytes(StandardCharsets.US_ASCII); + Regex regex = new Regex(patternBytes, 0, patternBytes.length, Option.NONE, + ASCIIEncoding.INSTANCE, Syntax.PerlNG); + Matcher matcher = regex.matcher(inputBytes); + assertEquals(-1, matcher.search(0, inputBytes.length, Option.NONE)); + return matcher; + } + + @Test + public void unnamedFailPreservesEarlierNamedCutError() { + assertEquals("blocked", + match("a(*PRUNE:blocked)(*FAIL)", "a").getControlError()); + } + + @Test + public void failStillPublishesItsOwnError() { + assertEquals("1", match("a(*FAIL)", "a").getControlError()); + assertEquals("named", match("a(*FAIL:named)", "a").getControlError()); + } +} diff --git a/third_party/joni/test/org/joni/test/TestPerlNamedAcceptFail.java b/third_party/joni/test/org/joni/test/TestPerlNamedAcceptFail.java new file mode 100644 index 000000000..d7f1bcf75 --- /dev/null +++ b/third_party/joni/test/org/joni/test/TestPerlNamedAcceptFail.java @@ -0,0 +1,68 @@ +/* + * 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 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.junit.Test; + +public class TestPerlNamedAcceptFail { + private static Matcher matcher(String pattern, String input) { + byte[] patternBytes = pattern.getBytes(StandardCharsets.US_ASCII); + byte[] inputBytes = input.getBytes(StandardCharsets.US_ASCII); + Regex regex = new Regex(patternBytes, 0, patternBytes.length, Option.NONE, + ASCIIEncoding.INSTANCE, Syntax.RUBY); + return regex.matcher(inputBytes); + } + + @Test + public void namedAcceptPublishesItsName() { + Matcher matcher = matcher("a(*ACCEPT:accepted)z", "ab"); + + assertEquals(0, matcher.search(0, 2, Option.NONE)); + assertEquals(1, matcher.getEnd()); + assertEquals("accepted", matcher.getControlMark()); + assertEquals(null, matcher.getControlError()); + } + + @Test + public void namedFailPublishesItsName() { + Matcher matcher = matcher("a(*FAIL:blocked)c", "ac"); + + assertEquals(-1, matcher.search(0, 2, Option.NONE)); + assertEquals(null, matcher.getControlMark()); + assertEquals("blocked", matcher.getControlError()); + } + + @Test + public void namedFailShorthandPublishesItsName() { + Matcher matcher = matcher("a(*F:short)c", "ac"); + + assertEquals(-1, matcher.search(0, 2, Option.NONE)); + assertEquals("short", matcher.getControlError()); + } +} diff --git a/third_party/joni/test/org/joni/test/TestPerlNegativeLookBehindCapture.java b/third_party/joni/test/org/joni/test/TestPerlNegativeLookBehindCapture.java new file mode 100644 index 000000000..a81fd2431 --- /dev/null +++ b/third_party/joni/test/org/joni/test/TestPerlNegativeLookBehindCapture.java @@ -0,0 +1,57 @@ +/* + * 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 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.junit.Test; + +public class TestPerlNegativeLookBehindCapture { + private static int search(String pattern, String input) { + byte[] patternBytes = pattern.getBytes(StandardCharsets.US_ASCII); + byte[] inputBytes = input.getBytes(StandardCharsets.US_ASCII); + Regex regex = new Regex(patternBytes, 0, patternBytes.length, Option.NONE, + ASCIIEncoding.INSTANCE, Syntax.PerlNG); + Matcher matcher = regex.matcher(inputBytes); + return matcher.search(0, inputBytes.length, Option.NONE); + } + + @Test + public void permitsCapturesInNegativeLookBehind() { + assertEquals(0, search("(? compile(pattern)); + assertEquals("Nested quantifiers", error.getMessage()); + } + + @Test + public void preservesGroupedNestedQuantifier() { + byte[] subject = "x~~".getBytes(StandardCharsets.UTF_8); + assertEquals(0, compile("x(~~)*(?:(?:F)?)?").matcher(subject) + .search(0, subject.length, Option.NONE)); + } +}