Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ priorities and future plans.
- Avoid arbitrary-precision allocation for ordinary native-integer unsigned
shifts while retaining the existing wide-integer fallback.

- Avoid materializing discarded simple list-assignment values for ordinary
scalar argument binding while retaining the observable result path.

- Avoid redundant active-runtime lookups while binding and restoring implicit
global `$_` `foreach` aliases.

Expand Down
14 changes: 12 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/EmitVariable.java
Original file line number Diff line number Diff line change
Expand Up @@ -1117,7 +1117,17 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo
// The my operator needs to be processed to create the variables first.
node.left.accept(emitterVisitor.with(RuntimeContextType.LVALUE_LIST)); // emit the variable (target)
mv.visitVarInsn(Opcodes.ALOAD, rhsListSlot); // reload RHS list
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", "setFromList", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;", false);
boolean discardResult = emitterVisitor.ctx.contextType == RuntimeContextType.VOID;
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeBase",
discardResult ? "setFromListVoid" : "setFromList",
discardResult ? "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)V"
: "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)Lorg/perlonjava/runtime/runtimetypes/RuntimeArray;",
false);
// The shared void-context epilogue consumes one stack value.
// Keep its existing stack contract without materializing a
// RuntimeArray assignment result.
if (discardResult) mv.visitInsn(Opcodes.ACONST_NULL);

