Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/reference/feature-matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,7 +379,7 @@ my @copy = @{$z}; # ERROR
- ✅ **Backreferences to Named Groups**: Using `\k<name>` or `\g{name}` for backreferences to named groups is supported.
- ✅ **Relative Backreferences**: Using `\g{-n}` for relative backreferences.
- ✅ **Basic Unicode Properties**: Common `\p{...}` and `\P{...}` forms such as `\p{L}` execute through Joni. General_Category assignments now enter the forked Joni parser unchanged and resolve to pinned Perl ranges there.
- 🟡 **Perl Unicode Property Syntax**: Perl-specific properties execute through Joni. `Age`, cumulative `In`/`Present_In`, General_Category, Canonical_Combining_Class, Bidi_Class, Decomposition_Type, East_Asian_Width, Numeric_Value, Joining_Group, Block, Script, and Script_Extensions assignments are generated from pinned Perl 5.44 Unicode 17.0 data with loose and wildcard aliases, ordered missing defaults, official compact aliases and shortcuts, Script-versus-Script_Extensions policy, reserved or composite values, exact rationals, and Perl's generated decimal keyword aliases; `ASCII_Hex_Digit`/`AHex` accepts Perl's eight boolean value aliases. General_Category and standalone exact Block, Script, Script_Extensions, Canonical_Combining_Class, Bidi_Class, Decomposition_Type, East_Asian_Width, Numeric_Value, and Joining_Group assignments use Joni's range-resolver API with explicit per-family case-fold policy. No-fold properties inside composed character classes, wildcard values, and Age-family assignments remain adapter-translated until Joni represents those syntax semantics natively. Other generated property/value aliases still have pinned acceptance/rejection gaps.
- 🟡 **Perl Unicode Property Syntax**: Perl-specific properties execute through Joni. `Age`, cumulative `In`/`Present_In`, General_Category, Canonical_Combining_Class, Bidi_Class, Decomposition_Type, East_Asian_Width, Numeric_Value, Joining_Group, Block, Script, and Script_Extensions assignments are generated from pinned Perl 5.44 Unicode 17.0 data with loose and wildcard aliases, ordered missing defaults, official compact aliases and shortcuts, Script-versus-Script_Extensions policy, reserved or composite values, exact rationals, and Perl's generated decimal keyword aliases; `ASCII_Hex_Digit`/`AHex` accepts Perl's eight boolean value aliases. General_Category and standalone exact Age, `In`/`Present_In`, Block, Script, Script_Extensions, Canonical_Combining_Class, Bidi_Class, Decomposition_Type, East_Asian_Width, Numeric_Value, and Joining_Group assignments use Joni's range-resolver API with explicit per-family case-fold policy. No-fold properties inside composed character classes and wildcard values remain adapter-translated until Joni represents those syntax semantics natively. Other generated property/value aliases still have pinned acceptance/rejection gaps.
- ✅ **Possessive Quantifiers**: Quantifiers like `*+`, `++`, `?+`, and `{n,m}+`, which disable backtracking, are supported.
- ✅ **Atomic Grouping**: Use of `(?>...)` for atomic groups is supported.
- ✅ **`\K` assertion**: Keep left — in `s///`, text before `\K` is preserved; match variables reflect only the portion after `\K`. Ordinary KEEP assertions route through native Joni and no longer use the Java marker rewrite; the adapter still rejects KEEP inside lookaround until the Joni analyser emits Perl's diagnostic directly.
Expand Down
48 changes: 36 additions & 12 deletions src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java
Original file line number Diff line number Diff line change
Expand Up @@ -983,6 +983,7 @@ static boolean isPerlBuiltInPropertyAlias(String property) {
static CharacterPropertyResolver.Result resolveJoniProperty(
String property, boolean inCharacterClass) {
if (property == null) return null;
property = normalizePerlIsPropertyAssignment(property);
int assignment = propertyValueDelimiter(property);
if (assignment <= 0 || assignment == property.length() - 1) {
return null;
Expand All @@ -1008,6 +1009,9 @@ static CharacterPropertyResolver.Result resolveJoniProperty(
|| PerlUnicodeJoiningGroupData.isPropertyAlias(name)) {
if (perlNumericWildcardBody(value) != null) return null;
caseFold = false;
} else if (isPerlAgeProperty(name)) {
if (isPerlAgeWildcard(value)) return null;
caseFold = false;
} else {
return null;
}
Expand Down Expand Up @@ -1060,7 +1064,7 @@ private static boolean isPerlSpecialPropertyAlias(String property) {
private static UnicodeSet resolvePerlBuiltInPropertyAlias(String property) {
if (property == null) return null;

String alias = property.trim();
String alias = normalizePerlIsPropertyAssignment(property.trim());
int assignment = propertyValueDelimiter(alias);
if (assignment == alias.length() - 1
&& (PerlUnicodeScriptData.isScriptPropertyAlias(
Expand All @@ -1081,6 +1085,8 @@ && isGeneralCategoryProperty(alias.substring(0, assignment))) {
}
return category;
}
UnicodeSet age = resolvePerlAgeProperty(alias, true);
if (age != null) return age;
if (assignment > 0 && assignment < alias.length() - 1
&& isCanonicalCombiningClassProperty(alias.substring(0, assignment))) {
UnicodeSet combiningClass = PerlUnicodeCombiningClassData.resolve(
Expand Down Expand Up @@ -1722,26 +1728,33 @@ private static int unicodePropertyValue(int property, String alias) {
}

private static String translatePerlAgeProperty(String property, boolean negated) {
int delimiter = property.indexOf('=');
int colon = property.indexOf(':');
if (delimiter < 0 || colon > 0 && colon < delimiter) delimiter = colon;
UnicodeSet result = resolvePerlAgeProperty(property, true);
return result == null ? null
: wrapCharClass(unicodeSetToJavaPattern(result), negated);
}

private static UnicodeSet resolvePerlAgeProperty(
String property, boolean allowWildcard) {
property = normalizePerlIsPropertyAssignment(property);
int delimiter = propertyValueDelimiter(property);
if (delimiter <= 0 || delimiter == property.length() - 1) return null;

String name = property.substring(0, delimiter)
.replace("_", "").replace("-", "").replace(" ", "");
String name = property.substring(0, delimiter);
boolean exact;
if (name.equalsIgnoreCase("Age")) {
String looseName = loosePropertyName(name);
if (looseName.equals("age")) {
exact = true;
} else if (name.equalsIgnoreCase("In") || name.equalsIgnoreCase("PresentIn")) {
} else if (looseName.equals("in") || looseName.equals("presentin")) {
exact = false;
} else {
return null;
}

String requested = normalizeUnicodeAgeVersion(property.substring(delimiter + 1));
String value = property.substring(delimiter + 1);
if (!allowWildcard && isPerlAgeWildcard(value)) return null;
String requested = normalizeUnicodeAgeVersion(value);
if (requested.equalsIgnoreCase("NA") || requested.equalsIgnoreCase("Unassigned")) {
return wrapCharClass(
unicodeSetToJavaPattern(PerlUnicodeAgeData.unassignedSet()), negated);
return PerlUnicodeAgeData.unassignedSet();
}

UnicodeSet result = exact
Expand All @@ -1750,7 +1763,18 @@ private static String translatePerlAgeProperty(String property, boolean negated)
if (result == null) {
throw new IllegalArgumentException("Unsupported Unicode age version: " + requested);
}
return wrapCharClass(unicodeSetToJavaPattern(result), negated);
return result;
}

private static boolean isPerlAgeProperty(String name) {
String looseName = loosePropertyName(name);
return looseName.equals("age") || looseName.equals("in")
|| looseName.equals("presentin");
}

private static boolean isPerlAgeWildcard(String value) {
String trimmed = value.trim();
return trimmed.startsWith(":\\A") && trimmed.endsWith("\\z:");
}

private static String normalizeUnicodeAgeVersion(String value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,27 @@ void passesRemainingExactEnumeratedPropertiesToJoni() {
}
}

@Test
void passesExactAgePropertiesToJoniWithoutTextExpansion() {
String[][] cases = {
{"\\p{Age=2.1}", "\u20AC"},
{"\\p{In=3.0}", "\u20AC"},
{"\\p{Present_In=3.0}", "\u20AC"},
{"\\p{Is_Age=6.1}", "\uD83D\uDE00"},
{"\\p{Age=Unassigned}", "\uD88D\uDC7A"},
};

for (String[] testCase : cases) {
JoniRegexPattern pattern = new JoniRegexPattern(testCase[0], FLAGS);
assertEquals(testCase[0], pattern.patternDescription());
assertTrue(pattern.matcher(testCase[1], java.util.List.of()).find());
}

JoniRegexPattern wildcard = new JoniRegexPattern(
"\\p{Age=:\\AV16_0\\z:}", FLAGS);
assertFalse(wildcard.patternDescription().contains("Age="));
}

@Test
void flattensTranslatedPropertiesInsideOrdinaryCharacterClasses() {
JoniRegexPattern pattern = new JoniRegexPattern(
Expand Down