{"record":{"id":"ff5f6233693a80b3","repo":"oracle/graal","slug":"recursive-subexpression-calls-are-not-supported","errorCode":null,"errorMessage":"recursive subexpression calls are not supported","messagePattern":"recursive subexpression calls are not supported","errorType":"exception","errorClass":"UnsupportedRegexException","httpStatus":null,"severity":"error","filePath":"regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/flavor/ruby/RubySubexpressionCalls.java","lineNumber":96,"sourceCode":"                replace(subexpressionCall, ast.getGroup(subexpressionCall.getGroupNr()).get(0), copyVisitor);\n            }\n            if (callGraph.containsKey(node)) {\n                for (CallGraphNode dependent : callGraph.get(node)) {\n                    int dependentInDegree = inDegree.getOrDefault(dependent, 0);\n                    if (dependentInDegree == 1) {\n                        expansionStack.add(dependent);\n                        inDegree.remove(dependent);\n                    } else {\n                        inDegree.put(dependent, dependentInDegree - 1);\n                    }\n                }\n                callGraph.remove(node);\n            }\n        }\n\n        assert callGraph.isEmpty() == inDegree.isEmpty();\n        if (!callGraph.isEmpty()) {\n            throw new UnsupportedRegexException(\"recursive subexpression calls are not supported\");\n        }\n    }\n\n    private static void replace(SubexpressionCall caller, Group callee, CopyVisitor copyVisitor) {\n        Group copy = (Group) copyVisitor.copy(callee);\n        MarkAsAliveVisitor.markAsAlive(copy);\n        copy.setQuantifier(caller.getQuantifier());\n        Sequence callerSeq = caller.getParent();\n        int callerSeqIndex = caller.getSeqIndex();\n        callerSeq.replace(callerSeqIndex, copy);\n    }\n\n    private abstract static class CallGraphNode {\n    }\n\n    private static final class SubexpressionCallNode extends CallGraphNode {\n\n        private final SubexpressionCall subexpressionCall;","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/oracle/graal/blob/a66e9ccd1d7bf2552883939aa0788dfd0e294aab/regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/flavor/ruby/RubySubexpressionCalls.java#L78-L114","documentation":"Thrown by RubySubexpressionCalls when inlining Ruby subexpression calls (\\\\g<name> / (?<name>) calls) would recurse. TRegex inlines called groups at compile time; it first runs a topological sort (Kahn's algorithm) over the call graph, and if the graph is not empty after processing — i.e. calls form a cycle such as a group calling itself — it throws UnsupportedRegexException because an infinitely-sized inlined AST cannot be built.","triggerScenarios":"Compiling a Ruby-flavor regex where a group calls itself directly or through a chain: (?(?<a>a|\\\\g<a>)) or (?<x>a\\\\g<y>)(?<y>b\\\\g<x>). After topological sorting, callGraph is non-empty (nodes with permanent in-degree), and the exception is thrown.","commonSituations":"Porting recursive matching patterns for balanced constructs (nested parentheses, HTML-ish trees) from Oniguruma/PCRE, where recursion is the idiomatic tool; Ruby guest applications on GraalVM/TruffleRuby using recursive regexes.","solutions":["Replace recursion with iterative matching in host code: match one nesting level per regex pass and drive the recursion in a loop","For balanced-delimiter matching, use a counter-based scan of the input instead of a regex","If only bounded nesting depth is needed, unroll the group a fixed number of times instead of calling it recursively"],"exampleFix":"# before (Ruby flavor)\n/(?<paren>\\\\(\\\\g<paren>*\\\\))/x\n\n# after: match one level, recurse in code\n/(\\((?:[^()]|\\\\((?<i>)|\\\\)(?<-i>))*(?\\(i>\\\\))?\\))/ # or simply loop with /\\([^()]*\\)/","handlingStrategy":"try-catch","validationCode":"boolean usesRecursiveSubexpressionCall(String rubyPattern) {\n    // crude check: named call \\\\g<name> where <name> is also a defined group in the same pattern\n    return java.util.regex.Pattern.compile(\"\\\\\\\\g<[^>]+>\").matcher(rubyPattern).find()\n        && java.util.regex.Pattern.compile(\"\\\\(\\\\?<[^>]+>\").matcher(rubyPattern).find();\n}","typeGuard":null,"tryCatchPattern":"begin\n  re = TRegex.compile_ruby(pattern)\nrescue UnsupportedRegexException => e\n  raise if e.reason !~ /recursive/\n  # fall back to an iterative, level-by-level matching loop\nend","preventionTips":["Avoid \\\\g<name> self-calls in Ruby-flavor patterns for TRegex; express recursive structure in host code","For balanced-delimiter matching, use counting scans instead of recursive regexes"],"tags":["regex","ruby-flavor","recursion","subexpression-call","unsupported-feature"],"backgroundTag":null,"analyzedSha":"a66e9ccd1d7bf2552883939aa0788dfd0e294aab","analyzedAt":"2026-08-14T13:58:47.161Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}