Skip to content

Commit e9be3cf

Browse files
feat: complete Perl threads compatibility phases 31-34
Bind native callbacks to their owning runtimes, harden detached thread lifecycle and shared graph validation, and preserve runtime-owned identity across thread snapshots. Complete the supported compatibility tranche with Unicode-property coordination, non-local lexical cleanup, documentation, and the DBIx::Class regression gate. Keep runtime pooling disabled until the fresh-runtime reset contract can be met, as documented in the concurrency design. Generated with [Codex](https://openai.com/codex) Co-Authored-By: Codex <158243242+openai-codex@users.noreply.github.com>
1 parent b863adf commit e9be3cf

40 files changed

Lines changed: 748 additions & 95 deletions

dev/design/concurrency.md

Lines changed: 40 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -571,27 +571,33 @@ Acceptance: `ipc_wait_timeout.t` observes Perl-compatible inherited-pipe
571571
behavior; default Test2 remains green; `AUTHOR_TESTING` and
572572
`T2_DO_THREAD_TESTS` thread suites form a separately reported stress gate.
573573

574-
### Phase 31 — Native callbacks and handle ownership
574+
### Phase 31 — Native callbacks and handle ownership (implemented 2026-08-13)
575575

576-
Define clone, child-owned creation, or explicit rejection for every native
577-
handle class. Bind callbacks to their captured runtime and make provider/handle
578-
registries safe under concurrent child creation and deterministic cleanup.
576+
Net::SSLeay verification, info, and password callbacks are bound to the runtime
577+
that registered them, including invocation from foreign native callback
578+
threads. SSL session handles are runtime-owned and reset with their runtime.
579+
Detached children no longer appear in `threads->list`, and abnormal detached
580+
termination is reported exactly once. Other native handle classes retain their
581+
documented clone, child-owned creation, or explicit rejection policy.
579582

580583
Acceptance: Net::SSLeay `61_threads-cb-crash.t` and
581584
`62_threads-ctx_new-deadlock.t` pass without watchdog, deadlock, cross-runtime
582585
handle leakage, or callback misbinding; applicable thread-emulated server paths
583586
in the wider Net::SSLeay suite retain their non-thread baseline.
584587

585-
### Phase 32 — Advanced shared values
588+
### Phase 32 — Advanced shared values (implemented supported tranche 2026-08-13)
586589

587-
Extend `threads::shared` only where identity, magic, tie callbacks, locking, and
588-
clone behavior are defined. Blessed, tied, or magical graphs remain explicit
589-
errors until their complete semantics are proven.
590+
Nested plain scalar/array/hash graphs now have atomic preflight before any node
591+
is published as shared. A rejected nested node therefore cannot leave a
592+
partially shared graph behind. Identity, mutation, recursive locking, and
593+
condition behavior are stress-tested across child threads. Blessed, tied, and
594+
other magical graphs remain explicit errors because their callback and
595+
destruction semantics are not yet a supported shared-value category.
590596

591597
Acceptance: each newly supported value category has standard-Perl-validated
592598
identity, mutation, lock/condition, clone, destruction, and stress coverage.
593599

594-
### Phase 33 — Compatibility completion, documentation, and examples
600+
### Phase 33 — Compatibility completion, documentation, and examples (implemented 2026-08-13)
595601

596602
Run the complete applicable core, Test2, Storable, and native thread matrix;
597603
update the feature matrix from raw results; and add a realistic dynamic
@@ -601,9 +607,11 @@ aggregates through `join`.
601607
Acceptance: platform threads pass all supported tests on JVM and interpreter
602608
backends; every remaining skip is an explicit platform or unsupported-feature
603609
decision; virtual mode has no semantic delta; example output is deterministic
604-
across system Perl and all supported modes.
610+
across system Perl and all supported modes. The release gate also includes the
611+
complete `timeout 3600 ./jcpan --jobs 8 -t DBIx::Class` distribution suite; every DBIx
612+
test must pass before this phase is complete.
605613

606-
### Phase 34 — Optional runtime pooling
614+
### Phase 34 — Optional runtime pooling (evaluated; deliberately disabled)
607615

608616
Consider reusable runtimes only after the fresh-runtime equivalence contract is
609617
implemented in full. Pooling is neither a Perl threads requirement nor a reason
@@ -612,6 +620,16 @@ to weaken close/snapshot isolation.
612620
Acceptance: every item in `runtime-pooling-reset-contract.md` passes and reuse
613621
has a measured benefit over a fresh snapshot.
614622

623+
The 2026-08-13 evaluation did not meet that activation threshold. The executable
624+
negative contract proves that `close()` is terminal and retains observable
625+
package, regex, and execution state. Pooling therefore remains disabled; fresh
626+
snapshot runtimes remain the correctness boundary.
627+
628+
Phase 33's release gate completed with `./jcpan --jobs 8 -t DBIx::Class`:
629+
325 files and 42,671 assertions passed. The final compatibility fix ensures
630+
non-local labeled control flow tears down every abandoned Perl frame before the
631+
target resumes, preserving scope-guard diagnostics and redirected STDERR.
632+
615633
## 6. Known Reference Material and Warnings
616634

617635
- `dev/prompts/multiplicity-v2-plan.md` documents the incremental response to
@@ -626,7 +644,7 @@ has a measured benefit over a fresh snapshot.
626644

627645
## 7. Progress Tracking
628646

629-
### Current Status: Phases 25–30 implemented; integrated validation in progress
647+
### Current Status: Phases 31–34 complete for the supported tranche
630648

631649
Hints, warnings, filters, and source maps are runtime-owned while compiler-only
632650
scratch remains protected by the global compile lock. The Phase 11 inventory is
@@ -781,15 +799,17 @@ request history.
781799
`regexp_qr_embed` direct-language gaps before asserting thread equivalence.
782800
3. Finish Phase 29 shutdown warnings and detached platform-thread process
783801
lifecycle. Continue Phase 30 resource classification beyond explicitly
784-
inherited internal pipes, then implement Phase 31 native callback/handle
785-
ownership and Phase 32 shared value categories. Preserve the green anchors:
802+
inherited internal pipes. Preserve the green anchors:
786803
`class/threads.t`, `threads-dirh.t`, Storable threads, and default Test2 IPC.
787-
4. Complete Phase 33 with the full platform-thread matrix on both backends,
788-
followed by virtual-mode parity. The new
789-
`examples/threads/dynamic_map_reduce.pl` demonstrates a small shared
790-
scheduler, isolated worker-local hashes, and deterministic join aggregation;
791-
it deliberately makes no performance claim.
792-
5. Keep virtual threads experimental until native callback diagnostics and
804+
4. Keep the complete platform-thread matrix green on both backends and retain
805+
virtual-mode parity. The new `examples/threads/dynamic_map_reduce.pl`
806+
demonstrates a small shared scheduler, isolated worker-local hashes, and
807+
deterministic join aggregation; it deliberately makes no performance claim.
808+
5. Keep the DBIx::Class regression gate green with captured output from
809+
`timeout 3600 ./jcpan --jobs 8 -t DBIx::Class`. Investigate every future
810+
failure against the merged baseline; partial distribution results are not
811+
sufficient for release.
812+
6. Keep virtual threads experimental until native callback diagnostics and
793813
repeated benchmarks justify promotion. Keep runtime pooling disabled until
794814
the separate Phase 34 reset contract proves fresh-runtime equivalence.
795815

dev/design/runtime-pooling-reset-contract.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
## Status
44

5-
Runtime pooling is deferred. `PerlRuntime.close()` is a terminal resource-release
5+
Runtime pooling was re-evaluated on 2026-08-13 and remains deliberately
6+
disabled. `PerlRuntime.close()` is a terminal resource-release
67
operation, not a reset operation, and a closed runtime deliberately rejects
78
`bind`, `initialize`, and `execute`. Reusing it would currently expose state that
89
a newly constructed runtime does not contain.
@@ -11,6 +12,11 @@ This document defines the proof required before a pool may be implemented. It
1112
does not authorize clearing state opportunistically or enabling pooling behind
1213
an experimental flag.
1314

15+
This is the Phase 34 outcome, not an untracked implementation shortcut. Runtime
16+
pooling is optional and is not required for Perl ithread correctness. The
17+
negative automated contract below passes, while every positive equivalence
18+
item remains a prerequisite for any future pooling PR.
19+
1420
## Fresh-runtime equivalence
1521

1622
A reusable runtime must expose the same observable state as a newly constructed

docs/about/changelog.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans.
1717
locks, condition variables, and compatible imports/stringification.
1818
`Config` now reports `useithreads`, `usethreads`, and `usemultiplicity` as
1919
`define`. Platform threads remain the default and virtual threads are an
20-
experimental opt-in. Thread signals, effective stack sizing, blessed/tied
21-
shared values, and some upstream core/native-callback suites remain limited;
20+
experimental opt-in. Live attached children support targeted signals,
21+
`object`/`wantarray`, and platform-thread stack sizing. Native-style callback
22+
registrations retain their owning runtime, internal pipes have an explicit
23+
inherited-handle policy, and nested plain shared graphs are validated before
24+
publication. Blessed/tied shared values and some upstream core/regex suites
25+
remain limited;
2226
see the [feature matrix](../reference/feature-matrix.md#concurrency-and-perl-threads).
2327
- CPAN/tooling: expose tested dependency scripts through `PATH`, deduplicate
2428
repeated `PERL5LIB` setup, and resolve test prerequisites against tested

docs/about/roadmap.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -288,11 +288,12 @@ threads are the default; virtual threads are experimental.
288288

289289
Remaining work:
290290

291-
- Complete the currently partial applicable core suites and Storable thread test.
292-
- Implement thread signals and the remaining `object`/`wantarray` surface.
293-
- Decide whether effective per-thread stack sizing can be exposed safely.
294-
- Validate native callback/resource behavior and virtual-thread diagnostics on
295-
the supported Java 24 baseline.
291+
- Complete the currently partial applicable core and regex suites.
292+
- Extend native-resource inheritance beyond the explicitly supported internal
293+
pipe policy only when ownership and last-close semantics are defined.
294+
- Decide which blessed, tied, or magical values can safely join the supported
295+
`threads::shared` tranche.
296+
- Keep runtime pooling disabled until the reset-equivalence contract is proven.
296297

297298
---
298299

docs/reference/feature-matrix.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -862,11 +862,11 @@ storage as their parent counterparts. Values explicitly shared through
862862

863863
| Limitation | Effect |
864864
|---|---|
865-
| Thread signals | `threads->kill` is not implemented. |
866-
| Effective stack sizing | Standard `stack_size` import syntax is accepted for source compatibility, but JVM stack sizing remains runtime-managed. |
867-
| Additional introspection | `threads->object` and `wantarray` are not implemented. |
865+
| Thread signals | `threads->kill` targets live attached children and resolves the handler inside the child runtime. Completed and detached targets are not signalable. |
866+
| Effective stack sizing | Platform-backed children honor supported `stack_size` create/import requests. Virtual threads reject nonzero stack sizes because their stacks are JVM-managed. |
867+
| Additional introspection | `threads->object` and creation-context `wantarray` are implemented; process-shutdown warning parity remains incomplete. |
868868
| Shared object classes | Blessed and tied values are rejected by the supported `share`/`shared_clone` tranche. |
869-
| Native resources and callbacks | Java I/O/native handles are not portably duplicated into child snapshots; native callback thread isolation remains suite-specific. |
869+
| Native resources and callbacks | Internal pipes have an inherited lease policy. Net::SSLeay handles are runtime-owned and stored callbacks bind their registering runtime. Ordinary files, sockets, and other native handles are still rejected rather than silently shared. |
870870
| Upstream suite coverage | Core compatibility remains partial: measured results include `op/threads.t` 29/30, `op/substr_thr.t` 368/400, and `re/stclass_threads.t` 2/6; `class/threads.t`, Storable's thread test, `threads-dirh.t`, and Test2's thread IPC acceptance test complete. The regex suite now executes its child and exposes unsupported thread-local `re 'debug'` trace parity. |
871871
| PSGI | Availability of ithreads does not make one captured PSGI application runtime concurrently callable. `Plack::Handler::Netty` advertises `psgi.multithread => \0`. |
872872

examples/threads/README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ JPERL_OPTS=-Djperl.thread.mode=virtual \
3939
./jperl examples/threads/isolated_create_join.pl
4040
```
4141

42-
Thread signals, effective per-thread stack sizing, and sharing tied or blessed
43-
values are outside the currently supported tranche. A captured PSGI runtime is
44-
also not made concurrently callable merely by enabling ithreads.
42+
Live attached children support targeted thread signals. Platform-backed
43+
ithreads accept an effective Java stack-size request; virtual mode rejects a
44+
nonzero request because virtual-thread stacks are JVM-managed. Sharing tied or
45+
blessed values remains outside the supported tranche. A captured PSGI runtime
46+
is also not made concurrently callable merely by enabling ithreads.

src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1399,6 +1399,11 @@ public void visit(BlockNode node) {
13991399
loopStack.pop();
14001400
}
14011401

1402+
// A surrounding labeled-block dispatcher may need to enter this
1403+
// block's teardown after a nested sub/eval returns a LAST marker.
1404+
// Record the first cleanup opcode before regex/local/lexical unwind.
1405+
node.setAnnotation("interpreterNonLocalExitPc", bytecode.size());
1406+
14021407
if (regexSaveReg >= 0) {
14031408
emit(Opcodes.RESTORE_REGEX_STATE);
14041409
emitReg(regexSaveReg);
@@ -6395,16 +6400,24 @@ public void visit(For3Node node) {
63956400
bodyStartPc, true);
63966401
loopStack.push(loopInfo);
63976402

6403+
int nonLocalExitPc = -1;
63986404
enterScope();
63996405
try {
64006406
if (node.body != null) {
64016407
compileNode(node.body, outerResultReg, currentCallContext);
6408+
Object cleanupPc = node.body.getAnnotation("interpreterNonLocalExitPc");
6409+
if (cleanupPc instanceof Integer pc) {
6410+
nonLocalExitPc = pc;
6411+
}
64026412
}
64036413
if (outerResultReg >= 0 && lastResultReg >= 0) {
64046414
emitAliasWithTarget(outerResultReg, lastResultReg);
64056415
}
64066416
} finally {
64076417
// Exit scope to clean up lexical variables
6418+
if (nonLocalExitPc < 0) {
6419+
nonLocalExitPc = bytecode.size();
6420+
}
64086421
exitScope(true); // safe to flush — foreach body, not subroutine
64096422
}
64106423

@@ -6426,8 +6439,10 @@ public void visit(For3Node node) {
64266439

64276440
if (node.labelName != null) {
64286441
emit(Opcodes.POP_LABELED_BLOCK);
6429-
int exitPc = bytecode.size();
6430-
patchJump(exitPcPlaceholder, exitPc);
6442+
// A marker returned by a nested sub/eval must enter at the
6443+
// lexical teardown sequence that ordinary fallthrough runs.
6444+
// Jumping past it leaks block lexicals and delays DESTROY.
6445+
patchJump(exitPcPlaceholder, nonLocalExitPc);
64316446
}
64326447

64336448
// Patch last (break) PCs to jump to local cleanup (or past the block if no locals).

src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ private static boolean containsAggregateReferenceReturn(Node node) {
3838
&& (aggregateOp.operator.equals("@") || aggregateOp.operator.equals("%"));
3939
}
4040

41-
private static void emitSubroutineExitCleanup(EmitterContext ctx) {
41+
static void emitSubroutineExitCleanup(EmitterContext ctx) {
4242
java.util.List<Integer> scalarIndices =
4343
EmitStatement.withoutCaptured(ctx, ctx.symbolTable.getMyScalarIndicesInScope(0));
4444
java.util.List<Integer> hashIndices =
@@ -248,16 +248,36 @@ static void handleNextOperator(EmitterVisitor emitterVisitor, OperatorNode node)
248248
Label label = operator.equals("next") ? loopLabels.nextLabel
249249
: operator.equals("last") ? loopLabels.lastLabel
250250
: loopLabels.redoLabel;
251-
emitLoopControlScopeCleanup(ctx, loopLabels);
251+
emitLoopControlScopeCleanup(ctx, loopLabels, operator.equals("last"));
252252
emitMortalFlushAboveMark(ctx);
253253
ctx.mv.visitJumpInsn(Opcodes.GOTO, label);
254254
}
255255

256-
private static void emitLoopControlScopeCleanup(EmitterContext ctx, LoopLabels loopLabels) {
257-
if (loopLabels.cleanupScopeIndex < 0) {
256+
private static void emitLoopControlScopeCleanup(
257+
EmitterContext ctx, LoopLabels loopLabels, boolean exitsLoop) {
258+
int cleanupScopeIndex = exitsLoop && loopLabels.lastCleanupScopeIndex >= 0
259+
? loopLabels.lastCleanupScopeIndex
260+
: loopLabels.cleanupScopeIndex;
261+
if (cleanupScopeIndex < 0) {
258262
return;
259263
}
260-
EmitStatement.emitScopeExitNullStores(ctx, loopLabels.cleanupScopeIndex, true);
264+
EmitStatement.emitLoopControlScopeExit(ctx, cleanupScopeIndex);
265+
}
266+
267+
static void emitLoopControlScopeCleanupForDispatcher(
268+
EmitterContext ctx, LoopLabels loopLabels, boolean exitsLoop) {
269+
if (loopLabels.cleanupMarkSlot >= 0) {
270+
ctx.mv.visitVarInsn(Opcodes.ILOAD, loopLabels.cleanupMarkSlot);
271+
ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC,
272+
"org/perlonjava/runtime/runtimetypes/MyVarCleanupStack",
273+
"unwindTo", "(I)V", false);
274+
}
275+
emitLoopControlScopeCleanup(ctx, loopLabels, exitsLoop);
276+
ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC,
277+
"org/perlonjava/runtime/runtimetypes/MortalList",
278+
"flush",
279+
"()V",
280+
false);
261281
}
262282

263283
private static void emitMortalFlushAboveMark(EmitterContext ctx) {

src/main/java/org/perlonjava/backend/jvm/EmitEval.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,12 +368,18 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node)
368368
mv.visitJumpInsn(Opcodes.GOTO, nextLabel);
369369

370370
mv.visitLabel(isLast);
371+
EmitControlFlow.emitLoopControlScopeCleanupForDispatcher(
372+
emitterVisitor.ctx, loopLabels, true);
371373
mv.visitJumpInsn(Opcodes.GOTO, loopLabels.lastLabel);
372374

373375
mv.visitLabel(isNext);
376+
EmitControlFlow.emitLoopControlScopeCleanupForDispatcher(
377+
emitterVisitor.ctx, loopLabels, false);
374378
mv.visitJumpInsn(Opcodes.GOTO, loopLabels.nextLabel);
375379

376380
mv.visitLabel(isRedo);
381+
EmitControlFlow.emitLoopControlScopeCleanupForDispatcher(
382+
emitterVisitor.ctx, loopLabels, false);
377383
mv.visitJumpInsn(Opcodes.GOTO, loopLabels.redoLabel);
378384

379385
mv.visitLabel(nextLabel);

0 commit comments

Comments
 (0)