{"record":{"id":"0105dc727ad3f737","repo":"oracle/graal","slug":"cannot-compile-regex-with-empty-state-to-dfa-nfa","errorCode":null,"errorMessage":"Cannot compile regex with empty state to DFA/NFA","messagePattern":"Cannot compile regex with empty state to DFA/NFA","errorType":"exception","errorClass":"UnsupportedRegexException","httpStatus":null,"severity":"error","filePath":"regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/tregex/parser/ast/visitors/NFATraversalRegexASTVisitor.java","lineNumber":602,"sourceCode":"            if (curTerm.isGroupWithGuards() && insideEmptyGuardGroup.get(curTerm.asGroup().getGroupsWithGuardsIndex()) &&\n                            !getFlavor().emptyChecksMonitorCaptureGroups()) {\n                Group curGroup = curTerm.asGroup();\n                Quantifier quantifier = curGroup.getQuantifier();\n                // If we are:\n                // - in ECMAScript or Python flavor\n                // - in the mandatory split part of a quantifier\n                // - that has not been unrolled\n                // - and capture groups are visible to the caller, or the expression contains\n                // back-references, or we crossed a caret\n                if (!getFlavor().emptyChecksOnMandatoryLoopIterations() &&\n                                curGroup.isMandatoryQuantifier() &&\n                                !curGroup.isExpandedQuantifier() &&\n                                (!ast.getOptions().isBooleanMatch() || ast.getProperties().hasBackReferences() || caretsOnPath() || isReverse() && dollarsOnPath())) {\n                    // the existence of a mandatory copy of the quantifier loop implies a minimum\n                    // greater than zero\n                    assert quantifier.getMin() > 0;\n                    if (isBuildingDFA()) {\n                        throw new UnsupportedRegexException(\"Cannot compile regex with empty state to DFA/NFA\");\n                    }\n                    popGroupExit();\n                    cur = curTerm;\n                    // Set the current group node as the path's target to indicate we want to\n                    // generate an EMPTY_STATE for it. The empty state allows the backtracking\n                    // engine to loop without consuming characters.\n                    curPath.add(PathElement.create(cur));\n                    return true;\n                }\n                if (isBuildingDFA() && curGroup.isMandatoryQuantifier() && !lookAroundsOnPath.isEmpty()) {\n                    for (int i = curPath.length() - 1; i >= 0; i--) {\n                        long element = curPath.get(i);\n                        RegexASTNode node = pathGetNode(element);\n                        if (PathElement.isGroupEnter(element) && node == curGroup) {\n                            break;\n                        }\n                        if (node.isLookAheadAssertion()) {\n                            throw new UnsupportedRegexException(\"empty path with look-ahead assertion in expression with bounded quantifier\");","sourceCodeStart":584,"sourceCodeEnd":620,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/tregex/parser/ast/visitors/NFATraversalRegexASTVisitor.java#L584-L620","documentation":"Thrown by NFATraversalRegexASTVisitor while walking a mandatory quantifier loop that can match the empty string: when the flavor does not require empty-loop checks (emptyChecksOnMandatoryLoopIterations() is false), the group is mandatory and not unrolled, and capture groups are visible / back-references exist / a caret (or dollar, in reverse matching) was crossed, a DFA cannot represent the state and compilation bails out.","triggerScenarios":"Compiling in DFA-building mode a pattern like ((a)?){3} or (a*){2} — a mandatory bounded loop whose body can match empty — under flavors that skip empty-iteration checks when captures/back-references/anchors are involved.","commonSituations":"Validation patterns wrapping optional content in counted groups, e.g. (\\w?){n}; patterns with back-references inside counted loops; porting between flavors (e.g. from Perl/PCRE to Java/OracleDB) with different empty-iteration semantics.","solutions":["Make the loop body unable to match empty: change ((a)?){3} to (a){0,3} or ((a)+)? forms.","Remove the count from the group or drop the unnecessary capture group (use (?:...)).","Let the engine fall back to the backtracking NFA, which creates an EMPTY_STATE and loops without consuming characters.","If you control the flavor options, enable empty-checks-on-mandatory-loop-iterations semantics instead of rewriting the pattern."],"exampleFix":"// before\nString pattern = \"((?:a)?){5}b\"; // mandatory loop whose body can match empty\n\n// after\nString pattern = \"a{0,5}b\"; // loop body cannot match empty","handlingStrategy":"try-catch","validationCode":"// reject mandatory loops whose body is wholly optional: ((x)?){n} shape\nif (Pattern.compile(\"\\\\(\\\\([^)]*\\\\)\\\\?\\\\)\\\\{\\\\d+\\\\}\").matcher(pattern).find()) throw new IllegalArgumentException(\"mandatory loop can match empty; unsupported in linear engine\");","typeGuard":"null","tryCatchPattern":"try { compileLinear(pattern); } catch (UnsupportedRegexException e) { compileBacktracking(pattern); } // backtracking engine handles empty loops via EMPTY_STATE","preventionTips":["Ensure every mandatory ({n} with n>0) loop body consumes at least one character.","Rewrite ((a)?){3} as a{0,3}.","Avoid back-references and anchors inside counted loops when targeting the DFA engine."],"tags":["regex","tregex","dfa","empty-match","quantifiers"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}