{"record":{"id":"cec7985504ebc3fa","repo":"oracle/graal","slug":"nested-look-behind-assertions","errorCode":null,"errorMessage":"nested look-behind assertions","messagePattern":"nested look-behind assertions","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":127,"sourceCode":"        }\n        lookBehinds.addAll(addLookBehinds);\n    }\n\n    public ArrayList<TransitionBuilder<RegexAST, Term, ASTTransition>> getMergedStates(ASTTransitionCanonicalizer canonicalizer, CompilationBuffer compilationBuffer) {\n        if (!lookAroundsMerged) {\n            mergeLookArounds(canonicalizer, compilationBuffer);\n            lookAroundsMerged = true;\n        }\n        return mergedStates;\n    }\n\n    private void mergeLookArounds(ASTTransitionCanonicalizer canonicalizer, CompilationBuffer compilationBuffer) {\n        assert mergedStates.isEmpty();\n        canonicalizer.addArgument(initialTransition, getInitialTransitionCharSet(compilationBuffer), initialTransition.getConstraints(), initialTransition.getOperations());\n        for (ASTStep lookBehind : lookBehinds) {\n            ASTSuccessor lb = lookBehind.getSuccessors().get(0);\n            if (lookBehind.getSuccessors().size() > 1 || lb.hasLookArounds()) {\n                throw new UnsupportedRegexException(\"nested look-behind assertions\");\n            }\n            CodePointSet intersection = getInitialTransitionCharSet(compilationBuffer).createIntersection(lb.getInitialTransitionCharSet(compilationBuffer), compilationBuffer);\n            if (intersection.matchesSomething()) {\n                canonicalizer.addArgument(lb.getInitialTransition(), intersection, lb.getInitialTransition().getConstraints(), lb.getInitialTransition().getOperations());\n            }\n        }\n        TransitionBuilder<RegexAST, Term, ASTTransition>[] mergedLookBehinds = canonicalizer.run(compilationBuffer);\n        Collections.addAll(mergedStates, mergedLookBehinds);\n        ArrayList<TransitionBuilder<RegexAST, Term, ASTTransition>> newMergedStates = new ArrayList<>();\n        for (ASTStep lookAhead : lookAheads) {\n            for (TransitionBuilder<RegexAST, Term, ASTTransition> state : mergedStates) {\n                addAllIntersecting(canonicalizer, state, lookAhead, newMergedStates, compilationBuffer);\n            }\n            ArrayList<TransitionBuilder<RegexAST, Term, ASTTransition>> tmp = mergedStates;\n            mergedStates = newMergedStates;\n            newMergedStates = tmp;\n            newMergedStates.clear();\n        }","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/tregex/nfa/ASTSuccessor.java#L109-L145","documentation":"Thrown by ASTSuccessor.mergeLookArounds while merging look-around assertions into the successor's transitions: a look-behind's entry step must have exactly one successor and must not itself contain look-arounds. If lookBehind.getSuccessors().size() > 1 or that successor has nested look-arounds, the merge is undefined for the linear engine and the regex is rejected.","triggerScenarios":"Compiling (in linear/DFA-capable mode) a regex containing a look-behind whose first step branches into multiple successors, e.g. (?<=(?:a|b|c)x), or a look-behind nested inside another look-around such as (?<=(?<=a)b).","commonSituations":"Porting PCRE/Perl-heavy patterns to GraalVM regex (e.g. Espresso/Java or another guest language) that rely on nested look-behinds; variable-width look-behinds written as alternations; look-behind inside look-ahead patterns from log-parsing or syntax-highlighting grammars.","solutions":["Flatten the look-behind's entry step to a single deterministic path, e.g. rewrite (?<=(?:a|b|c)x) as a character class (?<=[abc]x).","Remove nested look-arounds: pull the inner assertion out of the look-behind or restructure with capture groups checked after the match.","Rely on engine fallback: the exception is caught in TRegexCompilationRequest/TRegexCompiler and the pattern is retried on the backtracking NFA executor, which supports these constructs — do not force linear-only options.","Do the look-behind check in application code on the matched substring instead of in the pattern."],"exampleFix":"// before\nString pattern = \"(?<=(?:foo|bar)baz)quux\";\n\n// after\nString pattern = \"(?<=(?:foo|bar)baz)quux\"; // kept, but compiled by backtracking engine\n// or: match \"quux\", then verify preceding text manually with startsWith on the input","handlingStrategy":"try-catch","validationCode":"if (pattern.contains(\"(?<=\") && pattern.matches(\".*\\\\(\\\\?<[:=].*(?<=.*\\\\).*\")) { // nested look-around present\n    throw new IllegalArgumentException(\"nested look-behind not supported by the linear engine\");\n}","typeGuard":"null","tryCatchPattern":"try { linearEngine.compile(pattern); } catch (UnsupportedRegexException e) { backtrackingEngine.compile(pattern); } // nested look-behinds work on the backtracking executor","preventionTips":["Avoid nesting look-arounds; keep look-behinds single-branch and flat.","Prefer character classes inside look-behinds over alternations.","When porting PCRE patterns, re-check every look-behind for branching or nesting."],"tags":["regex","tregex","look-behind","look-around","compilation"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}