Skip to content

Commit b137aaf

Browse files
fglockcodex
andcommitted
fix: restore post-Joni core behavior
Snapshot list-operator inputs before callback execution, preserve EBADF for descriptorless handles, and fetch tied regex interpolation exactly once. Restore regex callback scope when a Joni callout throws and document the next Phase 36 parity blockers. Add focused standard-Perl-valid regressions for each recovered behavior. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex <codex@openai.com>
1 parent 33f4146 commit b137aaf

9 files changed

Lines changed: 151 additions & 25 deletions

File tree

dev/design/phase36-regex-parity.md

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ timing delta is a regression only after a serialized same-commit reproduction.
240240

241241
### Current Status: Stage 36.4 in progress
242242

243+
The merged Joni dynamic-pattern engine establishes the Stage 36.5 execution
244+
seam. The current same-commit Stage 36.4 core baseline is `rxcode.t` 39/42 and
245+
`reg_eval_scope.t` 22/49, with no timeout or incomplete file. Callback
246+
exceptions now restore dynamic locals, provisional match state, and `$^R` on
247+
both execution backends even though the matcher cannot create an unwind token
248+
for a callout that throws.
249+
243250
### Completed stages
244251

245252
- [x] Stage 36.0: Refresh differential baseline
@@ -253,18 +260,24 @@ timing delta is a regression only after a serialized same-commit reproduction.
253260

254261
### Next steps
255262

256-
1. Refresh direct and wrapper counts on the merged callout engine.
257-
2. Extend the callback semantic matrix with dynamic-local unwind, exceptions,
258-
interruption, timeout, and nested matches.
259-
3. Complete Stage 36.4 without changing thread wrappers.
260-
4. Implement dynamic patterns and then the remaining declarative parity slices.
263+
1. Add matcher-owned mutation checkpoints so writes made by a callback on a
264+
path that later backtracks are restored. This is the direct blocker for
265+
`rxcode.t` assertions 26 and 34.
266+
2. Complete callback lexical pragma, caller-frame, and control-flow isolation
267+
exposed by `reg_eval_scope.t`, without changing its thread wrapper.
268+
3. Extend the callback semantic matrix with interruption, timeout, and nested
269+
exception paths; require identical JVM/interpreter cleanup.
270+
4. Complete the merged dynamic-pattern validation gates, then mark Stage 36.5
271+
complete and proceed to the remaining declarative parity slices.
261272

262273
### Open blockers
263274

264275
- Several callback-localization and dynamic-pattern capture rules still require
265276
standard-Perl differential evidence.
266-
- Dynamic `(??{ EXPR })`, optimistic callback execution, and several declarative
267-
controls are not complete.
277+
- Ordinary scalar and aggregate mutations performed by a callback are not yet
278+
transactionally restored when the matcher abandons that path.
279+
- Dynamic `(??{ EXPR })` execution is integrated, but its full Stage 36.5 CPAN
280+
and unchanged-core exit matrix is not yet recorded.
268281
- Partial direct core tests still contain diagnostic, parser, Unicode, and
269282
regex-object gaps; their wrappers must not be mistaken for thread failures.
270283

