Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions dev/design/phase36-regex-parity.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
14 changes: 11 additions & 3 deletions src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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);
Expand All @@ -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) {
Expand Down
10 changes: 7 additions & 3 deletions src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, RuntimeScalar> entry
: DynamicVariableManager.activeLocalizedGlobalScalars().entrySet()) {
if (entry.getKey().endsWith("::REGMARK")) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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}?+")));
}
}
36 changes: 36 additions & 0 deletions src/test/resources/unit/regex/named_accept_fail_control_verbs.t
Original file line number Diff line number Diff line change
@@ -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;
18 changes: 18 additions & 0 deletions src/test/resources/unit/regex/negative_lookbehind_capture_accept.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use strict;
use warnings;
use Test::More;

my $accept = qr/(?<!([cd](*ACCEPT)|x)gggg)blrph/;

ok('cblrph' !~ $accept,
'ACCEPT inside negative lookbehind rejects the outer match');
ok('dblrph' !~ $accept,
'alternate ACCEPT path inside negative lookbehind rejects the outer match');
ok('qblrph' =~ $accept,
'failed negative-lookbehind body permits the outer match');

ok('b' =~ /(?<!(a))b/, 'capture group is legal inside negative lookbehind');
ok(!defined($1), 'capture remains unset when negative lookbehind succeeds');
ok('ab' !~ /(?<!(a))b/, 'capturing negative lookbehind can reject a match');

done_testing;
34 changes: 34 additions & 0 deletions src/test/resources/unit/regex/nested_quantifier_policy.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use strict;
use warnings;
use Test::More;

for my $pattern ('a**', '.{1}??', '.{1}?+', '(?i:a**)') {
my ($regex, @warnings);
{
local $SIG{__WARN__} = sub { push @warnings, @_ };
$regex = eval { qr/$pattern/ };
}
ok(!defined($regex), "$pattern is rejected");
like($@, qr/^Nested quantifiers/, "$pattern uses Perl nested-quantifier diagnostic");
}

for my $case (
[ 'x(~~)*(?:(?:F)?)?', 'x~~', undef, 1 ],
[ '(?:r?)*?r|(.{2,4})', 'abcde', 'abcd', 1 ],
[ '^(.)(?:(.)+)*[BX]', 'ABCDE', undef, 1 ],
[ '(?x:( a | ( bc ) ) {0,0} ? xyz)', 'xyz', undef, 0 ],
[ '(?x:( a | ( bc ) ) {0,0} + xyz)', 'xyz', undef, 0 ],
) {
my ($pattern, $subject, $capture, $requires_quiet) = @$case;
my (@warnings, $regex);
{
local $SIG{__WARN__} = sub { push @warnings, @_ };
$regex = eval { qr/$pattern/ };
}
ok(defined($regex) && $@ eq '' && (!$requires_quiet || !@warnings),
"$pattern has Perl's legal grouped-quantifier compile policy");
ok($subject =~ $regex, "$pattern retains match semantics");
is($1, $capture, "$pattern retains capture semantics") if defined($capture);
}

done_testing;
8 changes: 5 additions & 3 deletions third_party/joni/src/org/joni/Analyser.java
Original file line number Diff line number Diff line change
Expand Up @@ -1473,8 +1473,7 @@ private Node divideLookBehindAlternatives(Node node) {
}

private Node setupLookBehind(AnchorNode node) {
AcceptLengthInfo acceptLengths = node.type == AnchorType.LOOK_BEHIND
? getAcceptLengthInfo(node.target) : null;
AcceptLengthInfo acceptLengths = getAcceptLengthInfo(node.target);
if (acceptLengths != null && acceptLengths.hasAccept()) {
int min = acceptLengths.acceptMin;
int max = acceptLengths.acceptMax;
Expand Down Expand Up @@ -2593,7 +2592,10 @@ protected final Node setupTree(Node node, int state) {
int allowedInNegativeLookBehind = syntax.op2OptionPerl()
? AnchorType.ALLOWED_IN_PERL_LB_NOT
: AnchorType.ALLOWED_IN_LB_NOT;
if (checkTypeTree(an.target, NodeType.ALLOWED_IN_LB, EncloseType.ALLOWED_IN_LB_NOT, allowedInNegativeLookBehind)) newSyntaxException(INVALID_LOOK_BEHIND_PATTERN);
int allowedEnclosuresInNegativeLookBehind = syntax.op2OptionPerl()
? EncloseType.ALLOWED_IN_LB
: EncloseType.ALLOWED_IN_LB_NOT;
if (checkTypeTree(an.target, NodeType.ALLOWED_IN_LB, allowedEnclosuresInNegativeLookBehind, allowedInNegativeLookBehind)) newSyntaxException(INVALID_LOOK_BEHIND_PATTERN);
node = setupLookBehind(an);
if (node.getType() != NodeType.ANCHOR) continue restart;
setupTree(((AnchorNode)node).target, (state | IN_NOT | IN_LOOKAROUND));
Expand Down
6 changes: 4 additions & 2 deletions third_party/joni/src/org/joni/ArrayCompiler.java
Original file line number Diff line number Diff line change
Expand Up @@ -103,10 +103,12 @@ protected void compileControlVerbNode(ControlVerbNode node) {
case ACCEPT:
regex.requireStack = true;
addOpcode(OPCode.ACCEPT);
addInt(controlVerbLabelId(node.name));
break;
case FAIL:
regex.requireStack = true;
addOpcode(OPCode.FAIL);
addOpcode(OPCode.CONTROL_FAIL);
addInt(controlVerbLabelId(node.name));
break;
case PRUNE:
regex.requireStack = true;
Expand Down Expand Up @@ -1372,7 +1374,7 @@ private int compileLengthTree(Node node) {
if (node instanceof ControlVerbNode control) {
return switch (control.kind) {
case ACCEPT -> OPSize.ACCEPT;
case FAIL -> OPSize.FAIL;
case FAIL -> OPSize.CONTROL_FAIL;
case PRUNE -> OPSize.PRUNE;
case SKIP -> OPSize.SKIP;
case THEN -> OPSize.THEN;
Expand Down
17 changes: 17 additions & 0 deletions third_party/joni/src/org/joni/ByteCodeMachine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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++]);
Expand Down
33 changes: 31 additions & 2 deletions third_party/joni/src/org/joni/Lexer.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -1272,7 +1273,7 @@ private void fetchTokenFor_openBrace() {
greedyCheck();
break;
case 2:
if (syntax.fixedIntervalIsGreedyOnly()) {
if (syntax.fixedIntervalIsGreedyOnly() && !syntax.op2OptionPerl()) {
possessiveCheck();
} else {
greedyCheck();
Expand Down Expand Up @@ -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)) {
Expand All @@ -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;
Expand Down
Loading
Loading