From f90ce1011d99845728cf9a254ede8c79bf8c1796 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 09:23:50 +0200 Subject: [PATCH 01/13] perf: curate retained string fast paths Port the retained BMP substring offset scan and integer literal unary-minus lowering from performance research, with permanent Perl regressions and the delivery-selection manifest for the remaining closure and method groups. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/performance-delivery-selection.md | 62 +++++++++++++++++++ .../backend/jvm/EmitOperatorNode.java | 17 +++++ .../runtime/operators/PerlUtfString.java | 17 +++++ .../unit/substr_bmp_offset_fastpath.t | 15 +++++ .../unit/unary_minus_literal_fastpath.t | 10 +++ 5 files changed, 121 insertions(+) create mode 100644 dev/design/performance-delivery-selection.md create mode 100644 src/test/resources/unit/substr_bmp_offset_fastpath.t create mode 100644 src/test/resources/unit/unary_minus_literal_fastpath.t diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md new file mode 100644 index 0000000000..695652ebef --- /dev/null +++ b/dev/design/performance-delivery-selection.md @@ -0,0 +1,62 @@ +# Curated performance delivery selection + +Issue: [#1196](https://github.com/fglock/PerlOnJava/issues/1196) + +This is the integration record for the first compiler-performance delivery. +It is intentionally a small, dependency-complete selection from +`perf/concat-substr-transport`, not a merge of that research branch. + +## Sources + +| Item | Value | +| --- | --- | +| Fetched master | `5fdf4cb12` | +| Research head | `9362d12ff` | +| Merge base | `d90fa37f9` | +| Divergence at selection | master 8 commits; research 478 commits beyond merge base | +| Integration branch | `perf/curated-parity-gains` | + +The historical benchmark artifacts named below are selection evidence only. +This branch must be rebuilt and measured from its own source/JAR before a PR +can claim a current gain. + +## Logical changes + +| Logical change | Source commits and required coverage | Current status | Benefit and evidence | Integration and decision | +| --- | --- | --- | --- | --- | +| BMP substring offset scan | `b6c2ef49f3`; `substr_bmp_offset_fastpath.t` | Retained on research; absent from master | Seven exact-parent/candidate pairs favored the candidate, median 1.0569x. Artifact: documented historical pair series in the research handoff. | Include. It is self-contained in `PerlUtfString`, preserves the general decoder for surrogate/marker leads, and has no semantic follow-up. | +| Small negative integer literal lowering | `2a83a47f3`; `unary_minus_literal_fastpath.t` | Retained on research; absent from master | Seven exact-parent/candidate pairs favored the candidate, median 1.1274x. | Include. It is an emitter-only literal specialization with the generic unary path retained for non-integers and large values. | +| Direct scalar result for guarded closure-addition leaves | Runtime/call-boundary prerequisites culminating in `243fabfe1`, `41109c96e`, `2c771b73c`, and `d4e504ae9`; existing closure-addition and `direct_closure_scalar_fallback.t` coverage | Retained on research; absent from master | Seven exact-parent pairs: 1.2436x median candidate/parent. The source/JAR-matched closure portfolio median was 1.0944x Perl, 95% CI 1.0785–1.1117. Artifacts: `/tmp/perf-direct-leaf-scalar-closure-vs-perl-20260912/20260912T141322Z/portfolio.json` and its sibling analysis. | Primary integration group. Its final scalar-return step depends on the prior generated-CV/direct-entry metadata and current call-runtime ownership checks, so extract and port the complete minimal group rather than cherry-picking `d4e504ae9` alone. | +| Guarded plain-hash integer method lowering | Call-runtime prerequisites plus `7895074a0`; `direct_plain_hash_integer_method.t` | Retained on research; absent from master | Seven alternating pairs: 4.98036x median candidate/parent; complete source portfolio method 1.11657x Perl (95% CI 1.10711–1.13806). Artifact: `/tmp/direct-plain-hash-method-parent-candidate-20260912.json`; portfolio: `/tmp/perf-direct-plain-hash-method-full-20260912/20260912T204325Z/portfolio.json`. | Primary integration group. A trial cherry-pick conflicted because it relies on the research branch's evolved `RuntimeCode` call and method-argument interfaces. Preserve current-master semantics while porting a minimal complete dependency group, followed by focused standard-Perl/JVM/interpreter and current-master comparisons. | +| Lazy scalar regex whole-match materialization | `1c68f29dd`; `regex/lazy_whole_match_snapshot.t` | Retained on research; absent from master | Avoids publication allocation; the regression preserves `$&` and group-zero lifetime. No standalone current comparison is recorded in the handoff. | Defer: the source is compact, but a current focused comparison is required before inclusion because its isolated throughput benefit is not established. | +| Feature-free Joni matcher pooling | `31c364e7a`, `2b61c8b96`, `bf2a5a7d2`, later runtime-scope fix `5334a6bbf`; Joni and matcher lifetime tests | Partly superseded by later cursor experiments; absent from master | Allocation evidence exists, but multiple related cursor/pool variants were reverted. | Defer pending a dependency audit and a clean exact-parent comparison. Do not select a partial pool sequence. | +| Empty named-capture map reuse | `6f4c614bd` and prior variants | Reverted/rejected | Repeats were neutral or regressive despite allocation removal. | Reject. | +| Private native-array carrier | `09435ed53` through `3930df7d2` and associated tests | Experimental infrastructure | Does not select the scored Life recurrence. | Defer. It is not eligible for this delivery. | + +## Validation ledger + +Before a PR is opened, record here the integrated commit SHA, conflict +adaptations, system-Perl test logs, focused JVM/interpreter logs, immutable +`make` log, master/curated source and JAR identities, complete portfolio +artifacts, checksum status, confidence intervals, and intended-load metadata. + +### Integrated String prerequisites + +- System Perl: `substr_bmp_offset_fastpath.t` and + `unary_minus_literal_fastpath.t` passed; logs + `/tmp/perl-substr-bmp-offset-fastpath-20260915.log` and + `/tmp/perl-unary-minus-literal-fastpath-20260915.log`. +- JVM and interpreter focused tests passed; logs + `/tmp/perf-curated-{jvm,interp}-{substr,unary}-20260915.log`. +- Immutable full gate passed: `/tmp/make-perf-curated-string-fastpaths-20260915.log` + (`BUILD SUCCESSFUL`, exit 0). The built source began at `5fdf4cb12` plus + the uncommitted curated changes. + +## Next action + +Commit the validated String prerequisites, then audit and port the minimal +complete closure direct-scalar group and the guarded plain-hash method group. +Do not flatten either group into a single cherry-pick: current master must +retain its call-frame, caller, lexical-alias, debugger, lvalue, overflow, tie, +and overload fallbacks. Measure the resulting source/JAR against current +master before considering the deferred groups. diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java index ef30b4cd4b..40a8e6eba7 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java @@ -5,6 +5,7 @@ import org.perlonjava.frontend.analysis.EmitterVisitor; import org.perlonjava.frontend.astnode.ListNode; import org.perlonjava.frontend.astnode.Node; +import org.perlonjava.frontend.astnode.NumberNode; import org.perlonjava.frontend.astnode.OperatorNode; import org.perlonjava.runtime.perlmodule.Strict; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; @@ -76,6 +77,22 @@ public static void emitOperatorNode(EmitterVisitor emitterVisitor, OperatorNode // Unary operators case "unaryMinus" -> { + // A raw NumberNode has not been rewritten by overload::constant. + // Emit a negative integer as its cached immutable literal instead + // of dispatching through the general unary-overload path. + // Non-integer and out-of-range literals retain the generic path. + if (node.operand instanceof NumberNode numberNode) { + try { + int value = Integer.parseInt(numberNode.value.replace("_", "")); + if (value > 0) { + EmitLiteral.emitNumber(emitterVisitor.ctx, + new NumberNode(Integer.toString(-value), numberNode.tokenIndex)); + break; + } + } catch (NumberFormatException ignored) { + // Retain the generic path for non-integer and large literals. + } + } Object integerAnnotation = node.getAnnotation("useInteger"); boolean useInteger = integerAnnotation instanceof Boolean value ? value diff --git a/src/main/java/org/perlonjava/runtime/operators/PerlUtfString.java b/src/main/java/org/perlonjava/runtime/operators/PerlUtfString.java index 3bf15104f0..6d5a3b788f 100644 --- a/src/main/java/org/perlonjava/runtime/operators/PerlUtfString.java +++ b/src/main/java/org/perlonjava/runtime/operators/PerlUtfString.java @@ -200,6 +200,23 @@ public static int perlOffsetForJavaIndex(String s, int javaIndex) { } private static int scanOffsetByPerlCodePoints(String s, int startJava, int perlOffset) { + int j = startJava; + int simpleEnd = (int) Math.min((long) s.length(), (long) startJava + perlOffset); + while (j < simpleEnd) { + // All UTF-16 units below the surrogate range are exactly one Perl + // logical character. Avoid allocating a PerlStep for the common + // ASCII/BMP substring path, but hand the first possible surrogate + // or internal-marker lead back to the general decoder. + if (s.charAt(j) >= 0xD800) { + return scanOffsetByPerlCodePointsGeneral(s, j, + perlOffset - (j - startJava)); + } + j++; + } + return j; + } + + private static int scanOffsetByPerlCodePointsGeneral(String s, int startJava, int perlOffset) { int j = startJava; for (int k = 0; k < perlOffset && j < s.length(); k++) { j = readOnePerlLogical(s, j).nextJavaIndex(); diff --git a/src/test/resources/unit/substr_bmp_offset_fastpath.t b/src/test/resources/unit/substr_bmp_offset_fastpath.t new file mode 100644 index 0000000000..9e8846bbbe --- /dev/null +++ b/src/test/resources/unit/substr_bmp_offset_fastpath.t @@ -0,0 +1,15 @@ +use strict; +use warnings; +use utf8; +use Test::More; + +is(substr('abcdefghijklmnopqrstuvwxyz', -24), 'cdefghijklmnopqrstuvwxyz', + 'ASCII negative offset retains character semantics'); +is(substr("A\x{010A}B\x{0A23}C", 1, 3), "\x{010A}B\x{0A23}", + 'BMP characters each occupy one substring offset'); +is(substr("A\x{1F600}BC", 1, 1), "\x{1F600}", + 'supplementary character remains one substring offset'); +is(substr("A\x{1F600}BC", 2), 'BC', + 'offset following a supplementary character remains correct'); + +done_testing; diff --git a/src/test/resources/unit/unary_minus_literal_fastpath.t b/src/test/resources/unit/unary_minus_literal_fastpath.t new file mode 100644 index 0000000000..867ecd2f85 --- /dev/null +++ b/src/test/resources/unit/unary_minus_literal_fastpath.t @@ -0,0 +1,10 @@ +use strict; +use warnings; +use Test::More; + +is(-24, -24, 'small integer literal retains its value'); +is(substr('abcdefghijklmnopqrstuvwxyz', -24), 'cdefghijklmnopqrstuvwxyz', + 'negative literal works as a substring offset'); +is(-2_147_483_647, -2147483647, 'underscored small integer literal retains its value'); + +done_testing; From d52a3028ffa78906bd698df48b1cd6511126f0e8 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 09:33:12 +0200 Subject: [PATCH 02/13] test: establish curated closure and method safety baseline Add the retained closure and method semantic regressions, including a PadWalker capture-rebind case that any direct closure entry must preserve. Record the validation evidence and minimal-port constraints in the delivery selection manifest. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/performance-delivery-selection.md | 15 +++++++ .../unit/direct_closure_integer_addition.t | 19 +++++++++ .../unit/direct_closure_padwalker_rebind.t | 19 +++++++++ .../unit/direct_closure_scalar_fallback.t | 15 +++++++ .../unit/direct_leaf_integer_addition.t | 25 +++++++++++ .../unit/direct_plain_hash_integer_method.t | 41 +++++++++++++++++++ 6 files changed, 134 insertions(+) create mode 100644 src/test/resources/unit/direct_closure_integer_addition.t create mode 100644 src/test/resources/unit/direct_closure_padwalker_rebind.t create mode 100644 src/test/resources/unit/direct_closure_scalar_fallback.t create mode 100644 src/test/resources/unit/direct_leaf_integer_addition.t create mode 100644 src/test/resources/unit/direct_plain_hash_integer_method.t diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md index 695652ebef..fc8f406172 100644 --- a/dev/design/performance-delivery-selection.md +++ b/dev/design/performance-delivery-selection.md @@ -52,6 +52,21 @@ artifacts, checksum status, confidence intervals, and intended-load metadata. (`BUILD SUCCESSFUL`, exit 0). The built source began at `5fdf4cb12` plus the uncommitted curated changes. +### Closure/method port baseline + +- The four imported closure/method regressions pass on system Perl (17 + assertions) and on both current PerlOnJava backends. Logs: + `/tmp/prove-curated-closure-method-perl-20260915.log` and + `/tmp/perf-curated-{jvm,interpreter}-closure-method-20260915.log`. +- `direct_closure_padwalker_rebind.t` adds the required capture-rebinding + guard. It passed system Perl and both current backends; logs: + `/tmp/perl-direct-closure-padwalker-rebind-20260915.log` and + `/tmp/perf-curated-{jvm,interpreter}-direct-closure-padwalker-rebind-20260915.log`. +- Port constraint: a direct closure path must fetch the current generated + capture cells or invalidate and rebuild its cache through + `Internals.rebindCapturedVariable`. It must never continue using cells that + `PadWalker::set_closed_over` replaced. + ## Next action Commit the validated String prerequisites, then audit and port the minimal diff --git a/src/test/resources/unit/direct_closure_integer_addition.t b/src/test/resources/unit/direct_closure_integer_addition.t new file mode 100644 index 0000000000..1b0de412ae --- /dev/null +++ b/src/test/resources/unit/direct_closure_integer_addition.t @@ -0,0 +1,19 @@ +use strict; +use warnings; +use Test::More; + +my ($left, $middle, $right) = (10, 20, 30); +my $sum = sub { $left + $middle + $right }; + +is($sum->(), 60, 'captured integer sum'); +$middle = 200; +is($sum->(), 240, 'closure reads current captured cells'); + +my $temporary = $sum->(); +$temporary++; +is($sum->(), 240, 'returned rvalue does not alias a capture'); + +$left = '010'; +is($sum->(), 240, 'string capture falls back to ordinary numeric addition'); + +done_testing; diff --git a/src/test/resources/unit/direct_closure_padwalker_rebind.t b/src/test/resources/unit/direct_closure_padwalker_rebind.t new file mode 100644 index 0000000000..f1c3fde697 --- /dev/null +++ b/src/test/resources/unit/direct_closure_padwalker_rebind.t @@ -0,0 +1,19 @@ +use strict; +use warnings; +use Test::More; +use PadWalker qw(set_closed_over); + +my $left = 10; +my $right = 20; +my $sum = sub { $left + $right }; + +is($sum->(), 30, 'captured integer closure starts with original cells'); + +my $replacement = 40; +set_closed_over($sum, { '$left' => \$replacement }); +is($sum->(), 60, 'closure reads the PadWalker replacement cell'); + +$replacement = 70; +is($sum->(), 90, 'closure continues to observe the replacement cell'); + +done_testing; diff --git a/src/test/resources/unit/direct_closure_scalar_fallback.t b/src/test/resources/unit/direct_closure_scalar_fallback.t new file mode 100644 index 0000000000..2409c3f03b --- /dev/null +++ b/src/test/resources/unit/direct_closure_scalar_fallback.t @@ -0,0 +1,15 @@ +use strict; +use warnings; +use Test::More tests => 3; + +my $first = 10; +my $second = 20; +my $code = sub { $first + $second }; + +is($code->(), 30, 'captured numeric closure returns its scalar result'); + +$code = sub { 17 }; +is($code->(), 17, 'replaced code reference takes scalar fallback'); + +my @values = $code->(); +is_deeply(\@values, [17], 'replacement retains ordinary list context'); diff --git a/src/test/resources/unit/direct_leaf_integer_addition.t b/src/test/resources/unit/direct_leaf_integer_addition.t new file mode 100644 index 0000000000..260c257eda --- /dev/null +++ b/src/test/resources/unit/direct_leaf_integer_addition.t @@ -0,0 +1,25 @@ +use strict; +use warnings; +use Test::More; + +my ($a, $b, $c) = (10, 20, 30); +my $sum = sub { $a + $b + $c }; +is($sum->(), 60, 'captured integer addition returns its scalar result'); +$b = 7; +is($sum->(), 47, 'captured integer mutation is observed by the closure'); + +{ + package DirectLeafOverload; + use overload '+' => sub { 99 }, fallback => 1; +} +$a = bless {}, 'DirectLeafOverload'; +is($sum->(), 129, 'overloaded capture retains ordinary addition semantics'); + +my $observes_caller = sub { (caller(0))[3] }; +is($observes_caller->(), 'main::__ANON__', + 'caller-observing closure retains the ordinary call frame'); + +my $uses_args = sub { $_[0] }; +is($uses_args->(42), 42, 'argument-observing closure retains ordinary argument semantics'); + +done_testing; diff --git a/src/test/resources/unit/direct_plain_hash_integer_method.t b/src/test/resources/unit/direct_plain_hash_integer_method.t new file mode 100644 index 0000000000..3be65717c7 --- /dev/null +++ b/src/test/resources/unit/direct_plain_hash_integer_method.t @@ -0,0 +1,41 @@ +use strict; +use warnings; +use Test::More; + +{ + package DirectPlainHashIntegerMethod; + sub add { + my ($self, $n) = @_; + $self->{x} += $n; + $self->{y} += $n; + return $self->{x} + $self->{y}; + } +} + +my $plain = bless { x => 1, y => 2 }, 'DirectPlainHashIntegerMethod'; +is($plain->add(3), 9, 'plain native-integer method update'); +is_deeply($plain, { x => 4, y => 5 }, 'plain method retains both updated slots'); + +{ + package DirectPlainHashIntegerMethod::Tie; + sub TIEHASH { bless { values => { x => 1, y => 2 }, stores => 0 }, shift } + sub FETCH { $_[0]{values}{$_[1]} } + sub STORE { $_[0]{stores}++; $_[0]{values}{$_[1]} = $_[2] } + sub stores { $_[0]{stores} } +} + +tie my %tied, 'DirectPlainHashIntegerMethod::Tie'; +my $tied = bless \%tied, 'DirectPlainHashIntegerMethod'; +is($tied->add(2), 7, 'tied hash receiver retains ordinary method semantics'); +ok((tied(%tied))->stores >= 2, 'tied receiver performed its STORE callbacks'); + +{ + package DirectPlainHashIntegerMethod::Number; + use overload '0+' => sub { $_[0]{value} }, fallback => 1; +} + +my $overloaded = bless { x => 1, y => 2 }, 'DirectPlainHashIntegerMethod'; +my $number = bless { value => 4 }, 'DirectPlainHashIntegerMethod::Number'; +is($overloaded->add($number), 11, 'overloaded argument retains ordinary numeric dispatch'); + +done_testing; From 526ce54b954a33651f3c2a647cc61c7a12d80305 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 10:09:24 +0200 Subject: [PATCH 03/13] perf: specialize guarded captured integer closures Mark compiler-proven captured-scalar addition closures and bypass the ordinary call boundary only for scalar, zero-argument native-integer invocations. Preserve ordinary behavior for debugger, lvalue, overflow, tainted, overloaded, and PadWalker-rebound cases. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- .../backend/jvm/EmitSubroutine.java | 81 +++++++++++++++++++ .../runtime/perlmodule/Internals.java | 3 + .../runtime/runtimetypes/RuntimeCode.java | 51 ++++++++++++ .../unit/direct_closure_integer_addition.t | 9 ++- 4 files changed, 141 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index 428563ab43..c26b1eeacf 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -21,6 +21,7 @@ import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import java.util.Arrays; +import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; @@ -165,6 +166,16 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("AnonSub ctx.symbolTable.getAllVisibleVariables"); + Set directLeafCaptures = new HashSet<>(); + for (SymbolTable.SymbolEntry entry : visibleVariables.values()) { + directLeafCaptures.add(entry.name()); + } + ArrayList directLeafCaptureNames = new ArrayList<>(); + boolean directLeafIntegerAddition = !isPackageSub + && !tracksRuntimeRegexLexicals + && isDirectLeafIntegerAddition(node.block, directLeafCaptures, + directLeafCaptureNames); + // Create a new symbol table for the subroutine, but manually add only the filtered variables ScopedSymbolTable newSymbolTable = new ScopedSymbolTable(); newSymbolTable.enterScope(); @@ -768,6 +779,23 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { false); } + if (directLeafIntegerAddition) { + mv.visitLdcInsn(directLeafCaptureNames.size()); + mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String"); + for (int i = 0; i < directLeafCaptureNames.size(); i++) { + mv.visitInsn(Opcodes.DUP); + mv.visitLdcInsn(i); + mv.visitLdcInsn(directLeafCaptureNames.get(i)); + mv.visitInsn(Opcodes.AASTORE); + } + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "markDirectLeafIntegerAddition", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;[Ljava/lang/String;)" + + "Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); + } + // 6. Clean up the stack if context is VOID if (ctx.contextType == RuntimeContextType.VOID) { mv.visitInsn(Opcodes.POP); // Remove the RuntimeScalar object from the stack @@ -1083,6 +1111,25 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod ByteCodeSourceMapper.setDebugInfoLineNumber(emitterVisitor.ctx, errorSiteIndex); } + boolean directLeafCall = argCount == 0 + && emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR + && isScalarVariable; + Label directLeafFallback = directLeafCall ? new Label() : null; + Label directLeafDone = directLeafCall ? new Label() : null; + if (directLeafCall) { + mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "tryDirectLeafIntegerAddition", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); + mv.visitInsn(Opcodes.DUP); + mv.visitJumpInsn(Opcodes.IFNULL, directLeafFallback); + mv.visitJumpInsn(Opcodes.GOTO, directLeafDone); + mv.visitLabel(directLeafFallback); + mv.visitInsn(Opcodes.POP); + } + mv.visitVarInsn(Opcodes.ALOAD, codeRefSlot); mv.visitVarInsn(Opcodes.ALOAD, nameSlot); mv.visitMethodInsn( @@ -1230,6 +1277,40 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod } else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { mv.visitInsn(Opcodes.POP); } + if (directLeafCall) { + mv.visitLabel(directLeafDone); + } + } + + private static boolean isDirectLeafIntegerAddition(Node block, Set captures, + ArrayList captureNames) { + if (!(block instanceof BlockNode body) || body.elements == null + || body.elements.size() != 1 || captures.isEmpty()) return false; + Node expression = body.elements.getFirst(); + if (expression instanceof OperatorNode operator && "return".equals(operator.operator) + && operator.operand instanceof ListNode list && list.elements != null + && list.elements.size() == 1) { + expression = list.elements.getFirst(); + } + Set leaves = new HashSet<>(); + return isDirectLeafIntegerAdditionExpression(expression, captures, leaves, captureNames); + } + + private static boolean isDirectLeafIntegerAdditionExpression(Node node, Set captures, + Set leaves, + ArrayList captureNames) { + if (node instanceof OperatorNode operator && "$".equals(operator.operator) + && operator.operand instanceof IdentifierNode identifier) { + String name = "$" + identifier.name; + if (!captures.contains(name) || !leaves.add(name)) return false; + captureNames.add(name); + return true; + } + if (node instanceof BinaryOperatorNode binary && "+".equals(binary.operator)) { + return isDirectLeafIntegerAdditionExpression(binary.left, captures, leaves, captureNames) + && isDirectLeafIntegerAdditionExpression(binary.right, captures, leaves, captureNames); + } + return false; } private static int callerLineCallSiteIndex(BinaryOperatorNode node, int statementTokenIndex) { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java index c40b03bef2..147574b1b5 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java @@ -299,6 +299,9 @@ public static RuntimeList jperlCallerCv(RuntimeArray args, int ctx) { public static void rebindCapturedVariable( RuntimeCode code, String variableName, RuntimeBase replacement) { + if (code.closedOverVariables != null) { + code.closedOverVariables.put(variableName, replacement); + } if (code instanceof InterpretedCode interpreted) { Integer register = interpreted.variableRegistry.get(variableName); int capturedIndex = register == null ? -1 : register - 3; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index af2eaba50d..0bd42ec50c 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -36,6 +36,7 @@ import java.lang.invoke.MethodType; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; +import java.math.BigInteger; import java.nio.charset.StandardCharsets; import java.util.*; import java.util.function.Supplier; @@ -1552,6 +1553,11 @@ void restoreClonedSelfReference(RuntimeScalar selfRef) { /** Live lexical containers keyed by their Perl pad names for PadWalker. */ public Map closedOverVariables; + /** Capability marker for a guarded captured-integer addition closure. */ + public boolean directLeafIntegerAddition; + /** Captured pad names in the source addition order. */ + private String[] directLeafIntegerAdditionCaptureNames; + /** Lexicals declared by this CV, exposed by PadWalker::peek_sub. */ public Set lexicalVariableNames; @@ -1581,6 +1587,21 @@ public static RuntimeScalar markRuntimeRegexLexicals(RuntimeScalar codeRef) { return codeRef; } + /** Mark the narrow generated closure shape accepted by the direct scalar entry. */ + public static RuntimeScalar markDirectLeafIntegerAddition(RuntimeScalar codeRef, + String[] captureNames) { + if (codeRef != null && codeRef.value instanceof RuntimeCode code + && !(code instanceof InterpretedCode) && captureNames != null + && captureNames.length != 0 && code.closedOverVariables != null) { + for (String captureName : captureNames) { + if (!(code.closedOverVariables.get(captureName) instanceof RuntimeScalar)) return codeRef; + } + code.directLeafIntegerAdditionCaptureNames = captureNames.clone(); + code.directLeafIntegerAddition = true; + } + return codeRef; + } + /** Devel::LexAlias replacements applied when a lexical is instantiated. */ public Map lexicalAliases; @@ -5866,6 +5887,36 @@ private static String getWarningBitsForCode( return null; } + /** + * Return the fresh scalar result for a compiler-proven captured-integer + * addition closure, or {@code null} when the ordinary Perl call boundary + * is required. The capture map is consulted on every call so PadWalker + * rebinding cannot leave this path with stale cells. + */ + public static RuntimeScalar tryDirectLeafIntegerAddition(RuntimeScalar runtimeScalar) { + if (runtimeScalar == null || runtimeScalar.type != RuntimeScalarType.CODE + || !(runtimeScalar.value instanceof RuntimeCode code) + || !code.directLeafIntegerAddition || code.subroutine == null + || DebugState.isDebugMode() || isLvalueCode(code) + || code.directLeafIntegerAdditionCaptureNames == null + || code.closedOverVariables == null) return null; + RuntimeScalar[] captures = new RuntimeScalar[code.directLeafIntegerAdditionCaptureNames.length]; + for (int i = 0; i < captures.length; i++) { + RuntimeBase value = code.closedOverVariables.get(code.directLeafIntegerAdditionCaptureNames[i]); + if (!(value instanceof RuntimeScalar scalar) + || scalar.type != INTEGER || scalar.value instanceof BigInteger + || scalar.tainted || scalar.blessId != 0) return null; + captures[i] = scalar; + } + try { + long sum = captures[0].getLong(); + for (int i = 1; i < captures.length; i++) sum = Math.addExact(sum, captures[i].getLong()); + return new RuntimeScalar(sum); + } catch (ArithmeticException overflow) { + return null; + } + } + // Method to apply (execute) a subroutine reference using native array for parameters public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineName, RuntimeBase[] args, int callContext) { runtimeScalar = resolveDirectCallTarget(runtimeScalar, subroutineName); diff --git a/src/test/resources/unit/direct_closure_integer_addition.t b/src/test/resources/unit/direct_closure_integer_addition.t index 1b0de412ae..ad510f7c00 100644 --- a/src/test/resources/unit/direct_closure_integer_addition.t +++ b/src/test/resources/unit/direct_closure_integer_addition.t @@ -5,15 +5,18 @@ use Test::More; my ($left, $middle, $right) = (10, 20, 30); my $sum = sub { $left + $middle + $right }; -is($sum->(), 60, 'captured integer sum'); +my $first_result = $sum->(); +is($first_result, 60, 'captured integer sum in scalar context'); $middle = 200; -is($sum->(), 240, 'closure reads current captured cells'); +my $mutated_result = $sum->(); +is($mutated_result, 240, 'closure reads current captured cells'); my $temporary = $sum->(); $temporary++; is($sum->(), 240, 'returned rvalue does not alias a capture'); $left = '010'; -is($sum->(), 240, 'string capture falls back to ordinary numeric addition'); +my $fallback_result = $sum->(); +is($fallback_result, 240, 'string capture falls back to ordinary numeric addition'); done_testing; From 675eff5e1eca74f9a916e4c86760e7af8acbfa5a Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 10:19:16 +0200 Subject: [PATCH 04/13] docs: defer benchmark-shaped method specialization Record the integrated closure fast path and defer the two-slot method recognizer in favor of investigating costs shared by ordinary methods. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/performance-delivery-selection.md | 43 ++++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md index fc8f406172..67143c602f 100644 --- a/dev/design/performance-delivery-selection.md +++ b/dev/design/performance-delivery-selection.md @@ -26,8 +26,8 @@ can claim a current gain. | --- | --- | --- | --- | --- | | BMP substring offset scan | `b6c2ef49f3`; `substr_bmp_offset_fastpath.t` | Retained on research; absent from master | Seven exact-parent/candidate pairs favored the candidate, median 1.0569x. Artifact: documented historical pair series in the research handoff. | Include. It is self-contained in `PerlUtfString`, preserves the general decoder for surrogate/marker leads, and has no semantic follow-up. | | Small negative integer literal lowering | `2a83a47f3`; `unary_minus_literal_fastpath.t` | Retained on research; absent from master | Seven exact-parent/candidate pairs favored the candidate, median 1.1274x. | Include. It is an emitter-only literal specialization with the generic unary path retained for non-integers and large values. | -| Direct scalar result for guarded closure-addition leaves | Runtime/call-boundary prerequisites culminating in `243fabfe1`, `41109c96e`, `2c771b73c`, and `d4e504ae9`; existing closure-addition and `direct_closure_scalar_fallback.t` coverage | Retained on research; absent from master | Seven exact-parent pairs: 1.2436x median candidate/parent. The source/JAR-matched closure portfolio median was 1.0944x Perl, 95% CI 1.0785–1.1117. Artifacts: `/tmp/perf-direct-leaf-scalar-closure-vs-perl-20260912/20260912T141322Z/portfolio.json` and its sibling analysis. | Primary integration group. Its final scalar-return step depends on the prior generated-CV/direct-entry metadata and current call-runtime ownership checks, so extract and port the complete minimal group rather than cherry-picking `d4e504ae9` alone. | -| Guarded plain-hash integer method lowering | Call-runtime prerequisites plus `7895074a0`; `direct_plain_hash_integer_method.t` | Retained on research; absent from master | Seven alternating pairs: 4.98036x median candidate/parent; complete source portfolio method 1.11657x Perl (95% CI 1.10711–1.13806). Artifact: `/tmp/direct-plain-hash-method-parent-candidate-20260912.json`; portfolio: `/tmp/perf-direct-plain-hash-method-full-20260912/20260912T204325Z/portfolio.json`. | Primary integration group. A trial cherry-pick conflicted because it relies on the research branch's evolved `RuntimeCode` call and method-argument interfaces. Preserve current-master semantics while porting a minimal complete dependency group, followed by focused standard-Perl/JVM/interpreter and current-master comparisons. | +| Direct scalar result for guarded closure-addition leaves | Runtime/call-boundary prerequisites culminating in `243fabfe1`, `41109c96e`, `2c771b73c`, and `d4e504ae9`; `direct_closure_integer_addition.t`, `direct_closure_scalar_fallback.t`, and `direct_closure_padwalker_rebind.t` coverage | Integrated as `a8e4a8fc1` | Seven exact-parent pairs: 1.2436x median candidate/parent. The source/JAR-matched closure portfolio median was 1.0944x Perl, 95% CI 1.0785–1.1117. Artifacts: `/tmp/perf-direct-leaf-scalar-closure-vs-perl-20260912/20260912T141322Z/portfolio.json` and its sibling analysis. | Included as a bounded scalar-context fast path. It dynamically reads capture cells, falls back for debugger/lvalue/overflow/taint/overload cases, and updates the capture map during PadWalker rebinding. Current-source benchmark evidence is still required before a PR claim. | +| Guarded plain-hash integer method lowering | Call-runtime prerequisites plus `7895074a0`; `direct_plain_hash_integer_method.t` | Retained on research; absent from master | Seven alternating pairs: 4.98036x median candidate/parent; complete source portfolio method 1.11657x Perl (95% CI 1.10711–1.13806). Artifact: `/tmp/direct-plain-hash-method-parent-candidate-20260912.json`; portfolio: `/tmp/perf-direct-plain-hash-method-full-20260912/20260912T204325Z/portfolio.json`. | Defer. It recognizes a benchmark-shaped two-slot method body and adds disproportionate compiler/runtime complexity. Retain the semantic test as a baseline; investigate broadly shared method costs (argument frames, cached dispatch, scalar-result handling, and hash access) instead. | | Lazy scalar regex whole-match materialization | `1c68f29dd`; `regex/lazy_whole_match_snapshot.t` | Retained on research; absent from master | Avoids publication allocation; the regression preserves `$&` and group-zero lifetime. No standalone current comparison is recorded in the handoff. | Defer: the source is compact, but a current focused comparison is required before inclusion because its isolated throughput benefit is not established. | | Feature-free Joni matcher pooling | `31c364e7a`, `2b61c8b96`, `bf2a5a7d2`, later runtime-scope fix `5334a6bbf`; Joni and matcher lifetime tests | Partly superseded by later cursor experiments; absent from master | Allocation evidence exists, but multiple related cursor/pool variants were reverted. | Defer pending a dependency audit and a clean exact-parent comparison. Do not select a partial pool sequence. | | Empty named-capture map reuse | `6f4c614bd` and prior variants | Reverted/rejected | Repeats were neutral or regressive despite allocation removal. | Reject. | @@ -67,11 +67,38 @@ artifacts, checksum status, confidence intervals, and intended-load metadata. `Internals.rebindCapturedVariable`. It must never continue using cells that `PadWalker::set_closed_over` replaced. +### Integrated closure direct-scalar path + +- Integrated commit: `a8e4a8fc1`. +- The compiler marks only a single-expression captured-scalar addition closure. + The call site uses the direct result only for a scalar, zero-argument lexical + call. `RuntimeCode` otherwise keeps the ordinary call boundary, including in + debugger and lvalue modes and for overflow, taint, object/overload, or + PadWalker-rebound captures. +- System Perl and focused JVM/interpreter coverage passed before integration: + `/tmp/perf-direct-closure-stock-perl-20260915.log`, + `/tmp/perf-direct-closure-jvm-20260915.log`, and + `/tmp/perf-direct-closure-interpreter-20260915.log`. +- Bytecode selection was verified for a scalar assignment call; log: + `/tmp/perf-direct-closure-disassemble-scalar-20260915.log`. +- Immutable full gates passed: + `/tmp/make-perf-curated-direct-closure-final-20260915.log` and + `/tmp/make-perf-curated-direct-closure-debug-guard-20260915.log`. + +### Deferred method specialization + +- The attempted current-master port of the two-slot plain-hash method recognizer + was deliberately removed without commit. Its full validation log is retained + at `/tmp/make-perf-curated-direct-method-final-20260915.log`; it is not + evidence for shipping that specialization. +- The existing method regression remains as semantic coverage. Future method + work must target a broadly shared, measured cost and show a meaningful + repeatable whole-workload result before expanding compiler recognition. + ## Next action -Commit the validated String prerequisites, then audit and port the minimal -complete closure direct-scalar group and the guarded plain-hash method group. -Do not flatten either group into a single cherry-pick: current master must -retain its call-frame, caller, lexical-alias, debugger, lvalue, overflow, tie, -and overload fallbacks. Measure the resulting source/JAR against current -master before considering the deferred groups. +Measure the integrated String and closure changes from their current source/JAR +against current master before making any PR claim. Keep the method recognizer +deferred; next method work should begin with profiling and a design for a cost +shared across ordinary methods, such as argument-frame allocation, cached +dispatch, scalar-result handling, or hash access. From cfa70514a9a84e3ed4ae67e55157d147ef6d7f75 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 10:43:01 +0200 Subject: [PATCH 05/13] docs: qualify non-authoritative performance pilot Record the source-matched pilot, matching tooling identities, and unstable host conditions without treating its directional throughput as delivery evidence. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/performance-delivery-selection.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md index 67143c602f..d896132fea 100644 --- a/dev/design/performance-delivery-selection.md +++ b/dev/design/performance-delivery-selection.md @@ -95,6 +95,25 @@ artifacts, checksum status, confidence intervals, and intended-load metadata. work must target a broadly shared, measured cost and show a meaningful repeatable whole-workload result before expanding compiler recognition. +### Non-authoritative source-matched pilot + +- Master (`5fdf4cb12`) and curated (`5048e78ef`) were each rebuilt and passed + immutable full gates: `/tmp/make-perf-curated-baseline-20260915.log` and + `/tmp/make-perf-curated-final-measurement-20260915.log`. +- Both measurement worktrees received identical untracked tooling-only copies + of the runner, worker, and analyzer from `perf/benchmark-authority`; SHA-256 + values were recorded before execution. This keeps compiler source distinct + from the separate benchmark-tooling PR. +- A one-pair, five-warmup, three-window closure/method/string pilot completed + with matching semantic checksums. Artifacts: + `/tmp/perf-curated-baseline-pilot-20260915/20260915T083846Z/portfolio.json` + and + `/tmp/perf-curated-candidate-pilot-20260915/20260915T084028Z/portfolio.json`. + It is explicitly non-authoritative: `protocol_compliant` and `conclusive` + are both false, warmups did not stabilize, and host load changed from roughly + 22 to 17 while an unrelated test build drained. Do not use its directional + throughput values to retain, reject, or advertise an optimization. + ## Next action Measure the integrated String and closure changes from their current source/JAR From 3253cf91c21e68b9cbea86f5ae1427566cb62afe Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 10:45:14 +0200 Subject: [PATCH 06/13] docs: note curated compiler fast paths Add the required work-in-progress changelog entry for the selected guarded compiler performance improvements without making an unmeasured claim. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- docs/about/changelog.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/about/changelog.md b/docs/about/changelog.md index 3c1ab8168f..33dcea2255 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -8,6 +8,9 @@ priorities and future plans. - Fix direct execution of scripts generated with a PerlOnJava `$^X` shebang and update the Java-backed `Compress::Raw::{Bzip2,Zlib}` providers to the audited 2.224 compatibility level. +- Add guarded compiler fast paths for BMP substring offsets, small negative + integer literals, and scalar captured-integer closure additions. + - Restore Perl-compatible integer increment/decrement semantics, imprecision warnings, numeric overload fallback, and postfix-reference lifetime handling. From ad94b1c42dfed889713a2b4eb65f7b424978fb38 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 13:52:03 +0200 Subject: [PATCH 07/13] docs: record bracketed performance qualification Record the current-source control run, checksum validation, and the non-authoritative interpretation of the curated portfolio. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/performance-delivery-selection.md | 39 +++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md index d896132fea..b7c4f945e3 100644 --- a/dev/design/performance-delivery-selection.md +++ b/dev/design/performance-delivery-selection.md @@ -114,10 +114,39 @@ artifacts, checksum status, confidence intervals, and intended-load metadata. 22 to 17 while an unrelated test build drained. Do not use its directional throughput values to retain, reject, or advertise an optimization. +### Bracketed current-source portfolio + +- The immutable current-master gate at `5fdf4cb12` and the curated gate at + `54e2005b3` both passed. The candidate contains all retained String work and + the guarded closure path; it does **not** contain the method recognizer. +- Full seven-pair portfolio artifacts, in execution order, are: + `/tmp/perf-curated-baseline-full-20260915/20260915T085537Z/portfolio.json`, + `/tmp/perf-curated-candidate-full-20260915/20260915T095050Z/portfolio.json`, + and + `/tmp/perf-curated-baseline-post-20260915/20260915T103838Z/portfolio.json`. + The corresponding analyzer reports are in each artifact's parent directory + as `report.json`; the post-control analysis log is + `/tmp/perf-curated-baseline-post-20260915/analysis.log`. +- All runs used identical copied benchmark tooling and recorded 49 paired + samples with zero Perl/PerlOnJava semantic-checksum mismatches. They are + protocol-compliant but `conclusive: false`, so every analyzer report marks + the evidence non-authoritative. Host load and warm-up stability prevent an + acceptance or portfolio-wide throughput claim. +- The bracketing controls make the interpretation explicit. Their closure + geometric ratios to system Perl were 0.1267 and 0.1364, versus 0.6022 for + the candidate. Comparing each candidate pair to the geometric mean of its + bracketing master pairs gives a 4.631x median directional closure result. + Other workloads moved together by 1.032x--1.331x, including method at + 1.199x; those common shifts are host variation, not a result to attribute to + this branch. This supports retaining the closure candidate for further + controlled measurement, but does not make the portfolio authoritative. + ## Next action -Measure the integrated String and closure changes from their current source/JAR -against current master before making any PR claim. Keep the method recognizer -deferred; next method work should begin with profiling and a design for a cost -shared across ordinary methods, such as argument-frame allocation, cached -dispatch, scalar-result handling, or hash access. +The retained String and closure changes may be reviewed as a semantic-safe, +curated delivery, with the above measurement qualification and no advertised +whole-portfolio gain. Before any performance claim, repeat the source-matched +portfolio on a stable host until the protocol is conclusive. Keep the method +recognizer deferred: next method work should begin with profiling and a design +for a cost shared across ordinary methods, such as argument-frame allocation, +cached dispatch, scalar-result handling, or hash access. From 21081cf4b230f43ae9a7121e52f92f5bd7c7010a Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 14:03:11 +0200 Subject: [PATCH 08/13] docs: record rebased performance validation Distinguish the passing current-target correctness gate from the earlier non-authoritative portfolio artifacts. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/performance-delivery-selection.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md index b7c4f945e3..767aeb51ef 100644 --- a/dev/design/performance-delivery-selection.md +++ b/dev/design/performance-delivery-selection.md @@ -140,6 +140,11 @@ artifacts, checksum status, confidence intervals, and intended-load metadata. 1.199x; those common shifts are host variation, not a result to attribute to this branch. This supports retaining the closure candidate for further controlled measurement, but does not make the portfolio authoritative. +- Master advanced after these artifacts were collected. The curated series was + rebased onto `483a9b9ed` as `d5d956801`; its immutable full gate passed in + `/tmp/make-perf-curated-rebased-20260915.log` (`BUILD SUCCESSFUL`, exit 0). + This validates compatibility with the current PR target, not performance: + no throughput claim transfers from the pre-rebase artifacts. ## Next action From b736b580e877c63eaed4607edc9d070928651227 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 16:10:32 +0200 Subject: [PATCH 09/13] docs: refresh curated performance rebase evidence Record the current master base and passing immutable validation gate without transferring a throughput claim from the earlier inconclusive portfolio. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- dev/design/performance-delivery-selection.md | 27 ++++++++++---------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/dev/design/performance-delivery-selection.md b/dev/design/performance-delivery-selection.md index 767aeb51ef..e6b4c3b423 100644 --- a/dev/design/performance-delivery-selection.md +++ b/dev/design/performance-delivery-selection.md @@ -10,10 +10,10 @@ It is intentionally a small, dependency-complete selection from | Item | Value | | --- | --- | -| Fetched master | `5fdf4cb12` | +| Fetched master | `300954833` | | Research head | `9362d12ff` | | Merge base | `d90fa37f9` | -| Divergence at selection | master 8 commits; research 478 commits beyond merge base | +| Divergence at revalidation | master 60 commits; research 478 commits beyond merge base | | Integration branch | `perf/curated-parity-gains` | The historical benchmark artifacts named below are selection evidence only. @@ -141,17 +141,18 @@ artifacts, checksum status, confidence intervals, and intended-load metadata. this branch. This supports retaining the closure candidate for further controlled measurement, but does not make the portfolio authoritative. - Master advanced after these artifacts were collected. The curated series was - rebased onto `483a9b9ed` as `d5d956801`; its immutable full gate passed in - `/tmp/make-perf-curated-rebased-20260915.log` (`BUILD SUCCESSFUL`, exit 0). - This validates compatibility with the current PR target, not performance: - no throughput claim transfers from the pre-rebase artifacts. + rebased onto `300954833` as `21081cf4b`; its immutable full gate passed in + `/tmp/make-perf-curated-rebase-300954833-20260915.log` (`BUILD SUCCESSFUL`, + exit 0). This validates compatibility with the current PR target, not + performance: no throughput claim transfers from the pre-rebase artifacts. ## Next action -The retained String and closure changes may be reviewed as a semantic-safe, -curated delivery, with the above measurement qualification and no advertised -whole-portfolio gain. Before any performance claim, repeat the source-matched -portfolio on a stable host until the protocol is conclusive. Keep the method -recognizer deferred: next method work should begin with profiling and a design -for a cost shared across ordinary methods, such as argument-frame allocation, -cached dispatch, scalar-result handling, or hash access. +The retained String and closure changes are ready for review as a semantic-safe, +curated delivery at `21081cf4b`, with the above measurement qualification and +no advertised whole-portfolio gain. Before any performance claim, repeat the +source-matched portfolio on a stable host until the protocol is conclusive. +Keep the method recognizer deferred: next method work should begin with +profiling and a design for a cost shared across ordinary methods, such as +argument-frame allocation, cached dispatch, scalar-result handling, or hash +access. From c05cf7100391948c427201f64edd946b29ea1236 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 10 Sep 2026 06:51:00 +0200 Subject: [PATCH 10/13] perf: recycle scalar return list wrappers Recycle tagged one-scalar return lists only after direct JVM call sites extract their scalar result. Preserve ordinary list-context and multi-value result lifetime, add return-context regression coverage, and record bounded closure JFR allocation evidence. Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- .../backend/jvm/EmitSubroutine.java | 14 ++++---- .../runtimetypes/ExecutionRuntimeState.java | 2 ++ .../runtime/runtimetypes/RuntimeList.java | 36 +++++++++++++++++++ .../runtime/runtimetypes/RuntimeScalar.java | 2 +- .../unit/scalar_return_list_recycling.t | 18 ++++++++++ 5 files changed, 64 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/unit/scalar_return_list_recycling.t diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index c26b1eeacf..8488e779fd 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -992,9 +992,9 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR || emitterVisitor.ctx.contextType == RuntimeContextType.LVALUE) { - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalar", - "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalarAndRecycle", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { mv.visitInsn(Opcodes.POP); } @@ -1025,9 +1025,9 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR || emitterVisitor.ctx.contextType == RuntimeContextType.LVALUE) { - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, - "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalar", - "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalarAndRecycle", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { mv.visitInsn(Opcodes.POP); } @@ -1273,7 +1273,7 @@ static void handleApplyOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR || emitterVisitor.ctx.contextType == RuntimeContextType.LVALUE) { // Transform the value in the stack to RuntimeScalar - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalar", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalarAndRecycle", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { mv.visitInsn(Opcodes.POP); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java index 96c002994b..563db480b4 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ExecutionRuntimeState.java @@ -55,6 +55,8 @@ public final class ExecutionRuntimeState { public final Deque activeRegexCallbackPackages = new ArrayDeque<>(); public final Deque activeLexicalFrames = new ArrayDeque<>(); public final Deque> pristineArgsStack = new ArrayDeque<>(); + /** Reusable one-scalar return lists, populated only after scalar extraction. */ + final Deque availableScalarResultLists = new ArrayDeque<>(); final IdentityHashMap deferredArgumentAggregateCleanup = new IdentityHashMap<>(); public final Deque hasArgsStack = new ArrayDeque<>(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index f1c3b4619e..da17e13337 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -12,6 +12,10 @@ public class RuntimeList extends RuntimeBase { // List to hold the elements of the list. public List elements; + // Set only on lists acquired for RuntimeScalar.getList(). Such a list can + // be returned to its runtime-local pool once a JVM call site extracts its + // scalar value and drops the list reference. + private boolean recyclableScalarResult; // Constructor public RuntimeList() { @@ -42,6 +46,38 @@ public RuntimeList(RuntimeScalar value) { this.elements.add(value); } + /** Acquire a one-scalar result list without changing ordinary list semantics. */ + static RuntimeList acquireScalarResult(RuntimeScalar value) { + PerlRuntime runtime = PerlRuntime.currentOrNull(); + if (runtime == null) return new RuntimeList(value); + RuntimeList result = runtime.executionState().availableScalarResultLists.pollFirst(); + if (result == null) { + result = new RuntimeList(value); + result.recyclableScalarResult = true; + return result; + } + result.elements.add(value); + result.recyclableScalarResult = true; + return result; + } + + /** + * Extract a scalar result at a JVM call site and recycle only the private + * one-scalar wrapper allocated by RuntimeScalar.getList(). + */ + public static RuntimeScalar scalarAndRecycle(RuntimeList result) { + RuntimeScalar scalar = result.scalar(); + if (result.recyclableScalarResult && result.elements.size() == 1) { + result.elements.clear(); + result.recyclableScalarResult = false; + PerlRuntime runtime = PerlRuntime.currentOrNull(); + if (runtime != null) { + runtime.executionState().availableScalarResultLists.addFirst(result); + } + } + return scalar; + } + /** * Constructs a RuntimeList from another RuntimeList. * Creates a shallow copy of the elements list to prevent mutation of the original. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 01f3575118..85d1e95041 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -1428,7 +1428,7 @@ public RuntimeArray setArrayOfAlias(RuntimeArray arr) { // Get the list value of the Scalar public RuntimeList getList() { - return new RuntimeList(this); + return RuntimeList.acquireScalarResult(this); } // Get the scalar value of the Scalar diff --git a/src/test/resources/unit/scalar_return_list_recycling.t b/src/test/resources/unit/scalar_return_list_recycling.t new file mode 100644 index 0000000000..55f708b96e --- /dev/null +++ b/src/test/resources/unit/scalar_return_list_recycling.t @@ -0,0 +1,18 @@ +use strict; +use warnings; +use Test::More; + +my $value = 10; +my $scalar = sub { ++$value }; + +is($scalar->(), 11, 'scalar-context closure return has its scalar value'); +is($scalar->(), 12, 'a later scalar return does not retain prior result state'); + +my @list = $scalar->(); +is_deeply(\@list, [13], 'list-context caller receives the scalar return as a list'); + +my $multiple = sub { return 1, 2, 3 }; +is($multiple->(), 3, 'scalar context still collapses a multi-value return'); +is_deeply([$multiple->()], [1, 2, 3], 'list context retains every returned value'); + +done_testing; From 25ff620ede2dba85be892ae1ff338e147fef68ce Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 10 Sep 2026 19:00:13 +0200 Subject: [PATCH 11/13] perf: recycle scalar results in compound calls Recycle the private one-scalar RuntimeList wrapper when compound assignment scalarizes a subroutine or method result, without changing ordinary list identity or call-frame ownership. Add a cross-backend regression and record the allocation boundary and benchmark evidence in the performance handoff. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- .../runtime/runtimetypes/RuntimeList.java | 6 +++++- .../scalar_sub_call_compound_assignment.t | 21 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/test/resources/unit/scalar_sub_call_compound_assignment.t diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index da17e13337..9a97591b94 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -160,7 +160,11 @@ public void addToArray(RuntimeArray array) { * @return The scalar with the list's scalar value set. */ public RuntimeScalar addToScalar(RuntimeScalar scalar) { - return scalar.set(this.scalar()); + // Runtime-context subroutine calls are scalarized through addToScalar + // by compound operators. Recycle only the private one-scalar wrapper + // produced by RuntimeScalar.getList(); ordinary lists retain their + // normal identity and contents. + return scalar.set(scalarAndRecycle(this)); } /** diff --git a/src/test/resources/unit/scalar_sub_call_compound_assignment.t b/src/test/resources/unit/scalar_sub_call_compound_assignment.t new file mode 100644 index 0000000000..5b2c9fde48 --- /dev/null +++ b/src/test/resources/unit/scalar_sub_call_compound_assignment.t @@ -0,0 +1,21 @@ +use strict; +use warnings; +use Test::More tests => 3; + +sub increment_by { + my ($value) = @_; + return $value; +} + +my $total = 0; +$total += increment_by(2) for 1 .. 100; +is($total, 200, 'compound assignment scalarizes repeated subroutine results'); + +my $method = bless {}, 'ScalarCallResult'; +sub ScalarCallResult::value { + return 3; +} +$total += $method->value for 1 .. 100; +is($total, 500, 'compound assignment scalarizes repeated method results'); + +is(increment_by(0), 0, 'scalar subroutine result preserves false values'); From 2b854f62ca0e4396e414a6f85ac2155298ca0452 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 10 Sep 2026 19:30:35 +0200 Subject: [PATCH 12/13] perf: recycle direct scalar call results Route JVM coderef and method scalar-result conversions through the existing private RuntimeList recycler. Add opt-in lifecycle counters that prove the method hot path now recycles its scalar return wrappers, and record the allocation evidence and measurement limits in the performance handoff. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- .../perlonjava/backend/jvm/Dereference.java | 9 ++- .../perlonjava/backend/jvm/EmitVariable.java | 8 ++- .../runtime/runtimetypes/RuntimeList.java | 4 ++ .../runtimetypes/ScalarResultDiagnostics.java | 69 +++++++++++++++++++ 4 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/main/java/org/perlonjava/runtime/runtimetypes/ScalarResultDiagnostics.java diff --git a/src/main/java/org/perlonjava/backend/jvm/Dereference.java b/src/main/java/org/perlonjava/backend/jvm/Dereference.java index 1a7bc09ba7..42fd69046a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Dereference.java +++ b/src/main/java/org/perlonjava/backend/jvm/Dereference.java @@ -1224,8 +1224,13 @@ && firstMethodArgumentIsLiteralSub(callNode) } if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR || emitterVisitor.ctx.contextType == RuntimeContextType.LVALUE) { - // Transform the value in the stack to RuntimeScalar - emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalar", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + // Method dispatch produces a RuntimeList. Once the caller has + // selected scalar/lvalue context, recycle only a private + // one-scalar result wrapper; normal lists and markers are + // unchanged. + emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalarAndRecycle", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { // Remove the value from the stack emitterVisitor.ctx.mv.visitInsn(Opcodes.POP); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index 766589588e..f707bfd8c9 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -759,8 +759,12 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n // VOID context: consume the stack mv.visitInsn(Opcodes.POP); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { - // SCALAR context: convert RuntimeList to RuntimeScalar - mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalar", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + // A call result consumed as a scalar can return its private + // one-scalar wrapper to the runtime-local pool. Ordinary + // lists and markers retain scalar() behavior. + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeList", "scalarAndRecycle", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); } // LIST context: RuntimeList is already correct, no conversion needed diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index 9a97591b94..7e48ec0242 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -52,10 +52,12 @@ static RuntimeList acquireScalarResult(RuntimeScalar value) { if (runtime == null) return new RuntimeList(value); RuntimeList result = runtime.executionState().availableScalarResultLists.pollFirst(); if (result == null) { + ScalarResultDiagnostics.acquired(false); result = new RuntimeList(value); result.recyclableScalarResult = true; return result; } + ScalarResultDiagnostics.acquired(true); result.elements.add(value); result.recyclableScalarResult = true; return result; @@ -67,12 +69,14 @@ static RuntimeList acquireScalarResult(RuntimeScalar value) { */ public static RuntimeScalar scalarAndRecycle(RuntimeList result) { RuntimeScalar scalar = result.scalar(); + ScalarResultDiagnostics.scalarExtracted(result.recyclableScalarResult, result.elements.size()); if (result.recyclableScalarResult && result.elements.size() == 1) { result.elements.clear(); result.recyclableScalarResult = false; PerlRuntime runtime = PerlRuntime.currentOrNull(); if (runtime != null) { runtime.executionState().availableScalarResultLists.addFirst(result); + ScalarResultDiagnostics.recycled(); } } return scalar; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarResultDiagnostics.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarResultDiagnostics.java new file mode 100644 index 0000000000..5c2dee4776 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarResultDiagnostics.java @@ -0,0 +1,69 @@ +package org.perlonjava.runtime.runtimetypes; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.concurrent.atomic.LongAdder; + +/** + * Opt-in lifecycle counters for private one-scalar {@link RuntimeList} results. + * + *

