From d3ce1b05a1c74c7c4951f6cb33ee53a4fb45cf6b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 12:11:43 +0200 Subject: [PATCH 1/9] fix(regex): flatten translated properties in Joni classes Classify the established IsDigit, IsLower, and IsUpper aliases for frontend translation and splice their generated ranges into an enclosing character class without nested brackets. This prevents Joni from misparsing adjacent Perl property aliases in ordinary classes. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- .../org/perlonjava/runtime/regex/JoniRegexPattern.java | 9 +++++++-- .../org/perlonjava/runtime/regex/UnicodeResolver.java | 7 +++++++ .../perlonjava/runtime/regex/JoniRegexPatternTest.java | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java index e9a39fad46..4e51cc6d9f 100644 --- a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java +++ b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java @@ -211,8 +211,13 @@ private static UserPropertyTranslation translateUserDefinedProperties( } if ((frontendProperty || scriptExtensions || perlBuiltInAlias) && standardClassBracketDepth > 0) { - translated.append(UnicodeResolver.translateUnicodePropertyForCharClass( - property, pattern.charAt(i + 1) == 'P')); + String propertyClass = UnicodeResolver.translateUnicodePropertyForCharClass( + property, pattern.charAt(i + 1) == 'P'); + if (propertyClass.startsWith("[") && propertyClass.endsWith("]")) { + translated.append(propertyClass, 1, propertyClass.length() - 1); + } else { + translated.append(propertyClass); + } i = end; continue; } diff --git a/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java b/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java index d9e1c57049..aa065c0da8 100644 --- a/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java +++ b/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java @@ -1020,10 +1020,17 @@ static boolean isUserDefinedPropertyName(String property) { static boolean isPerlBuiltInPropertyAlias(String property) { return isPerlSpecializedBinarySyntax(property) + || isPerlJoniClassAlias(property) || !normalizePerlIsPropertyAssignment(property).equals(property) || resolvePerlBuiltInPropertyAlias(property) != null; } + private static boolean isPerlJoniClassAlias(String property) { + return "IsDigit".equals(property) + || "IsLower".equals(property) + || "IsUpper".equals(property); + } + private static boolean isPerlSpecializedBinarySyntax(String property) { if (property == null) return false; String alias = normalizePerlIsPropertyAssignment(property).trim(); diff --git a/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java b/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java index 7d030eaf34..01f2604e40 100644 --- a/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java +++ b/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java @@ -81,6 +81,16 @@ void acceptsPerlBarePropertiesAndWideOctalEscapes() { .matcher("\u0100", java.util.List.of()).find()); } + @Test + void flattensTranslatedPropertiesInsideOrdinaryCharacterClasses() { + JoniRegexPattern pattern = new JoniRegexPattern( + "[\\p{IsDigit}\\p{IsLower}\\p{IsUpper}]", FLAGS); + + assertTrue(pattern.matcher("A", java.util.List.of()).find()); + assertTrue(pattern.matcher("7", java.util.List.of()).find()); + assertFalse(pattern.matcher("-", java.util.List.of()).find()); + } + @Test void acceptsPerlNumericGBackrefsAndHexCodePoints() { assertTrue(new JoniRegexPattern("(a)(b)(c)\\g1\\g2\\g3", FLAGS) From a959b824410de541df41f52c460e721f665bc7c1 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 12:23:09 +0200 Subject: [PATCH 2/9] fix(regex): match beyond-Unicode Joni class members Translate standalone Perl code points above U+10FFFF into exact internal-marker alternatives before Joni compilation. This preserves Perl's ANYOFH membership behavior without changing range or negated-class semantics. Generated with [OpenAI Codex](https://openai.com/codex/) Co-Authored-By: OpenAI Codex --- .../runtime/regex/JoniRegexPattern.java | 130 ++++++++++++++++++ .../runtime/regex/JoniRegexPatternTest.java | 18 +++ 2 files changed, 148 insertions(+) diff --git a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java index 4e51cc6d9f..ce352104e8 100644 --- a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java +++ b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.Iterator; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.Map; import java.util.List; @@ -324,6 +325,7 @@ static String translatePattern(String pattern) { private static String translatePattern(String pattern, RegexFlags flags, int trustedCalloutCount) { pattern = translateDefineBlocks(pattern); + pattern = translateBeyondUnicodeClassMembers(pattern); StringBuilder out = new StringBuilder(pattern.length() + 16); boolean escaped = false; boolean inClass = false; @@ -500,6 +502,134 @@ private static String translatePattern(String pattern, RegexFlags flags, return out.toString(); } + /** + * Perl scalar strings can contain values above Unicode's maximum code point. + * They are represented internally as {@code U+FFFD}, which Joni can + * match as ordinary text, but Joni rejects the original {@code \\x{...}} + * class member before matching begins. Lift standalone beyond-Unicode class + * members into alternatives that match the complete internal marker. + */ + private static String translateBeyondUnicodeClassMembers(String pattern) { + StringBuilder translated = new StringBuilder(pattern.length()); + boolean escaped = false; + for (int i = 0; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (escaped) { + translated.append(ch); + escaped = false; + continue; + } + if (ch == '\\') { + translated.append(ch); + escaped = true; + continue; + } + if (ch != '[') { + translated.append(ch); + continue; + } + + int close = findStandardClassClose(pattern, i + 1); + if (close < 0) { + translated.append(ch); + continue; + } + String replacement = translateBeyondUnicodeClassContent( + pattern.substring(i + 1, close)); + if (replacement == null) { + translated.append(pattern, i, close + 1); + } else { + translated.append(replacement); + } + i = close; + } + return translated.toString(); + } + + private static int findStandardClassClose(String pattern, int start) { + boolean escaped = false; + boolean leading = true; + int posixDepth = 0; + for (int i = start; i < pattern.length(); i++) { + char ch = pattern.charAt(i); + if (escaped) { + escaped = false; + leading = false; + continue; + } + if (ch == '\\') { + escaped = true; + leading = false; + continue; + } + if (ch == '[' && i + 1 < pattern.length() + && (pattern.charAt(i + 1) == ':' + || pattern.charAt(i + 1) == '.' + || pattern.charAt(i + 1) == '=')) { + posixDepth++; + leading = false; + continue; + } + if (ch == ']' && posixDepth > 0) { + posixDepth--; + continue; + } + if (ch == ']' && leading) { + leading = false; + continue; + } + if (ch == ']') return i; + if (ch != '^' || !leading) leading = false; + } + return -1; + } + + private static String translateBeyondUnicodeClassContent(String content) { + if (content.startsWith("^")) return null; + + StringBuilder retained = new StringBuilder(content.length()); + List markers = new ArrayList<>(); + for (int i = 0; i < content.length();) { + if (content.startsWith("\\\\", i)) { + retained.append("\\\\"); + i += 2; + continue; + } + if (content.startsWith("\\x{", i)) { + int close = content.indexOf('}', i + 3); + if (close > i + 3) { + String hex = content.substring(i + 3, close); + try { + long value = Long.parseUnsignedLong(hex, 16); + boolean rangeMember = (i > 0 && content.charAt(i - 1) == '-') + || (close + 1 < content.length() + && content.charAt(close + 1) == '-'); + if (Long.compareUnsigned(value, 0x10FFFFL) > 0 && !rangeMember) { + markers.add(Long.toUnsignedString(value, 16).toUpperCase( + java.util.Locale.ROOT)); + i = close + 1; + continue; + } + } catch (NumberFormatException ignored) { + // Let Joni produce the normal malformed-escape diagnostic. + } + } + } + retained.append(content.charAt(i++)); + } + if (markers.isEmpty()) return null; + + StringBuilder replacement = new StringBuilder("(?:"); + for (int i = 0; i < markers.size(); i++) { + if (i > 0) replacement.append('|'); + replacement.append("\\x{FFFD}<").append(markers.get(i)).append('>'); + } + if (!retained.isEmpty()) { + replacement.append("|[").append(retained).append(']'); + } + return replacement.append(')').toString(); + } + private static void appendResolvedNamedCharacter(StringBuilder out, int codePoint, RegexFlags flags) { boolean extendedSyntax = flags.isExtended() diff --git a/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java b/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java index 01f2604e40..a7c28b25eb 100644 --- a/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java +++ b/src/test/java/org/perlonjava/runtime/regex/JoniRegexPatternTest.java @@ -1,5 +1,6 @@ package org.perlonjava.runtime.regex; +import org.perlonjava.runtime.operators.PerlUtfString; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; @@ -130,6 +131,23 @@ void supplementaryCaptureUsesTheHighSurrogateBoundary() { assertEquals(2, matcher.end(1)); } + @Test + void matchesStandaloneBeyondUnicodeCharacterClassMembers() { + JoniRegexPattern pattern = new JoniRegexPattern( + "[\\x{4000001}\\x{4000003}\\x{4000005}]+", FLAGS); + + assertFalse(pattern.matcher(PerlUtfString.encodeBeyondUnicode(0x4000000L), + java.util.List.of()).find()); + assertTrue(pattern.matcher(PerlUtfString.encodeBeyondUnicode(0x4000001L), + java.util.List.of()).find()); + assertTrue(pattern.matcher(PerlUtfString.encodeBeyondUnicode(0x4000003L), + java.util.List.of()).find()); + assertTrue(pattern.matcher(PerlUtfString.encodeBeyondUnicode(0x4000005L), + java.util.List.of()).find()); + assertFalse(pattern.matcher(PerlUtfString.encodeBeyondUnicode(0x4000006L), + java.util.List.of()).find()); + } + @Test void resolvesBlockPropertiesInsideExtendedClassesBeforeJoniCompilation() { JoniRegexPattern pattern = new JoniRegexPattern( From f07e28b6224962dd6b685d3708a3730a5b44c687 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 13:24:45 +0200 Subject: [PATCH 3/9] fix(regex): preserve surrogate property scalar semantics Render surrogate and beyond-Unicode class members through complete internal scalar markers, including complements, captures, substitutions, and global matching. Prevent translated ordinary alternatives from beginning inside a marker payload during unanchored searches. Record the same-worktree mutable-JAR validation incident and its prevention rule in AGENTS.md. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- AGENTS.md | 1 + .../runtime/regex/JoniRegexPattern.java | 195 ++++++++++++++---- .../runtime/regex/UnicodeResolver.java | 3 +- .../unit/regex/surrogate_property_rendering.t | 70 +++++++ 4 files changed, 232 insertions(+), 37 deletions(-) create mode 100644 src/test/resources/unit/regex/surrogate_property_rendering.t diff --git a/AGENTS.md b/AGENTS.md index 4c3e61c9f5..1b909c066c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -199,6 +199,7 @@ | 2026-04-30 | (no work lost — recovered) Working tree on `fix/class-trait-tests` was overwritten with master content | Agent ran `git checkout master -- .` to A/B test failures vs master without first snapshotting and without switching branches. Recovery only worked because the changes had already been committed to HEAD: `git restore .` (also a forbidden command on a dirty tree, but safe here because "dirty" was master content, not user work) brought the tree back from HEAD. Correct workflow would have been: stash via `git diff > /tmp/wip.patch`, or use `git worktree add` for the master comparison instead of mutating the current tree. | | 2026-04-30 | A full afternoon chasing a phantom "DBIx::Class regression" in `t/76joins.t` / `t/96_is_deteministic_value.t` | Investigative agent launched the test repeatedly under `/usr/bin/time -p ./jperl …` (no `timeout` wrapper). Each hung JVM survived past the agent's lifetime, accumulated as ~14 orphans at 100% CPU each, and starved the active `jcpan` harness — which then SIGKILLed innocent tests after 300 s of no TAP output. Symptom looked exactly like a real perf regression. Fix: always `timeout N ./jperl …` for any potentially-hanging run. | | 2026-08-06 | (no source work lost — build recovered) A process cleanup killed the active Gradle test workers, producing exit 137 failures in two shards. | Agent selected Java PIDs from a broad CPU list without first constraining them to stale processes. Recovery: rerun `make` without killing workers; subsequent build completed successfully. Fix: never kill by CPU list alone; identify the exact command and build ownership first. | +| 2026-08-18 | (no source work lost — corpus rerun required) A generated Unicode corpus read the worktree JAR while a concurrent `make` replaced it, and an interpreter worker failed loading an ICU class. | Agent launched `jperl` corpus workers from the same worktree before its `shadowJar` task had finished. Recovery: let bounded workers finish and rerun both backends after the build stops mutating the artifact. Fix: never run `jperl`/corpus validation from a worktree whose build may replace its JAR; wait for the build or use a verified immutable artifact copy. | | 2026-08-17 | (no source work lost — stale workers removed) Failed `make` runs were interrupted after their known Joni failures, but their Gradle unit-shard workers survived and competed with later builds. | Agent sent Ctrl-C to the parent build session before all parallel workers had exited. Recovery: identified stale workers by PID, start time, and shard work directory, terminated only those exact PIDs, and left the current build and sibling repositories untouched. Fix: let failed parallel `make` runs finish naturally, or verify and clean up their exact child PIDs before starting another build. | | 2026-08-17 | (no source work lost — CPAN run rerun) A concurrent `make` replaced the development shadow JAR while an active `jcpan` process was spawning a child JVM, causing a transient `ClassNotFoundException`. | Agent waited for another worktree's build but did not wait for the same worktree's bounded CPAN runs before rebuilding `target/perlonjava-5.44.0.jar`. Recovery: let `make` finish and rerun the affected CPAN target. Fix: never rebuild a worktree's development JAR while that worktree has active `jperl` or `jcpan` processes. | diff --git a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java index ce352104e8..48affee202 100644 --- a/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java +++ b/src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java @@ -35,6 +35,8 @@ final class JoniRegexPattern { private static final Map INPUT_ENCODINGS = new WeakHashMap<>(); private static final Map BYTE_INPUT_ENCODINGS = new WeakHashMap<>(); + private static final String INTERNAL_SCALAR_BOUNDARY_GUARD = + internalScalarBoundaryGuard(); // Ruby syntax defaults \w to ASCII even for a Unicode encoding. Perl's // default and /u modes use Unicode character classes; /a adds ASCII_RANGE @@ -325,7 +327,7 @@ static String translatePattern(String pattern) { private static String translatePattern(String pattern, RegexFlags flags, int trustedCalloutCount) { pattern = translateDefineBlocks(pattern); - pattern = translateBeyondUnicodeClassMembers(pattern); + pattern = translateInternalScalarClassMembers(pattern); StringBuilder out = new StringBuilder(pattern.length() + 16); boolean escaped = false; boolean inClass = false; @@ -503,13 +505,14 @@ private static String translatePattern(String pattern, RegexFlags flags, } /** - * Perl scalar strings can contain values above Unicode's maximum code point. + * Perl scalar strings can contain surrogate and beyond-Unicode values. * They are represented internally as {@code U+FFFD}, which Joni can - * match as ordinary text, but Joni rejects the original {@code \\x{...}} - * class member before matching begins. Lift standalone beyond-Unicode class - * members into alternatives that match the complete internal marker. + * match as ordinary text, but not as one logical character. Lift those + * members and surrogate ranges into alternatives that consume a complete + * internal marker. For complemented classes, exclude selected marker + * payloads while still consuming every other marker atomically. */ - private static String translateBeyondUnicodeClassMembers(String pattern) { + private static String translateInternalScalarClassMembers(String pattern) { StringBuilder translated = new StringBuilder(pattern.length()); boolean escaped = false; for (int i = 0; i < pattern.length(); i++) { @@ -534,7 +537,7 @@ private static String translateBeyondUnicodeClassMembers(String pattern) { translated.append(ch); continue; } - String replacement = translateBeyondUnicodeClassContent( + String replacement = translateInternalScalarClassContent( pattern.substring(i + 1, close)); if (replacement == null) { translated.append(pattern, i, close + 1); @@ -584,50 +587,170 @@ private static int findStandardClassClose(String pattern, int start) { return -1; } - private static String translateBeyondUnicodeClassContent(String content) { - if (content.startsWith("^")) return null; + private record HexEscape(long value, int endExclusive) {} + private static String translateInternalScalarClassContent(String content) { + boolean negated = content.startsWith("^"); + int contentStart = negated ? 1 : 0; StringBuilder retained = new StringBuilder(content.length()); - List markers = new ArrayList<>(); - for (int i = 0; i < content.length();) { + List exactMarkers = new ArrayList<>(); + List surrogateRanges = new ArrayList<>(); + for (int i = contentStart; i < content.length();) { if (content.startsWith("\\\\", i)) { retained.append("\\\\"); i += 2; continue; } - if (content.startsWith("\\x{", i)) { - int close = content.indexOf('}', i + 3); - if (close > i + 3) { - String hex = content.substring(i + 3, close); - try { - long value = Long.parseUnsignedLong(hex, 16); - boolean rangeMember = (i > 0 && content.charAt(i - 1) == '-') - || (close + 1 < content.length() - && content.charAt(close + 1) == '-'); - if (Long.compareUnsigned(value, 0x10FFFFL) > 0 && !rangeMember) { - markers.add(Long.toUnsignedString(value, 16).toUpperCase( - java.util.Locale.ROOT)); - i = close + 1; - continue; - } - } catch (NumberFormatException ignored) { - // Let Joni produce the normal malformed-escape diagnostic. + HexEscape first = parseHexEscape(content, i); + if (first != null) { + HexEscape last = first.endExclusive() < content.length() + && content.charAt(first.endExclusive()) == '-' + ? parseHexEscape(content, first.endExclusive() + 1) : null; + if (last != null && first.value() <= last.value() + && first.value() <= 0x10FFFFL && last.value() <= 0x10FFFFL + && first.value() <= Character.MAX_SURROGATE + && last.value() >= Character.MIN_SURROGATE) { + int surrogateStart = (int) Math.max( + first.value(), Character.MIN_SURROGATE); + int surrogateEnd = (int) Math.min( + last.value(), Character.MAX_SURROGATE); + surrogateRanges.add(new int[] {surrogateStart, surrogateEnd}); + if (first.value() < Character.MIN_SURROGATE) { + appendHexClassRange(retained, first.value(), + Character.MIN_SURROGATE - 1L); } + if (last.value() > Character.MAX_SURROGATE) { + appendHexClassRange(retained, + Character.MAX_SURROGATE + 1L, last.value()); + } + i = last.endExclusive(); + continue; + } + if (isSurrogate(first.value())) { + int value = (int) first.value(); + surrogateRanges.add(new int[] {value, value}); + i = first.endExclusive(); + continue; + } + if (Long.compareUnsigned(first.value(), 0x10FFFFL) > 0 + && last == null) { + exactMarkers.add(Long.toUnsignedString(first.value(), 16) + .toUpperCase(java.util.Locale.ROOT)); + i = first.endExclusive(); + continue; } } retained.append(content.charAt(i++)); } - if (markers.isEmpty()) return null; + if (exactMarkers.isEmpty() && surrogateRanges.isEmpty()) return null; - StringBuilder replacement = new StringBuilder("(?:"); - for (int i = 0; i < markers.size(); i++) { - if (i > 0) replacement.append('|'); - replacement.append("\\x{FFFD}<").append(markers.get(i)).append('>'); + List payloads = new ArrayList<>( + exactMarkers.size() + surrogateRanges.size()); + payloads.addAll(exactMarkers); + for (int[] range : surrogateRanges) { + payloads.add(fixedWidthHexRange(range[0], range[1], 4)); + } + String payload = payloads.size() == 1 + ? payloads.get(0) : "(?:" + String.join("|", payloads) + ")"; + String anyMarker = "\\x{FFFD}<[0-9A-F]+>"; + String markerBoundary = INTERNAL_SCALAR_BOUNDARY_GUARD; + + List alternatives = new ArrayList<>(2); + if (negated) { + alternatives.add("\\x{FFFD}<(?!(?:" + payload + ")>)[0-9A-F]+>"); + StringBuilder ordinary = new StringBuilder("(?!") + .append(anyMarker).append(')'); + if (!retained.isEmpty()) { + ordinary.append("(?![").append(retained).append("])" ); + } + alternatives.add(markerBoundary + ordinary.append("[\\s\\S]").toString()); + } else { + alternatives.add("\\x{FFFD}<" + payload + ">"); + if (!retained.isEmpty()) { + alternatives.add(markerBoundary + "(?!" + anyMarker + ")[" + + retained + "]"); + } } - if (!retained.isEmpty()) { - replacement.append("|[").append(retained).append(']'); + return alternatives.size() == 1 ? alternatives.get(0) + : "(?:" + String.join("|", alternatives) + ")"; + } + + private static HexEscape parseHexEscape(String content, int offset) { + if (offset < 0 || !content.startsWith("\\x{", offset)) return null; + int close = content.indexOf('}', offset + 3); + if (close <= offset + 3) return null; + try { + return new HexEscape(Long.parseUnsignedLong( + content.substring(offset + 3, close), 16), close + 1); + } catch (NumberFormatException ignored) { + // Let Joni produce the normal malformed-escape diagnostic. + return null; + } + } + + private static boolean isSurrogate(long value) { + return value >= Character.MIN_SURROGATE + && value <= Character.MAX_SURROGATE; + } + + private static void appendHexClassRange(StringBuilder out, long start, long end) { + out.append("\\x{") + .append(Long.toHexString(start).toUpperCase(java.util.Locale.ROOT)) + .append('}'); + if (start != end) { + out.append("-\\x{") + .append(Long.toHexString(end).toUpperCase(java.util.Locale.ROOT)) + .append('}'); + } + } + + private static String fixedWidthHexRange(int start, int end, int width) { + if (width <= 0) return ""; + int divisor = 1 << ((width - 1) * 4); + int startDigit = start / divisor; + int endDigit = end / divisor; + int startRemainder = start % divisor; + int endRemainder = end % divisor; + if (startDigit == endDigit) { + return hexDigit(startDigit) + + fixedWidthHexRange(startRemainder, endRemainder, width - 1); + } + + List alternatives = new ArrayList<>(); + alternatives.add(hexDigit(startDigit) + + fixedWidthHexRange(startRemainder, divisor - 1, width - 1)); + for (int digit = startDigit + 1; digit < endDigit; digit++) { + alternatives.add(hexDigit(digit) + fullHexSuffix(width - 1)); + } + alternatives.add(hexDigit(endDigit) + + fixedWidthHexRange(0, endRemainder, width - 1)); + return alternatives.size() == 1 ? alternatives.get(0) + : "(?:" + String.join("|", alternatives) + ")"; + } + + private static String fullHexSuffix(int width) { + if (width <= 0) return ""; + return width == 1 ? "[0-9A-F]" : "[0-9A-F]{" + width + "}"; + } + + private static char hexDigit(int value) { + return "0123456789ABCDEF".charAt(value); + } + + /** + * Keep an ordinary class alternative from starting inside the visible + * payload of an internal scalar marker during an unanchored search. Each + * lookbehind is fixed-width for Joni; an unsigned Perl UV has at most 16 + * hexadecimal digits. + */ + private static String internalScalarBoundaryGuard() { + StringBuilder guard = new StringBuilder("(? Date: Tue, 18 Aug 2026 13:53:19 +0200 Subject: [PATCH 4/9] docs(regex): checkpoint surrogate renderer validation Record PR 1049's focused and full-build gates, keep the generated map as an explicit pending acceptance condition, and track the separate native-property marker-boundary reducer. Document the overloaded-corpus cleanup and lower-load rerun rule. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- AGENTS.md | 1 + dev/design/phase36-regex-parity.md | 34 +++++++++++++++++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1b909c066c..723b600468 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -200,6 +200,7 @@ | 2026-04-30 | A full afternoon chasing a phantom "DBIx::Class regression" in `t/76joins.t` / `t/96_is_deteministic_value.t` | Investigative agent launched the test repeatedly under `/usr/bin/time -p ./jperl …` (no `timeout` wrapper). Each hung JVM survived past the agent's lifetime, accumulated as ~14 orphans at 100% CPU each, and starved the active `jcpan` harness — which then SIGKILLed innocent tests after 300 s of no TAP output. Symptom looked exactly like a real perf regression. Fix: always `timeout N ./jperl …` for any potentially-hanging run. | | 2026-08-06 | (no source work lost — build recovered) A process cleanup killed the active Gradle test workers, producing exit 137 failures in two shards. | Agent selected Java PIDs from a broad CPU list without first constraining them to stale processes. Recovery: rerun `make` without killing workers; subsequent build completed successfully. Fix: never kill by CPU list alone; identify the exact command and build ownership first. | | 2026-08-18 | (no source work lost — corpus rerun required) A generated Unicode corpus read the worktree JAR while a concurrent `make` replaced it, and an interpreter worker failed loading an ICU class. | Agent launched `jperl` corpus workers from the same worktree before its `shadowJar` task had finished. Recovery: let bounded workers finish and rerun both backends after the build stops mutating the artifact. Fix: never run `jperl`/corpus validation from a worktree whose build may replace its JAR; wait for the build or use a verified immutable artifact copy. | +| 2026-08-18 | (no source work lost — overloaded corpus rerun required) Eight generated TestProp JVMs overlapped another engineer's full build and the five-worker acceptance suite; the 20-minute outer commands ended while exact per-test timeout/JVM descendants remained at high CPU. | Agent treated corpus workers as independent of the two-build limit without accounting for total host load and descendant cleanup. Recovery: identify the exact worktree JAR, test names, PIDs, and parentage; terminate only those orphaned corpus runners/timeouts/JVMs; preserve the unrelated build and acceptance workers. Fix: run this corpus at lower parallelism after builds release their slots, and verify exact descendants after every outer timeout. | | 2026-08-17 | (no source work lost — stale workers removed) Failed `make` runs were interrupted after their known Joni failures, but their Gradle unit-shard workers survived and competed with later builds. | Agent sent Ctrl-C to the parent build session before all parallel workers had exited. Recovery: identified stale workers by PID, start time, and shard work directory, terminated only those exact PIDs, and left the current build and sibling repositories untouched. Fix: let failed parallel `make` runs finish naturally, or verify and clean up their exact child PIDs before starting another build. | | 2026-08-17 | (no source work lost — CPAN run rerun) A concurrent `make` replaced the development shadow JAR while an active `jcpan` process was spawning a child JVM, causing a transient `ClassNotFoundException`. | Agent waited for another worktree's build but did not wait for the same worktree's bounded CPAN runs before rebuilding `target/perlonjava-5.44.0.jar`. Recovery: let `make` finish and rerun the affected CPAN target. Fix: never rebuild a worktree's development JAR while that worktree has active `jperl` or `jcpan` processes. | diff --git a/dev/design/phase36-regex-parity.md b/dev/design/phase36-regex-parity.md index a45130db75..0e2591c782 100644 --- a/dev/design/phase36-regex-parity.md +++ b/dev/design/phase36-regex-parity.md @@ -288,6 +288,18 @@ The property corpus therefore reaches 153,882/167,506 and the complete property-plus-boundary evidence reaches 393,748/407,372, leaving 13,624 property assertions. +Draft PR #1049 implements the shared marker-aware surrogate property +renderer. Its standard-Perl-first focused oracle passes 27/27 on standard +Perl, JVM, and interpreter; direct Joni tests, packaging verification, and a +warning-free 4m58s `make` pass. Positive and complemented properties, +ordinary and negated classes, captures, substitution, and `/g` now consume +U+D800..U+DFFF markers as one Perl scalar. The exact generated chunks 01–04 +map remains the acceptance gate before changing the totals above. The first +two map attempts were discarded rather than counted because one read a JAR +while `shadowJar` replaced it and the next exceeded its resource bound while +overlapping the full-tree acceptance suite. The rerun must use the stable JAR +at lower parallelism after competing heavy workers finish. + Lexical `use bytes` now compiles non-ASCII substitution patterns with a single-byte Joni encoding while preserving upgraded, byte-backed, and compiled `qr//` source provenance. The focused oracle passes 12/12 on system Perl, JVM, @@ -815,9 +827,20 @@ is retained for now. with only nine isolated-surrogate TODOs; chunk 01 gains exactly 2,765 numbered assertions with zero losses and exact 41,848-assertion backend identity, reaching 153,882/167,506 properties. - - [ ] Render resolved surrogate property subsets as Perl scalar markers so + - [x] Render resolved surrogate property subsets as Perl scalar markers so positive, complemented, class, capture, substitution, and `/g` membership - preserve the exact U+D800..U+DFFF truth table. + preserve the exact U+D800..U+DFFF truth table. The focused oracle passes + 27/27 on standard Perl, JVM, and interpreter; warning-free `make`, direct + Joni, and packaging gates pass in draft PR #1049. + - [ ] Complete the authoritative stable-JAR chunks 01–04 map for the + surrogate renderer with exact JVM/interpreter identity, zero losses, and + no missing numbered assertions before updating aggregate totals. + - [ ] Prevent unanchored native Joni properties/classes that contain no + translated surrogate range from beginning inside the visible payload of + an internal scalar marker. The isolated reducer at + `/tmp/phase36-native-property-marker-boundary.t` passes 2/2 on standard + Perl and 1/2 on PerlOnJava; this is separate from PR #1049's + surrogate-bearing translated-class renderer. - [x] Integrated native Perl `\v`/`\V` dispatch inside and outside character classes (`1eff1db97`, integrated as `6328935cd`). The focused oracle passes 92/92 and unchanged `reg_posixcc.t` passes 2,560/2,560 on both backends. @@ -874,9 +897,10 @@ is retained for now. 3. Preserve draft PR #1046's completed combined QC/HST, five-family enumerated, and InPC/InSC map plus the follow-on mechanical-cleanup, Identifier, `kEH_Core`, and Block/Blk wildcard checkpoints. Integrate the independently - validated importer-owned Unikemet snapshot, then complete the shared - marker-aware isolated-surrogate property renderer. Preserve pinned Perl 5.44 - acceptance and rejection semantics rather than inheriting host ICU breadth. + validated importer-owned Unikemet snapshot, then complete PR #1049's + stable-JAR generated map and the separate native-property marker-boundary + reducer. Preserve pinned Perl 5.44 acceptance and rejection semantics + rather than inheriting host ICU breadth. The post-Block generated residual is 13,624 assertions: 12,797 alias/precedence, 476 wildcard, 346 diagnostic/value-policy, and five shared runtime assertions. From 8372ccb63b93c51b7a63fcbdd29bb28e410e2aec Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 13:56:45 +0200 Subject: [PATCH 5/9] docs(regex): track progressive Joni routing cleanup Record the temporary Java routing workaround without weakening the full-Joni architecture or Phase 1 and Phase 5 exit criteria. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- dev/design/phase36-regex-parity.md | 36 +++++++++++++++++++++--------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/dev/design/phase36-regex-parity.md b/dev/design/phase36-regex-parity.md index 0e2591c782..af7fead395 100644 --- a/dev/design/phase36-regex-parity.md +++ b/dev/design/phase36-regex-parity.md @@ -41,12 +41,18 @@ baseline, with no per-file pass-count regressions. ### Migration controls -A temporary developer-only backend selector supports separate Java and Joni -corpus runs. It must never run both matchers for one operation because callbacks, -tied variables, `pos()`, and substitutions may have observable side effects. -Joni is the default matcher; explicit Java mode remains only for differential -measurement. The selector and Java matching fields are removed at the end of the -migration. +A temporary backend selector supports separate Java and Joni corpus runs. It +must never run both matchers for one operation because callbacks, tied +variables, `pos()`, and substitutions may have observable side effects. The +intended migration default is Joni. While the remaining forced-Joni corpus gap +would otherwise regress the PR 958 acceptance baseline, automatic routing may +temporarily keep ordinary patterns on Java and require Joni only for executable +callbacks/dynamic callouts and the advanced constructs recognized by +`requiresJoniBackend`. Forced-Joni differential coverage remains mandatory. +This progressive-routing workaround is not a final architecture, does not +satisfy the Phase 1 or Phase 5 exit criteria, and must be removed at the +earliest evidence-backed point. The selector and all Java matching fields are +removed at the end of the migration. ### Preprocessing boundary @@ -571,6 +577,11 @@ is retained for now. - [ ] Phase 1: Joni ordinary-pattern parity (implementation substantially complete; forced Java/Joni corpus comparison remains) - [x] Added the temporary backend selector and made Joni the default. + - [ ] Remove the temporary progressive-routing acceptance workaround after + closing its forced-Joni regressions. Until then automatic routing keeps + ordinary patterns on Java while executable closures, dynamic callouts, + recursion/conditions, lookbehind, and control verbs require Joni. This is + explicitly cleanup debt and does not weaken the all-Joni exit criterion. - [x] Routed ordinary matching, substitution, and split through the selected backend without per-operation fallback. - [x] Completed the forced-Java/JVM 80-file leg and identified the @@ -909,10 +920,15 @@ is retained for now. 600-second bound and retain complete TAP/JSON. After the two fatal roots and native line boundaries integrate, refresh the complete forced-Joni 80-file corpus and apply the no-regression gate against Phase 0 and PR 958. -5. Audit every `RegexPreprocessor` rule against the final ownership boundary. - Move matcher semantics into Joni, retain only source-policy scanning, delete - Java-only rewrites and compiled-pattern variants, and remove the temporary - Java backend selector after the performance gate passes. +5. Continue the forced-Joni remediation immediately after the temporary + progressive-routing integration gate. Prioritize the ordinary-pattern + regressions hidden by automatic Java routing, remove that workaround as soon + as the PR 958 no-regression gate permits, and do not mark Phase 1 complete + while it remains. Audit every `RegexPreprocessor` rule against the final + ownership boundary; move matcher semantics into Joni, retain only + source-policy scanning, delete Java-only rewrites and compiled-pattern + variants, and remove the temporary backend selector after the performance + gate passes. 6. Reconcile `docs/reference/feature-matrix.md` with the final corpus; update `dev/implementation/regex.md` and `docs/design/joni-callout-fork.md` to the as-implemented architecture and review both for clarity and structure. From 6115ec11d292f391f97cb0940d9d684a1317c408 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 14:03:03 +0200 Subject: [PATCH 6/9] docs(regex): record final PR 1042 priority map Capture the exact pre-workaround PR 958 comparison and use its recoverable assertion counts to rank the next full-Joni remediation slices. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- dev/design/phase36-regex-parity.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/dev/design/phase36-regex-parity.md b/dev/design/phase36-regex-parity.md index af7fead395..f7405bd76c 100644 --- a/dev/design/phase36-regex-parity.md +++ b/dev/design/phase36-regex-parity.md @@ -564,6 +564,16 @@ Strict-regex source policy and Perl code points above U+10FFFF remain explicit frontend/representation debt, so the forced-Java underscore compatibility pass is retained for now. +The final pre-workaround PR #1042 integration run completed at +505,864/561,385 against PR 958's 269,171/324,252. Its exact file comparison +reports 41 regression files and a 5,035-assertion negative sum. The leading +pure-Joni blockers are 4,298 assertions behind the first aborts in +`pat_advanced{,_thr}.t` and `pat{,_thr}.t`, followed by 224 in `reg_mesg.t` +and 259 across seven `regexp*` variants; `regex_sets.t` is now only one +assertion below baseline. These counts prioritize the cleanup after temporary +progressive routing: unblock the four `pat*` files first, then diagnostics and +the ordinary `regexp*` matrix. They are not accepted limitations. + ### Completed Phases - [x] Phase 0: Reproducible differential baseline (2026-08-17) From d7c0b7254d4135db78ed4c4d9f14a0136ebca063 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 14:12:00 +0200 Subject: [PATCH 7/9] docs(regex): record progressive routing checkpoint Record PR 1042's temporary progressive implementation, its forced-Joni safety gate, and the evidence that still keeps cleanup open. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- dev/design/phase36-regex-parity.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dev/design/phase36-regex-parity.md b/dev/design/phase36-regex-parity.md index f7405bd76c..9823c24170 100644 --- a/dev/design/phase36-regex-parity.md +++ b/dev/design/phase36-regex-parity.md @@ -587,6 +587,12 @@ the ordinary `regexp*` matrix. They are not accepted limitations. - [ ] Phase 1: Joni ordinary-pattern parity (implementation substantially complete; forced Java/Joni corpus comparison remains) - [x] Added the temporary backend selector and made Joni the default. + - [x] Added the explicitly temporary progressive acceptance route in PR + #1042 (`7173d9bff`): default/`auto` keeps ordinary patterns on Java while + every existing `requiresJoniBackend` construct still forces Joni. Explicit + Joni mode and the unchanged unit corpus remain forced-Joni; its focused + nine-file gate gained six passes with no regressions and `make` passed + warning-free in 4m04s. - [ ] Remove the temporary progressive-routing acceptance workaround after closing its forced-Joni regressions. Until then automatic routing keeps ordinary patterns on Java while executable closures, dynamic callouts, From 341bcd96ff328fc26a84702a2b19f0285291371b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 14:18:34 +0200 Subject: [PATCH 8/9] docs(regex): record bare Script alias checkpoint Record the independently validated exact +5,064 target-map gain while keeping aggregate totals deferred until the old-base slice is rebased. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- dev/design/phase36-regex-parity.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/dev/design/phase36-regex-parity.md b/dev/design/phase36-regex-parity.md index 9823c24170..5366054a80 100644 --- a/dev/design/phase36-regex-parity.md +++ b/dev/design/phase36-regex-parity.md @@ -461,6 +461,16 @@ focused precedence, class-negation, and bare-scx reducers pass 7/7, 8/8, and regressions and exact JVM/interpreter counts, raising current generated evidence to 377,602/407,367. +The independently validated bare-Script alias follow-up is published as +`296f240c2`. It applies Perl's ASCII-loose bare `Is` Script shortcut without +changing explicit assignment, generated-data, or precedence policy. Its +standard-Perl, JVM, and interpreter oracle passes 12/12; protected boundaries +remain exact; warning-free `make`, Joni, and packaging pass. On its exact old +base, the pinned target map gains 3,832 assertions in chunk 03 and 1,232 in +chunk 04, exactly +5,064 with zero missing/failing target IDs and exact backend +identity. Aggregate totals remain deferred until this commit is rebased onto +the current residual stack. + `Grapheme_Cluster_Break`/`gcb`, `Sentence_Break`/`sb`, `Word_Break`/`wb`, and `Line_Break`/`lb` assignments now resolve every pinned Unicode 17 value. Short, long, loose, colon, wildcard, missing-value, and exact @@ -796,6 +806,11 @@ the ordinary `regexp*` matrix. They are not accepted limitations. aliases, precedence, and ordinary character-class complements. The focused oracle passes 95/95; chunks 01–04 gain 8,140 assertions with zero numbered regressions and exact backend counts. + - [x] Validated bare ASCII-loose `Is` Script aliases independently + (`296f240c2`): 12/12 on standard Perl, JVM, and interpreter, protected + boundaries exact, warning-free `make`, and an exact pinned target-map gain + of 5,064 with zero losses or backend differences. Rebase/integration onto + the current residual stack remains pending before aggregate totals change. - [x] Generated and integrated pinned Unicode 17.0 GCB/SB/WB/LB property values with official aliases, loose and wildcard policy, exact `Is` rejection rules, and complete defaults. The focused oracle passes 92/92; From 74cdec7666d9e8644378864c2a592202b510cb79 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 18 Aug 2026 15:07:36 +0200 Subject: [PATCH 9/9] docs(regex): record authoritative surrogate property map Record exact JVM/interpreter identity at 154,132/167,506, 250 gains with no losses or missing assertions, and the resulting 393,998/407,372 generated property-plus-boundary total. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex --- dev/design/phase36-regex-parity.md | 32 ++++++++++++++++-------------- docs/reference/feature-matrix.md | 4 ++-- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/dev/design/phase36-regex-parity.md b/dev/design/phase36-regex-parity.md index 5366054a80..1654507cab 100644 --- a/dev/design/phase36-regex-parity.md +++ b/dev/design/phase36-regex-parity.md @@ -299,12 +299,13 @@ renderer. Its standard-Perl-first focused oracle passes 27/27 on standard Perl, JVM, and interpreter; direct Joni tests, packaging verification, and a warning-free 4m58s `make` pass. Positive and complemented properties, ordinary and negated classes, captures, substitution, and `/g` now consume -U+D800..U+DFFF markers as one Perl scalar. The exact generated chunks 01–04 -map remains the acceptance gate before changing the totals above. The first -two map attempts were discarded rather than counted because one read a JAR -while `shadowJar` replaced it and the next exceeded its resource bound while -overlapping the full-tree acceptance suite. The rerun must use the stable JAR -at lower parallelism after competing heavy workers finish. +U+D800..U+DFFF markers as one Perl scalar. The authoritative stable-JAR, +two-worker chunks 01–04 map passes 154,132/167,506 on both JVM and interpreter +with exact numbered-test identity, 250 gains, zero losses, and no missing +assertions. Combined with the complete boundary corpus, generated evidence is +393,998/407,372 and the residual property surface is 13,374 assertions. The +first two resource-collided attempts remain explicitly discarded and do not +contribute to these totals. Lexical `use bytes` now compiles non-ASCII substitution patterns with a single-byte Joni encoding while preserving upgraded, byte-backed, and compiled @@ -874,9 +875,10 @@ the ordinary `regexp*` matrix. They are not accepted limitations. preserve the exact U+D800..U+DFFF truth table. The focused oracle passes 27/27 on standard Perl, JVM, and interpreter; warning-free `make`, direct Joni, and packaging gates pass in draft PR #1049. - - [ ] Complete the authoritative stable-JAR chunks 01–04 map for the - surrogate renderer with exact JVM/interpreter identity, zero losses, and - no missing numbered assertions before updating aggregate totals. + - [x] Completed the authoritative stable-JAR chunks 01–04 map for the + surrogate renderer at 154,132/167,506 on both backends: exactly 250 gains, + zero losses, no missing numbered assertions, and exact backend identity. + Generated property-plus-boundary evidence is 393,998/407,372. - [ ] Prevent unanchored native Joni properties/classes that contain no translated surrogate range from beginning inside the visible payload of an internal scalar marker. The isolated reducer at @@ -939,13 +941,13 @@ the ordinary `regexp*` matrix. They are not accepted limitations. 3. Preserve draft PR #1046's completed combined QC/HST, five-family enumerated, and InPC/InSC map plus the follow-on mechanical-cleanup, Identifier, `kEH_Core`, and Block/Blk wildcard checkpoints. Integrate the independently - validated importer-owned Unikemet snapshot, then complete PR #1049's - stable-JAR generated map and the separate native-property marker-boundary - reducer. Preserve pinned Perl 5.44 acceptance and rejection semantics + validated importer-owned Unikemet snapshot, preserve PR #1049's completed + stable-JAR generated map, and integrate the separate native-property + marker-boundary reducer. Preserve pinned Perl 5.44 acceptance and rejection semantics rather than inheriting host ICU breadth. - The post-Block generated residual is 13,624 assertions: 12,797 - alias/precedence, 476 wildcard, 346 diagnostic/value-policy, and five shared - runtime assertions. + The post-surrogate-renderer generated residual is 13,374 assertions; retain + the exact numbered differential as the classification source for subsequent + alias/precedence, wildcard, diagnostic/value-policy, and shared-runtime work. Keep native `\v`/`\V` exact at 2,560/2,560 in `reg_posixcc.t`. 4. Rerun generated property chunks 01–04 on both backends with the classified 600-second bound and retain complete TAP/JSON. After the two fatal roots and diff --git a/docs/reference/feature-matrix.md b/docs/reference/feature-matrix.md index 135103832a..091c410758 100644 --- a/docs/reference/feature-matrix.md +++ b/docs/reference/feature-matrix.md @@ -398,8 +398,8 @@ my @copy = @{$z}; # ERROR - ✅ **Branch Reset Groups**: `(?|...)` resets capture numbering across alternatives and preserves mapped match variables. - ✅ **Advanced Subroutine Calls**: Sub-pattern calls with numbered or named references like `(?1)` and `(?&name)` execute through Joni. - ✅ **Conditional Expressions**: Numbered and named capture conditions, positive and negative assertion conditions, recursion conditions `(?(R))`, `(?(R1))`, and `(?(R&name))`, executable callback conditions, and optimistic predicates execute through Joni. -- 🟡 **Extended Unicode Regex Features**: Complete pinned Script, Script_Extensions, Block, break-property, 51 core binary-property, seven specialized binary-property, QC/HST, five residual enumerated-property, InPC/InSC, Identifier_Status, Identifier_Type, and provisional `kEH_Core` sets, `Extended_Pictographic`, `Age`, `In`/`Present_In`, General_Category, Canonical_Combining_Class, Bidi_Class, Decomposition_Type, East_Asian_Width, Numeric_Value, Joining_Group, and invalid-property gates execute through Joni, and `regexp_unicode_prop.t` passes 1,110/1,110. Block/Blk value wildcards preserve official aliases, Perl-loose matching, colon/slash delimiters, warning order, and the `InKana` collision policy. Current generated evidence is 393,748/407,372 on both backends: chunks 01–04 pass 153,882/167,506 and the complete boundary corpus passes 239,866/239,866. -- 🟡 **Remaining generated Unicode property surface**: The measured post-Block-wildcard map has 13,624 residual assertions: 12,797 alias/precedence, 476 wildcard, 346 diagnostic/value-policy, and five shared boundary/runtime assertions. The shared marker-aware isolated-surrogate renderer is the next cross-cutting property slice. The focused Block oracle passes 38/47 per backend and the focused `kEH_Core` oracle passes 167/169 because isolated-surrogate property membership remains pending. +- 🟡 **Extended Unicode Regex Features**: Complete pinned Script, Script_Extensions, Block, break-property, 51 core binary-property, seven specialized binary-property, QC/HST, five residual enumerated-property, InPC/InSC, Identifier_Status, Identifier_Type, and provisional `kEH_Core` sets, `Extended_Pictographic`, `Age`, `In`/`Present_In`, General_Category, Canonical_Combining_Class, Bidi_Class, Decomposition_Type, East_Asian_Width, Numeric_Value, Joining_Group, and invalid-property gates execute through Joni, and `regexp_unicode_prop.t` passes 1,110/1,110. Block/Blk value wildcards preserve official aliases, Perl-loose matching, colon/slash delimiters, warning order, and the `InKana` collision policy. The marker-aware isolated-surrogate renderer raises current generated evidence to 393,998/407,372 on both backends: chunks 01–04 pass 154,132/167,506 with exact numbered-test identity and the complete boundary corpus passes 239,866/239,866. +- 🟡 **Remaining generated Unicode property surface**: The authoritative post-surrogate-renderer map has 13,374 residual assertions. The renderer gains 250 assertions over the preceding family-specific baselines with zero losses and no missing numbered assertions; remaining failures are alias/precedence, wildcard, diagnostic/value-policy, and five shared boundary/runtime assertions tracked by the generated differential. - 🟡 **Extended regex sets**: `(?[...])` set expressions are implemented through the frontend extended-character-class translator, but full Perl parser and diagnostic parity is not complete in native Joni. - ✅ **Extended Grapheme Clusters**: Native `\b{gcb}`/`\B{gcb}` implement GB1–GB13 and GB999, and `\X` includes repeated GB9c Indic conjuncts. The complete 8,516-assertion GCB/`\X` section of authoritative chunk 05 passes identically on JVM and interpreter. - ✅ **Unicode Sentence Boundaries**: Native `\b{sb}`/`\B{sb}` implement SB1–SB11 and SB998 from the reproducible Perl 5.44 Unicode 17.0 sentence-break table. Authoritative chunk 05 passes 14,976/14,976 identically on JVM and interpreter.