Skip to content

Commit 33f4146

Browse files
authored
Merge pull request #970 from fglock/feature/joni-dynamic-patterns
feat(regex): execute dynamic patterns through Joni
2 parents 9c8a933 + 6418466 commit 33f4146

23 files changed

Lines changed: 552 additions & 66 deletions

File tree

build.gradle

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ cyclonedxBom {
8484
outputFormat = "all" // Generate both JSON and XML
8585
componentName = "perlonjava"
8686
componentVersion = project.version
87-
organizationalEntity { oe ->
87+
organizationalEntity = { oe ->
8888
oe.name = "PerlOnJava Project"
8989
oe.urls = ["https://github.com/fglock/PerlOnJava"]
9090
}
@@ -260,6 +260,10 @@ tasks.withType(JavaCompile).configureEach {
260260
// Test execution configuration with native access and adequate heap
261261
tasks.withType(Test).configureEach {
262262
jvmArgs += '--enable-native-access=ALL-UNNAMED'
263+
// Netty still uses the transitional sun.misc.Unsafe memory API. PerlOnJava
264+
// requires Java 24, where explicitly allowing it suppresses the terminal
265+
// deprecation banner while retaining the tested Netty allocation path.
266+
jvmArgs += '--sun-misc-unsafe-memory-access=allow'
263267
maxHeapSize = '1g'
264268
}
265269

docs/design/joni-callout-fork.md

Lines changed: 43 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,11 @@ commit messages, not here.
2828

2929
## Internal callout syntax
3030

31-
The structured Perl regex frontend emits `(?{=CALL:<id>})` only in an
32-
engine-facing skeleton. `<id>` is a non-negative decimal index into the callback
33-
table owned by that regex value. The fork parses this representation into a
34-
dedicated callout node; it never parses Perl source.
31+
The structured Perl regex frontend emits `(?{=CALL:<id>})` for plain and
32+
conditional callbacks and `(?{=DYNAMIC:<id>})` for dynamic subprograms only in
33+
an engine-facing skeleton. `<id>` is a non-negative decimal index into the
34+
callback table owned by that regex value. The fork parses these representations
35+
into dedicated callout nodes; it never parses Perl source.
3536

3637
Runtime-interpolated pattern text must not be promoted to trusted skeleton
3738
syntax. The PerlOnJava frontend remains responsible for preserving source
@@ -44,10 +45,17 @@ The fork exposes a runtime-neutral API in `org.joni`:
4445
```java
4546
interface CalloutHandler {
4647
CalloutResult execute(int calloutId, MatchView match);
48+
default DynamicPatternResult executeDynamic(int calloutId, MatchView match);
4749
void unwind(Object backtrackToken);
4850
default void complete(Object successfulToken) { unwind(successfulToken); }
4951
}
5052

53+
final class DynamicPatternResult {
54+
Regex getRegex();
55+
CalloutHandler getCalloutHandler();
56+
Object getBacktrackToken();
57+
}
58+
5159
interface MatchView {
5260
int currentBytePosition();
5361
int captureCount();
@@ -64,6 +72,11 @@ state.
6472
token. The engine neither interprets that token nor depends on PerlOnJava
6573
runtime classes.
6674

75+
`DynamicPatternResult` supplies a compiled nested program, its matcher-local
76+
handler, and the token for the dynamic expression's provisional Perl state. The
77+
default `executeDynamic` implementation fails fast, so handlers that only use
78+
plain callouts remain source compatible.
79+
6780
## Execution and unwind contract
6881

6982
1. The callout opcode invokes the handler with the callout ID and a read-only
@@ -78,12 +91,20 @@ runtime classes.
7891
still-active token in reverse execution order.
7992
6. A `FAIL` result enters normal matcher backtracking after installing the frame,
8093
so cleanup follows the same path as later-pattern failure.
81-
7. Patterns without callout nodes allocate no handler state or callout frames.
94+
7. A dynamic callout resolves its nested program only when execution reaches
95+
the opcode. The nested matcher yields one result at a time; an outer failure
96+
resumes the nested matcher at its next alternative before the outer engine
97+
backtracks past the dynamic frame.
98+
8. Nested captures remain private to the nested matcher. Its endpoint advances
99+
the outer input position, while outer capture numbering and match variables
100+
remain unchanged.
101+
9. Completing or abandoning a nested continuation propagates completion or
102+
unwind to all of its active callback tokens exactly once.
103+
10. Patterns without callout nodes allocate no handler state or callout frames.
82104

83105
Callback conditions use an internal conditional-callout opcode. `CONTINUE`
84106
selects the yes branch and `FAIL` selects the no branch without treating the
85-
condition itself as a failed match. Dynamic nested patterns remain outside this
86-
contract until their alternatives can participate in outer backtracking.
107+
condition itself as a failed match.
87108

88109
## Structured Perl frontend
89110

@@ -106,10 +127,11 @@ retaining the last successful plain callback result.
106127

107128
Every structured executable callback template selects Joni, including a
108129
callback whose body happens to return a constant: `(?{ 1 })` still has match-time
109-
side effects and unwind semantics. Constant dynamic-pattern expressions
110-
`(??{ ... })` may retain the existing compile-time fold path; a runtime-dependent
111-
dynamic-pattern closure selects Joni only after the nested-pattern backtracking
112-
contract is implemented.
130+
side effects and unwind semantics. Runtime-dependent dynamic-pattern expressions
131+
also select Joni and execute only when their opcode is reached. Semantically safe
132+
constant expressions may use the compile-time fold path; constants with captures
133+
or top-level alternatives remain dynamic so they cannot change outer grouping or
134+
capture numbers.
113135

114136
Literal match targets have one stable scalar identity per compiled call site so
115137
`pos()` and `/g` survive repeated loop execution without allowing two identical
@@ -170,8 +192,9 @@ branch reset, subpattern calls, conditions, extended Unicode and graphemes,
170192
embedded code, regex debugging, runtime eval, lexical default flags, and named
171193
capture behavior. As each feature becomes executable, patterns containing it
172194
select Joni. The current selector routes declarative subpattern calls and every
173-
structured executable callback template; constant `(??{...})` expressions keep
174-
their established fold-to-pattern path.
195+
structured executable callback template, including runtime `(??{...})`.
196+
Semantically safe constant dynamic expressions may keep their established
197+
fold-to-pattern path.
175198

176199
## Implementation stages
177200

@@ -185,8 +208,8 @@ their established fold-to-pattern path.
185208
6. Publish provisional match state for plain callbacks and preserve `$^R` across
186209
successful completion and backtracking.
187210
7. Add dynamic-local checkpoints and callback conditions.
188-
8. Add dynamic nested patterns only after focused differential tests establish
189-
their outer-backtracking contract.
211+
8. Resolve dynamic nested programs at match time and preserve their alternatives
212+
as resumable matcher continuations on the outer backtracking stack.
190213

191214
## Verification
192215

@@ -199,8 +222,11 @@ their established fold-to-pattern path.
199222
both relocated trees, and includes all required notices.
200223
- An embedding smoke test can load stock Joni and PerlOnJava together.
201224
- Full `make` and the focused direct/thread regex matrix pass.
202-
- The executable-callback unit test passes under standard Perl and both
203-
PerlOnJava execution backends.
225+
- The executable-callback and dynamic-pattern unit tests pass under standard
226+
Perl and both PerlOnJava execution backends.
227+
- Dynamic-pattern tests cover delayed expression evaluation, returned strings
228+
and `qr//` values, private nested captures, outer-suffix backtracking, nested
229+
callback cleanup, and recursive `qr//` values.
204230
- `dev/tools/perl_test_runner.pl perl5_t/t/re/` is compared file-by-file with
205231
`../PerlOnJava/logs/test_20260815_080000_958.log`; no previously passing regex
206232
file may regress, and changed pass counts or blocked-test totals are reported.

docs/reference/feature-matrix.md

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -388,21 +388,20 @@ my @copy = @{$z}; # ERROR
388388
### Missing Regular Expression Features
389389

390390
-**Dynamically-scoped regex variables**: Regex variables are not dynamically-scoped.
391-
- 🟡 **Recursive Patterns**: `(?R)` and `(?0)` execute through Joni. Constant `(??{ code })` expressions fold to a pattern; runtime-dependent dynamic patterns remain unsupported.
392-
- 🟡 **Backtracking Control Verbs and Definitions**: `(?(DEFINE)...)` and named recursive definitions execute through Joni. Control verbs such as `(*PRUNE)`, `(*SKIP)`, `(*THEN)`, and `(*COMMIT)` are not yet complete. Atomic groups `(?>...)` are supported as noted above.
391+
- 🟡 **Recursive and Dynamic Patterns**: `(?R)`, `(?0)`, and runtime `(??{ code })` execute through Joni. Dynamic expressions may return strings or `qr//` values, and nested alternatives participate in outer backtracking without changing outer grouping or capture numbering. Deep dynamic recursion still needs an engine-owned depth limit that does not depend on the Java call stack.
392+
-**Backtracking Control Verbs**: `(*ACCEPT)`, `(*PRUNE)`, `(*SKIP)`, `(*THEN)`, and `(*COMMIT)` are not supported. `(*FAIL)`/`(*F)` and atomic groups `(?>...)` are supported.
393+
-**Regex Definitions**: `(?(DEFINE)...)` containers and numbered or named calls to their subpatterns execute through Joni.
393394
-**Lookbehind Assertions**: Variable-length negative or positive lookbehind assertions, e.g., `(?<=...)` or `(?<!...)`, are not supported.
394-
- **Branch Reset Groups**: Use of `(?|...)` to reset group numbering across branches is not supported.
395+
- **Branch Reset Groups**: `(?|...)` resets capture numbering across alternatives and preserves mapped match variables.
395396
-**Advanced Subroutine Calls**: Sub-pattern calls with numbered or named references like `(?1)` and `(?&name)` execute through Joni.
396397
- 🟡 **Conditional Expressions**: Executable callback conditions `(?(?{ code })yes|no)` execute through Joni; other condition forms are not yet complete.
397398
-**Extended Unicode Regex Features**: Some extended Unicode regex functionalities are not supported.
398399
-**Extended Grapheme Clusters**: Matching with `\X` for extended grapheme clusters is not supported.
399-
- 🟡 **Embedded Code in Regex**: `(?{ code })` and executable callback conditions run as lexical closures in Joni with provisional captures, `$^R`, and backtracking unwind. Runtime-dependent `(??{ code })` remains unsupported.
400-
- 🟡 **Regex Debugging**: Lexical `use/no re 'debug'` and `debugcolor` state is runtime-owned and produces a stable compile/execution trace on both backends and in child threads. Exact Perl optimizer opcodes and diagnostic wording are not complete.
401-
- **Regex Optimizations**: Using `use re 'eval';` for runtime regex compilation is not supported.
400+
- 🟡 **Embedded Code in Regex**: `(?{ code })`, executable callback conditions, and `(??{ code })` run as lexical closures in Joni with provisional captures and backtracking unwind. Optimistic callbacks `(*{ code })` are not supported.
401+
- **Regex Debugging**: Lexically scoped `use/no re 'debug'` and `debugcolor` are supported, including runtime snapshot ownership.
402+
- 🟡 **Runtime Regex Evaluation**: `use re 'eval'` controls whether interpolated patterns containing eval groups may compile, but runtime strings cannot manufacture trusted callback closures.
402403
-**Regex Compilation Flags**: Setting default regex flags with `use re '/flags';` is not supported.
403-
-**Stricter named captures**
404-
-**No underscore in named captures** `(?<test_field>test)` the name in named captures cannot have underscores.
405-
-**No duplicate named capture groups**: In Java regular expression engine, each named capturing group must have a unique name within a regular expression.
404+
-**Perl Named Captures**: Names may contain underscores, and duplicate named groups preserve Perl-style `%+`/`%-` and backreference behavior.
406405

407406

408407
## Statements and Special Operators

src/main/java/org/perlonjava/frontend/parser/StringSegmentParser.java

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
import org.perlonjava.frontend.lexer.LexerToken;
99
import org.perlonjava.frontend.lexer.LexerTokenType;
1010
import org.perlonjava.runtime.operators.PerlUtfString;
11-
import org.perlonjava.runtime.regex.RegexMarkers;
1211
import org.perlonjava.runtime.regex.UnicodeResolver;
1312
import org.perlonjava.runtime.runtimetypes.PerlCompilerException;
1413
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
@@ -919,15 +918,18 @@ private void parseRegexCodeBlock(boolean isRecursive) {
919918
TokenUtils.consume(parser, LexerTokenType.OPERATOR, ")");
920919

921920
if (isRecursive) {
922-
// Preserve the existing constant (??{...}) support. Runtime-dependent
923-
// nested patterns remain deferred to Stage 36.5.
921+
// Keep constant folding as the zero-overhead path, but preserve a
922+
// runtime-dependent expression as a lexical dynamic-program closure.
924923
Node folded = ConstantFoldingVisitor.foldConstants(block);
925924
if (folded instanceof BlockNode blockNode && blockNode.elements.size() == 1) {
926925
folded = blockNode.elements.getFirst();
927926
}
928927
RuntimeScalar constant = ConstantFoldingVisitor.getConstantValue(folded);
929-
segments.add(new StringNode(constant == null
930-
? RegexMarkers.RECURSIVE_PATTERN : constant.toString(), savedTokenIndex));
928+
if (constant == null || !isSafeDynamicConstantFold(constant.toString())) {
929+
segments.add(regexCallback(block, "DYNAMIC", savedTokenIndex));
930+
} else {
931+
segments.add(new StringNode(constant.toString(), savedTokenIndex));
932+
}
931933
} else {
932934
segments.add(regexCallback(block, "BLOCK", savedTokenIndex));
933935
}
@@ -941,6 +943,33 @@ private Node regexCallback(Node block, String kind, int index) {
941943
return callback;
942944
}
943945

946+
/**
947+
* Folding is safe only when textual insertion preserves the nested program's
948+
* grouping and capture isolation. A top-level alternative could absorb the
949+
* outer suffix, and a capturing group could consume an outer capture number.
950+
*/
951+
private static boolean isSafeDynamicConstantFold(String pattern) {
952+
boolean escaped = false;
953+
boolean inClass = false;
954+
for (int i = 0; i < pattern.length(); i++) {
955+
char ch = pattern.charAt(i);
956+
if (escaped) {
957+
escaped = false;
958+
} else if (ch == '\\') {
959+
escaped = true;
960+
} else if (ch == '[') {
961+
inClass = true;
962+
} else if (ch == ']' && inClass) {
963+
inClass = false;
964+
} else if (ch == '(' && !inClass) {
965+
return false;
966+
} else if (ch == '|' && !inClass) {
967+
return false;
968+
}
969+
}
970+
return true;
971+
}
972+
944973
/**
945974
* Gets a string context around the specified position for error reporting.
946975
* This shows the actual string content around where the error occurred.

0 commit comments

Comments
 (0)