src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,14 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle)
361361
return scalarUndef;
362362
}
363363
int fd = descriptor.getInt();
364+
// Scalar-backed and other descriptorless handles report -1.
365+
// Passing that sentinel to isatty() turns Perl's EBADF/undef
366+
// result into a defined false value and clears $!.
367+
if (fd < 0) {
368+
getGlobalVariable("main::!").set(9);
369+
updateLastStat(fileHandle, false, 9);
370+
return scalarUndef;
371+
}
364372
try {
365373
boolean isTty = FFMPosix.get().isatty(fd) != 0;
366374
getGlobalVariable("main::!").set(0);

src/main/java/org/perlonjava/runtime/operators/ListOperators.java

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,19 @@
1010
import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue;
1111

1212
public class ListOperators {
13+
/**
14+
* Perl evaluates the input list before entering a map/grep-style block.
15+
* Keep the scalar objects (and therefore their aliasing) but detach the
16+
* iteration order from a source array that the block may mutate.
17+
*/
18+
private static List<RuntimeScalar> snapshotElements(RuntimeList runtimeList) {
19+
List<RuntimeScalar> snapshot = new ArrayList<>();
20+
for (RuntimeScalar element : runtimeList) {
21+
snapshot.add(element);
22+
}
23+
return snapshot;
24+
}
25+
1326
/**
1427
* Eagerly release captured variable references from an ephemeral grep/map/all/any
1528
* block closure. Like eval BLOCK closures, these blocks execute and are immediately
@@ -55,8 +68,8 @@ public static RuntimeList map(RuntimeList runtimeList, RuntimeScalar perlMapClos
5568
// This allows $_[0], $_[1], etc. to work inside map blocks
5669
RuntimeArray mapArgs = outerArgs != null ? outerArgs : new RuntimeArray();
5770

58-
// Iterate over each element in the current RuntimeArray
59-
for (RuntimeScalar element : runtimeList) {
71+
// Iterate over the list value captured before the block starts.
72+
for (RuntimeScalar element : snapshotElements(runtimeList)) {
6073
// Create $_ argument for the map subroutine
6174
GlobalVariable.aliasTemporaryGlobalVariable("main::_", element);
6275

@@ -244,8 +257,8 @@ public static RuntimeList grep(RuntimeList runtimeList, RuntimeScalar perlFilter
244257
// Use the outer @_ instead of an empty array
245258
RuntimeArray filterArgs = outerArgs != null ? outerArgs : new RuntimeArray();
246259

247-
// Iterate over each element in the current RuntimeArray
248-
for (RuntimeScalar element : runtimeList) {
260+
// Iterate over the list value captured before the block starts.
261+
for (RuntimeScalar element : snapshotElements(runtimeList)) {
249262
try {
250263
// Create $_ argument for the filter subroutine
251264
GlobalVariable.aliasTemporaryGlobalVariable("main::_", element);
@@ -320,8 +333,8 @@ public static RuntimeList all(RuntimeList runtimeList, RuntimeScalar perlFilterC
320333
try {
321334
RuntimeArray filterArgs = outerArgs != null ? outerArgs : new RuntimeArray();
322335

323-
// Iterate over each element in the current RuntimeArray
324-
for (RuntimeScalar element : runtimeList) {
336+
// Iterate over the list value captured before the block starts.
337+
for (RuntimeScalar element : snapshotElements(runtimeList)) {
325338
try {
326339
// Create $_ argument for the filter subroutine
327340
GlobalVariable.aliasTemporaryGlobalVariable("main::_", element);
@@ -380,8 +393,8 @@ public static RuntimeList any(RuntimeList runtimeList, RuntimeScalar perlFilterC
380393
try {
381394
RuntimeArray filterArgs = outerArgs != null ? outerArgs : new RuntimeArray();
382395

383-
// Iterate over each element in the current RuntimeArray
384-
for (RuntimeScalar element : runtimeList) {
396+
// Iterate over the list value captured before the block starts.
397+
for (RuntimeScalar element : snapshotElements(runtimeList)) {
385398
try {
386399
// Create $_ argument for the filter subroutine
387400
GlobalVariable.aliasTemporaryGlobalVariable("main::_", element);

src/main/java/org/perlonjava/runtime/regex/JoniRegexPattern.java

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -558,12 +558,20 @@ private Evaluation evaluate(RuntimeRegexCallback callback, MatchView match) {
558558
RuntimeScalar previousR = rVariable.clone();
559559
publishProvisional(match);
560560

561-
RuntimeScalar result = RuntimeCode.apply(new RuntimeScalar(callback.code),
562-
new RuntimeArray(), RuntimeContextType.SCALAR).scalar();
563-
boolean block = callback.kind == RuntimeRegexCallback.Kind.BLOCK;
564-
if (block) rVariable.set(result);
565-
Token token = new Token(localLevel, savedRegex, previousR, result.clone(), block);
566-
return new Evaluation(result, token);
561+
try {
562+
RuntimeScalar result = RuntimeCode.apply(new RuntimeScalar(callback.code),
563+
new RuntimeArray(), RuntimeContextType.SCALAR).scalar();
564+
boolean block = callback.kind == RuntimeRegexCallback.Kind.BLOCK;
565+
if (block) rVariable.set(result);
566+
Token token = new Token(localLevel, savedRegex, previousR, result.clone(), block);
567+
return new Evaluation(result, token);
568+
} catch (RuntimeException | Error failure) {
569+
// The matcher cannot register an unwind token when the callout
570+
// itself throws. Restore the provisional match and dynamic
571+
// scope here before the exception crosses an eval boundary.
572+
restoreCallbackScope(localLevel, savedRegex, previousR);
573+
throw failure;
574+
}
567575
}
568576

569577
@Override
@@ -584,15 +592,23 @@ void finish(boolean matched) {
584592
}
585593

586594
private void restore(Token token, boolean completed) {
587-
DynamicVariableManager.popToLocalLevel(token.localLevel());
588-
token.regexState().restore();
589-
GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("R"))
590-
.set(token.previousR());
595+
restoreCallbackScope(token.localLevel(), token.regexState(), token.previousR());
591596
if (completed && token.block() && completedResult == null) {
592597
completedResult = token.result();
593598
}
594599
}
595600

601+
private static void restoreCallbackScope(int localLevel, RegexState regexState,
602+
RuntimeScalar previousR) {
603+
try {
604+
DynamicVariableManager.popToLocalLevel(localLevel);
605+
} finally {
606+
regexState.restore();
607+
GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("R"))
608+
.set(previousR);
609+
}
610+
}
611+
596612
private void publishProvisional(MatchView match) {
597613
RuntimeRegexState state = PerlRuntime.current().regexState;
598614
int count = match.captureCount();

src/main/java/org/perlonjava/runtime/regex/RuntimeRegexTemplate.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import org.perlonjava.runtime.runtimetypes.RuntimeBase;
44
import org.perlonjava.runtime.runtimetypes.RuntimeList;
55
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
6+
import org.perlonjava.runtime.runtimetypes.RuntimeScalarType;
67

78
import java.util.ArrayList;
89
import java.util.List;
@@ -34,6 +35,11 @@ public static RuntimeScalar build(RuntimeList parts) {
3435
boolean tainted = false;
3536
for (RuntimeBase part : parts.elements) {
3637
RuntimeScalar scalar = part.scalar();
38+
// Interpolation is one scalar read. Resolve tied magic once, then
39+
// use that materialized value for type inspection, taint, and text.
40+
if (scalar.type == RuntimeScalarType.TIED_SCALAR) {
41+
scalar = scalar.tiedFetch();
42+
}
3743
tainted |= scalar.isTainted();
3844
if (scalar.value instanceof RuntimeRegexCallback callback) {
3945
int id = callbacks.size();
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
use strict;
2+
use warnings;
3+
4+
print "1..2\n";
5+
6+
my @mapped_source = (1, 2);
7+
my @mapped = map {
8+
unshift @mapped_source, 9;
9+
$_;
10+
} @mapped_source;
11+
print join(',', @mapped) eq '1,2' ? "ok 1\n" : "not ok 1\n";
12+
13+
my @grep_source = (1, 2);
14+
my @filtered = grep {
15+
unshift @grep_source, 9;
16+
1;
17+
} @grep_source;
18+
print join(',', @filtered) eq '1,2' ? "ok 2\n" : "not ok 2\n";
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use strict;
2+
use warnings;
3+
4+
our $localized = 'outer';
5+
'z' =~ /(z)/;
6+
7+
print "1..4\n";
8+
{
9+
local $^R = 9;
10+
my $ok = eval {
11+
'a' =~ /(a)(?{
12+
local $localized = 'inside';
13+
die "callback failure\n";
14+
})/;
15+
1;
16+
};
17+
print !defined($ok) && $@ =~ /callback failure/ ? "ok 1\n" : "not ok 1\n";
18+
print $localized eq 'outer' ? "ok 2\n" : "not ok 2\n";
19+
print $^R == 9 ? "ok 3\n" : "not ok 3\n";
20+
print !defined($1) ? "ok 4\n" : "not ok 4\n";
21+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use strict;
2+
use warnings;
3+
4+
{
5+
package TiedPattern;
6+
our $fetches = 0;
7+
sub TIESCALAR { bless [$_[1]], $_[0] }
8+
sub FETCH { $fetches++; $_[0][0] }
9+
}
10+
11+
print "1..2\n";
12+
13+
tie my $pattern, 'TiedPattern', 'foo';
14+
$_ = 'foo foo';
15+
/$pattern foo/;
16+
print $TiedPattern::fetches == 1 ? "ok 1\n" : "not ok 1\n";
17+
18+
$TiedPattern::fetches = 0;
19+
s/$pattern foo//;
20+
print $TiedPattern::fetches == 1 ? "ok 2\n" : "not ok 2\n";
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use strict;
2+
use warnings;
3+
4+
print "1..2\n";
5+
6+
my $buffer = "text";
7+
open my $handle, '<', \$buffer or die $!;
8+
$! = 0;
9+
my $is_tty = -t $handle;
10+
print !defined($is_tty) ? "ok 1\n" : "not ok 1\n";
11+
print 0 + $! == 9 ? "ok 2\n" : "not ok 2\n";

0 commit comments

Comments
 (0)