The counters identify whether scalar-context callers actually return the + * wrappers produced by {@link RuntimeScalar#getList()} to the runtime-local + * pool. They deliberately collect no timing or allocation data and are absent + * from ordinary execution unless both the enable and output properties are + * supplied.

+ */ +final class ScalarResultDiagnostics { + static final boolean ENABLED = Boolean.getBoolean("perlonjava.scalarResultDiagnostics"); + private static final String OUTPUT = System.getProperty("perlonjava.scalarResultDiagnosticsOutput"); + + private static final LongAdder ACQUIRE_POOL_HIT = new LongAdder(); + private static final LongAdder ACQUIRE_POOL_MISS = new LongAdder(); + private static final LongAdder SCALAR_EXTRACTION = new LongAdder(); + private static final LongAdder RECYCLED = new LongAdder(); + private static final LongAdder REJECTED_ORDINARY_LIST = new LongAdder(); + private static final LongAdder REJECTED_MULTI_ELEMENT = new LongAdder(); + + static { + if (ENABLED && OUTPUT != null && !OUTPUT.isBlank()) { + Runtime.getRuntime().addShutdownHook(new Thread(ScalarResultDiagnostics::writeReport, + "perlonjava-scalar-result-diagnostics")); + } + } + + private ScalarResultDiagnostics() { } + + static void acquired(boolean reused) { + if (!ENABLED) return; + (reused ? ACQUIRE_POOL_HIT : ACQUIRE_POOL_MISS).increment(); + } + + static void scalarExtracted(boolean recyclable, int size) { + if (!ENABLED) return; + SCALAR_EXTRACTION.increment(); + if (!recyclable) REJECTED_ORDINARY_LIST.increment(); + else if (size != 1) REJECTED_MULTI_ELEMENT.increment(); + } + + static void recycled() { + if (ENABLED) RECYCLED.increment(); + } + + private static void writeReport() { + String json = "{\n" + + " \"kind\": \"perlonjava-scalar-result-diagnostics\",\n" + + " \"acquire_pool_hit\": " + ACQUIRE_POOL_HIT.sum() + ",\n" + + " \"acquire_pool_miss\": " + ACQUIRE_POOL_MISS.sum() + ",\n" + + " \"scalar_extraction\": " + SCALAR_EXTRACTION.sum() + ",\n" + + " \"recycled\": " + RECYCLED.sum() + ",\n" + + " \"rejected_ordinary_list\": " + REJECTED_ORDINARY_LIST.sum() + ",\n" + + " \"rejected_multi_element\": " + REJECTED_MULTI_ELEMENT.sum() + "\n" + + "}\n"; + try { + Files.writeString(Path.of(OUTPUT), json); + } catch (IOException e) { + System.err.println("cannot write scalar-result diagnostics: " + e.getMessage()); + } + } +} From 9237320100efd8553deaa91b0da6d574aabb5dc2 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 15 Sep 2026 18:09:09 +0200 Subject: [PATCH 13/13] perf: recycle copied scalar return wrappers Keep Perl's required scalar return copy, then mark only the private one-element scalar-context wrapper for runtime-local recycling. Generated with Codex (https://openai.com/codex) Co-Authored-By: Codex --- .../runtime/runtimetypes/RuntimeCode.java | 20 ++++++++++++++----- .../unit/scalar_result_recycler_rvalue.t | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) create mode 100644 src/test/resources/unit/scalar_result_recycler_rvalue.t diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 0bd42ec50c..4f240a797c 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -933,7 +933,7 @@ public static RuntimeList returnList(RuntimeBase retVal, int callContext, boolea } RuntimeList result = retVal.getList(); if (copyReferenceScalars && callContext == RuntimeContextType.LIST) { - RuntimeList copied = copyReturnedReferenceScalars(result, callContext, true); + RuntimeList copied = copyReturnedReferenceScalars(result, callContext, true, false); if (copied != result) { MortalList.pushTemporaryRoot(copied); } @@ -1013,7 +1013,8 @@ public static RuntimeList coerceScalarCallResult(RuntimeList result, int effecti return result; } if (effectiveContext == RuntimeContextType.SCALAR && result.elements.size() > 1) { - return copyReturnedReferenceScalars(new RuntimeList(result.scalar()), originalContext, copyCapturedScalars); + return copyReturnedReferenceScalars(new RuntimeList(result.scalar()), originalContext, + copyCapturedScalars, true); } if (effectiveContext == RuntimeContextType.SCALAR && result.elements.size() == 1) { RuntimeBase value = result.elements.getFirst(); @@ -1021,18 +1022,21 @@ public static RuntimeList coerceScalarCallResult(RuntimeList result, int effecti && originalContext != RuntimeContextType.LVALUE_LIST && value instanceof RuntimeScalar scalar && scalar.type == RuntimeScalarType.TIED_SCALAR) { - return copyReturnedReferenceScalars(new RuntimeList(scalar.tiedFetch()), originalContext, copyCapturedScalars); + return copyReturnedReferenceScalars(new RuntimeList(scalar.tiedFetch()), originalContext, + copyCapturedScalars, true); } } return copyReturnedReferenceScalars(copyReadonlyListReturns(result, effectiveContext), - originalContext, copyCapturedScalars); + originalContext, copyCapturedScalars, + effectiveContext == RuntimeContextType.SCALAR); } finally { MortalList.popTemporaryRoot(result); } } private static RuntimeList copyReturnedReferenceScalars(RuntimeList result, int originalContext, - boolean copyCapturedScalars) { + boolean copyCapturedScalars, + boolean recyclableScalarResult) { if (result == null || result instanceof RuntimeControlFlowList || !copyCapturedScalars @@ -1043,6 +1047,12 @@ private static RuntimeList copyReturnedReferenceScalars(RuntimeList result, int for (RuntimeBase value : result.elements) { if (value instanceof RuntimeScalar scalar && !isCodeScalar(scalar)) { + if (recyclableScalarResult && result.elements.size() == 1) { + // Scalar-context callers discard the list wrapper after extracting this + // copied rvalue. Keep Perl's required scalar copy, then make only the + // new one-element wrapper runtime-local and recyclable. + return RuntimeList.acquireScalarResult(scalar.clone()); + } return result.cloneScalars(); } } diff --git a/src/test/resources/unit/scalar_result_recycler_rvalue.t b/src/test/resources/unit/scalar_result_recycler_rvalue.t new file mode 100644 index 0000000000..5e704dd345 --- /dev/null +++ b/src/test/resources/unit/scalar_result_recycler_rvalue.t @@ -0,0 +1,20 @@ +use strict; +use warnings; +use Test::More tests => 3; + +our $global = 10; +sub returned_global { return $global } + +my $global_result = returned_global(); +$global_result = 20; +is($global, 10, 'scalar return remains a copied global rvalue'); + +my $captured = 30; +my $closure = sub { return $captured }; +my $captured_result = $closure->(); +$captured_result = 40; +is($captured, 30, 'scalar return remains a copied captured rvalue'); + +my @list_result = $closure->(); +$list_result[0] = 50; +is($captured, 30, 'list-context return remains a copied captured rvalue');