{"record":{"id":"df936ac368c10c0f","repo":"oracle/graal","slug":"regex-with-overlapping-bounded-quantifier-after-l","errorCode":null,"errorMessage":"Regex with overlapping bounded quantifier (after look-ahead merging)","messagePattern":"Regex with overlapping bounded quantifier \\(after look-ahead merging\\)","errorType":"exception","errorClass":"UnsupportedRegexException","httpStatus":null,"severity":"error","filePath":"regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/tregex/nfa/ASTSuccessor.java","lineNumber":177,"sourceCode":"                    LongArrayBuffer mergedOperations = new LongArrayBuffer();\n                    StateSet<RegexAST, Term> mergedStateSet = state.getTransitionSet().getTargetStateSet().copy();\n                    mergedTransitions.addAll(state.getTransitionSet().getTransitions());\n                    mergedConstraints.addAll(state.getConstraints());\n                    mergedOperations.addAll(state.getOperations());\n\n                    for (int i = 0; i < lookAroundState.getTransitionSet().size(); i++) {\n                        ASTTransition t = lookAroundState.getTransitionSet().getTransition(i);\n                        if (mergedStateSet.add(t.getTarget())) {\n                            mergedTransitions.add(t);\n                            mergedConstraints.addAll(t.getConstraints());\n                            mergedOperations.addAll(t.getOperations());\n                        }\n                    }\n                    if (!mergedConstraints.isEmpty()) {\n                        int firstQid = TransitionConstraint.getQuantifierID(mergedConstraints.get(0));\n                        for (long constraint : mergedConstraints) {\n                            if (TransitionConstraint.getQuantifierID(constraint) != firstQid) {\n                                throw new UnsupportedRegexException(\"Regex with overlapping bounded quantifier (after look-ahead merging)\");\n                            }\n                        }\n                    }\n                    if (result.size() >= TRegexOptions.TRegexMaxNumberOfASTSuccessorsInOneASTStep) {\n                        throw new UnsupportedRegexException(\"ASTSuccessor explosion\");\n                    }\n                    result.add(new TransitionBuilder<>(mergedTransitions.toArray(new ASTTransition[mergedTransitions.length()]), mergedStateSet, intersection, mergedConstraints.toArray(),\n                                    mergedOperations.toArray()));\n                }\n            }\n        }\n    }\n\n    @TruffleBoundary\n    @Override\n    public JsonValue toJson() {\n        return Json.obj(Json.prop(\"lookAheads\", lookAheads.stream().map(x -> Json.val(x.getRoot().getId())).collect(Collectors.toList())),\n                        Json.prop(\"lookBehinds\", lookBehinds.stream().map(x -> Json.val(x.getRoot().getId())).collect(Collectors.toList())),","sourceCodeStart":159,"sourceCodeEnd":195,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/tregex/nfa/ASTSuccessor.java#L159-L195","documentation":"Thrown by ASTSuccessor.addAllIntersecting during look-ahead merging: after merging a look-ahead's transitions into a successor state, the accumulated TransitionConstraints must all reference the same quantifier id (firstQid). If constraints on different quantifiers end up on one merged transition, the linear engine cannot evaluate them and rejects the regex.","triggerScenarios":"A look-ahead that contains a bounded quantifier whose constraints overlap with a bounded quantifier active on the main path, e.g. patterns like (a{1,3}b)*(?=x{1,2}y) where the merged transition constrains both the outer and the look-ahead's quantifier.","commonSituations":"Password/validation regexes combining counted repetition with look-aheads; ported PCRE patterns using (?=(...){m,n}) constructs; generated patterns that wrap every alternative in a counted group and a look-ahead.","solutions":["Remove the bounded count from inside the look-ahead, e.g. replace (?=x{1,2}y) with (?=x{0,2}y) variants, or an unbounded/greedy form (?=x*y) when the exact count is not load-bearing.","Move the counted constraint out of the look-ahead into the main expression.","Let the pattern fall back to the backtracking engine, which handles overlapping counted quantifiers.","Verify the counted condition in application code after the match."],"exampleFix":"// before\nString pattern = \"(a{1,3}b)*(?=x{1,2}y)\";\n\n// after\nString pattern = \"(a{1,3}b)*(?=x{0,2}y)\"; // constraint on only one quantifier, or\nString pattern = \"(a{1,3}b)*\"; // check the x{1,2}y condition in code after matching","handlingStrategy":"try-catch","validationCode":"// reject look-aheads containing bounded quantifiers when the main path also has one\nMatcher m = Pattern.compile(\"\\\\(\\\\?=(.*?\\\\{\\\\d+,\\\\d+\\\\}.*)\\\\)\").matcher(pattern);\nif (m.find() && pattern.matches(\".*\\\\{\\\\d+,\\\\d+\\\\}.*\")) throw new IllegalArgumentException(\"overlapping bounded quantifier across look-ahead\");","typeGuard":"null","tryCatchPattern":"try { compile(pattern); } catch (UnsupportedRegexException e) { /* rewrite look-ahead without counted bounds, or use backtracking fallback */ }","preventionTips":["Keep counted quantifiers out of look-ahead bodies.","Move counted constraints into the main expression.","Check counted conditions in code after the match instead of in the pattern."],"tags":["regex","tregex","look-ahead","quantifiers","compilation"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}