oracle/graal · error · UnsupportedRegexException

Grapheme clusters are not supported

Error message

Grapheme clusters are not supported

What it means

Thrown by the Java-flavor lexer when the pattern contains \X, the 'grapheme cluster' escape (any extended grapheme cluster, roughly one user-perceived character). TRegex has no AST node or NFA construction for unbounded grapheme clustering because it needs Unicode segmentation rather than a regular expression over code points, so case 'X' in parseCustomEscape rejects it outright with UnsupportedRegexException.

Source

Thrown at regex/src/com.oracle.truffle.regex/src/com/oracle/truffle/regex/flavor/java/JavaRegexLexer.java:801

            }
            case 'k' -> {
                if (atEnd()) {
                    handleUnfinishedEscape();
                }
                if (consumeChar() != '<') {
                    throw syntaxError(JavaErrorMessages.NAMED_CAPTURE_GROUP_REFERENCE_MISSING_BEGIN, ErrorCode.InvalidBackReference);
                }
                String groupName = javaParseGroupName();
                // backward reference
                if (namedCaptureGroups.containsKey(groupName)) {
                    return Token.createBackReference(getSingleNamedGroupNumber(groupName), false);
                }
                throw syntaxError(JavaErrorMessages.unknownGroupReference(groupName), ErrorCode.InvalidBackReference);
            }
            case 'R' -> {
                return Token.createLineBreak();
            }
            case 'X' -> throw new UnsupportedRegexException("Grapheme clusters are not supported");
            case 'G' -> throw new UnsupportedRegexException("End of previous match boundary matcher is not supported");
            case 'Q' -> {
                int start = position;
                int end = pattern.indexOf("\\E", start);
                if (end < 0) {
                    end = pattern.length();
                    position = end;
                } else {
                    position = end + 2;
                }
                return Token.createLiteralString(start, end);
            }
        }
        return null;
    }

    @Override
    protected int parseCustomEscapeChar(char c, boolean inCharClass) {

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Replace \X with a base-plus-marks approximation such as (?:\P{M}\p{M}*)
  2. If only line-oriented matching was intended, use . with DOTALL or an explicit (?:\r\n|[\s\S]) alternation
  3. Do heavyweight grapheme segmentation with a dedicated Unicode library (ICU BreakIterator) outside the regex, matching each segment with a simple regex

Example fix

// before
String p = "\\X"; // one grapheme cluster

// after
String p = "(?:\\P{M}\\p{M}*)"; // base char + combining marks
Defensive patterns

Strategy: try-catch

Validate before calling

boolean usesGraphemeEscape(String pattern) {
    return pattern.contains("\\X");
}

Try / catch

try {
    RegexObject re = compileJavaFlavor(pattern);
} catch (UnsupportedRegexException e) {
    pattern = pattern.replace("\\X", "(?:\\P{M}\\p{M}*)");
    re = compileJavaFlavor(pattern);
}

Prevention

When it happens

Trigger: Any Java-flavor pattern containing the two-character escape \X, e.g. compiling "\\X+" or "\\b\\X*\\b". The lexer hits case 'X' in parseCustomEscape and throws before any parser or DFA stage runs.

Common situations: Patterns ported from PCRE, Perl, Ruby, or .NET where \X is idiomatic for Unicode text segmentation; emoji/CJK processing regexes copied from other languages into a TRegex-backed Java engine; older patterns that used (?:\r\n|.) and get 'modernized' to \X.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/df85b285f2012b13. Report an issue: GitHub.