if (pooledRhsList) {
ctx.javaClassInfo.releaseSpillSlot();
Expand All @@ -1127,7 +1137,7 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo
// caller's context. RuntimeArray.scalar() uses the RHS
// element count recorded by setFromList().
emitRuntimeContextConversion(emitterVisitor, "@");
} else {
} else if (!discardResult) {
EmitOperator.handleScalarContext(emitterVisitor, node);
}
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -917,6 +917,15 @@ public RuntimeScalar createReferenceWithTrackedElements() {
*/
public abstract RuntimeArray setFromList(RuntimeList list);

/**
* Sets this value from a list when the Perl assignment result is discarded.
* Subclasses may avoid materializing that result while retaining assignment
* side effects. The default preserves ordinary assignment semantics.
*/
public void setFromListVoid(RuntimeList list) {
setFromList(list);
}

/**
* Retrieves the result of keys() as a RuntimeArray instance.
*
Expand Down
106 changes: 58 additions & 48 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java
Original file line number Diff line number Diff line change
Expand Up @@ -582,54 +582,8 @@ public RuntimeList createListReference() {
public RuntimeArray setFromList(RuntimeList value) {
// Fast path: LHS is all simple scalars, RHS is a single RuntimeArray
// This handles the common case: my ($a, $b) = @_
if (value.elements.size() == 1 && value.elements.get(0) instanceof RuntimeArray rhsArray) {
boolean allSimpleScalars = true;
for (RuntimeBase elem : elements) {
if (!(elem instanceof RuntimeScalar) || elem instanceof RuntimeScalarReadOnly) {
allSimpleScalars = false;
break;
}
}
if (allSimpleScalars) {
// Suppress MortalList.flush() during LHS assignments, matching
// the slow path below. Without this, a blessed return value
// (e.g., Holler->new()) passed as an argument following a
// reference-typed arg can fire DESTROY mid-assignment when
// an earlier lhs.set() triggers setLargeRefCounted → flush()
// before the blessed value's own lhs.set() captures it.
// Repros: t/tt_leak.t tests 5, 9 (TT stash updates with
// blessed temps as values).
boolean wasFlushing = MortalList.suppressFlush(true);
try {
List<RuntimeScalar> rhsElements = rhsArray.elements;
int rhsSize = rhsElements.size();
int lhsSize = elements.size();

// Copy RHS values first to handle aliasing (e.g., ($a,$b) = ($b,$a))
RuntimeScalar[] rhsValues = new RuntimeScalar[Math.min(lhsSize, rhsSize)];
for (int i = 0; i < rhsValues.length; i++) {
RuntimeScalar elem = rhsElements.get(i);
// Handle null elements (from delete $array[i])
rhsValues[i] = (elem == null) ? new RuntimeScalar() : new RuntimeScalar(elem);
}

RuntimeArray result = new RuntimeArray(lhsSize);
result.scalarContextSize = rhsSize;
for (int i = 0; i < lhsSize; i++) {
RuntimeScalar lhs = (RuntimeScalar) elements.get(i);
if (i < rhsValues.length) {
lhs.set(rhsValues[i]);
} else {
lhs.set(new RuntimeScalar());
}
result.elements.add(lhs);
}
return result;
} finally {
MortalList.suppressFlush(wasFlushing);
}
}
}
RuntimeArray simpleRhs = simpleScalarListAssignmentRhs(value);
if (simpleRhs != null) return assignSimpleScalarList(simpleRhs, true);

boolean hasUndefPlaceholderLhs = false;
for (RuntimeBase elem : elements) {
Expand Down Expand Up @@ -803,6 +757,62 @@ public RuntimeArray setFromList(RuntimeList value) {
return result;
}

@Override
public void setFromListVoid(RuntimeList value) {
RuntimeArray simpleRhs = simpleScalarListAssignmentRhs(value);
if (simpleRhs != null) {
assignSimpleScalarList(simpleRhs, false);
return;
}
setFromList(value);
}

private RuntimeArray simpleScalarListAssignmentRhs(RuntimeList value) {
if (value.elements.size() != 1 || !(value.elements.get(0) instanceof RuntimeArray rhsArray)) {
return null;
}
for (RuntimeBase elem : elements) {
if (!(elem instanceof RuntimeScalar) || elem instanceof RuntimeScalarReadOnly) {
return null;
}
}
return rhsArray;
}

private RuntimeArray assignSimpleScalarList(RuntimeArray rhsArray, boolean materializeResult) {
// Suppress MortalList.flush() during LHS assignments, matching the slow
// path below. A blessed return value passed beside a reference-typed
// argument must remain alive until every LHS slot captures it.
boolean wasFlushing = MortalList.suppressFlush(true);
try {
List<RuntimeScalar> rhsElements = rhsArray.elements;
int rhsSize = rhsElements.size();
int lhsSize = elements.size();

// Copy RHS values first to handle aliasing (e.g., ($a,$b) = ($b,$a)).
RuntimeScalar[] rhsValues = new RuntimeScalar[Math.min(lhsSize, rhsSize)];
for (int i = 0; i < rhsValues.length; i++) {
RuntimeScalar elem = rhsElements.get(i);
rhsValues[i] = (elem == null) ? new RuntimeScalar() : new RuntimeScalar(elem);
}

RuntimeArray result = materializeResult ? new RuntimeArray(lhsSize) : null;
if (result != null) result.scalarContextSize = rhsSize;
for (int i = 0; i < lhsSize; i++) {
RuntimeScalar lhs = (RuntimeScalar) elements.get(i);
if (i < rhsValues.length) {
lhs.set(rhsValues[i]);
} else {
lhs.set(new RuntimeScalar());
}
if (result != null) result.elements.add(lhs);
}
return result;
} finally {
MortalList.suppressFlush(wasFlushing);
}
}

/**
* Converts the list to a string, concatenating all elements without separators.
*
Expand Down
24 changes: 24 additions & 0 deletions src/test/resources/unit/void_list_assignment.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use strict;
use warnings;
use Test::More;

sub bind_and_format {
my ($left, $right, $missing) = @_;
return join ':', map { defined $_ ? $_ : 'undef' } $left, $right, $missing;
}

is bind_and_format('alpha', 'beta'), 'alpha:beta:undef',
'void-context parameter binding preserves supplied and missing arguments';

my ($first, $second) = ('left', 'right');
($first, $second) = ($second, $first);
is_deeply [$first, $second], ['right', 'left'],
'void list assignment preserves RHS values before aliased LHS writes';

my ($one, $two);
my $count = scalar(($one, $two) = qw(one two));
is $count, 2, 'non-void list assignment still returns its scalar element count';
is_deeply [$one, $two], [qw(one two)],
'non-void list assignment still assigns every element';

done_testing;