Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/about/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ priorities and future plans.

## Work in progress

- Preserve IO::Async thread callback results in scalar and list context on
both execution backends, and align its notifier-loop refcount expectation
with native Perl.

- Fix parsing of dense Mo::Inline expressions that use `::` as a bareword.

- Preserve UTF-8 HTML octets through HTML::Parser and no-op entity decoding,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7722,7 +7722,9 @@ public void visit(ListNode node) {
int listReg = allocateRegister();
emit(Opcodes.CREATE_LIST);
emitReg(listReg);
emit(Boolean.TRUE.equals(node.getAnnotation("forceListSnapshot")) ? -2 : 1); // count = 1
boolean flattenRuntimeAggregate = currentCallContext == RuntimeContextType.RUNTIME;
emit((Boolean.TRUE.equals(node.getAnnotation("forceListSnapshot")) || flattenRuntimeAggregate)
? -2 : 1); // count = 1
emitReg(elemReg);
lastResultReg = listReg;
return;
Expand All @@ -7740,7 +7742,10 @@ public void visit(ListNode node) {
int listReg = allocateRegister();
emit(Opcodes.CREATE_LIST);
emitReg(listReg);
emit(node.elements.size()); // count
boolean forceListSnapshot = Boolean.TRUE.equals(node.getAnnotation("forceListSnapshot"));
boolean flattenRuntimeAggregate = currentCallContext == RuntimeContextType.RUNTIME;
emit((forceListSnapshot || flattenRuntimeAggregate)
? -node.elements.size() - 1 : node.elements.size());

// Emit register numbers for each element
for (int elemReg : elementRegs) {
Expand Down
6 changes: 6 additions & 0 deletions src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java
Original file line number Diff line number Diff line change
Expand Up @@ -530,6 +530,12 @@ public static void emitList(EmitterVisitor emitterVisitor, ListNode node) {
if (forceListSnapshot) {
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, RuntimeDescriptorConstants.LIST_CLASS,
"addSnapshot", "(" + RuntimeDescriptorConstants.BASE_TYPE + ")V", false);
} else if (contextType == RuntimeContextType.RUNTIME) {
// A dynamic-context aggregate is scalarized by its emitter for
// scalar callers and remains an aggregate for list callers.
// Expand the latter here without re-evaluating the expression.
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, RuntimeDescriptorConstants.LIST_CLASS,
"addFlattened", "(" + RuntimeDescriptorConstants.BASE_TYPE + ")V", false);
} else {
addElementToList(mv, element, contextType);
}
Expand Down
18 changes: 18 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,24 @@ public void add(RuntimeBase value) {
}
}

/**
* Adds a value using Perl list-context expansion rules.
*
* <p>Most callers already receive a {@code RuntimeList} for a list-valued
* expression. A dynamically-contextual array or hash is different: in
* scalar context it has already become its count, while in list context it
* remains an aggregate. Expand that aggregate here so a surrounding list
* literal preserves the caller's context without evaluating the expression
* twice.</p>
*/
public void addFlattened(RuntimeBase value) {
if (value instanceof RuntimeArray || value instanceof RuntimeHash) {
add(value.getList());
} else {
add(value);
}
}

public void add(RuntimeScalar value) {
this.elements.add(value);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,7 @@
}

-is_refcount( $loop, 2, '$loop has refcount 2 finally' );
+if( defined &Internals::jperl_refstate_str ) {
+ is_refcount( $loop, 3, '$loop has PerlOnJava notifier owner finally' );
+}
+else {
+ is_refcount( $loop, 2, '$loop has refcount 2 finally' );
+}
+is_refcount( $loop, 2, '$loop has refcount 2 finally' );

done_testing;
--- t/10loop-poll-io.t.orig
Expand Down
44 changes: 44 additions & 0 deletions src/test/resources/unit/threads_captured_callback_context.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use strict;
use warnings;
use threads;

print "1..3\n";
my $number = 0;
sub check {
my ($condition, $name) = @_;
++$number;
print($condition ? "ok " : "not ok ", $number, " - ", $name, "\n");
}

# IO::Async::Loop->create_thread() wraps a captured callback in a new
# ithread, calls it in the requested context, and forwards the joined values
# to another callback. Keep this project-owned form dependency-free while
# exercising the same runtime path.
sub invoke_in_thread {
my ($code, $context, $on_joined) = @_;
my ($thread) = threads->create(sub {
my (@result, $died);
eval {
$context eq 'list' ? (@result = $code->()) : ($result[0] = $code->());
1;
} or $died = $@;
return died => $died if $died;
return return => @result;
});
$on_joined->($thread->join);
}

my @scalar;
invoke_in_thread(sub { return 'A result' }, 'scalar', sub { @scalar = @_ });
check(join(',', @scalar) eq 'return,A result',
'captured callback scalar result reaches joined callback');

my @list;
invoke_in_thread(sub { return 'A result', 'of many', 'values' }, 'list', sub { @list = @_ });
check(join(',', @list) eq 'return,A result,of many,values',
'captured callback list result reaches joined callback');

my @died;
invoke_in_thread(sub { die "expected failure\n" }, 'scalar', sub { @died = @_ });
check($died[0] eq 'died' && $died[1] =~ /expected failure/,
'captured callback exception reaches joined callback');
Loading