Skip to content

Commit 58fb19d

Browse files
authored
Merge pull request #966 from fglock/feature/complete-perl-threads-final
Complete Perl ithread lifecycle compatibility
2 parents 7097da1 + fe9d6bc commit 58fb19d

53 files changed

Lines changed: 2418 additions & 1036 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dev/design/concurrency.md

Lines changed: 82 additions & 886 deletions
Large diffs are not rendered by default.

dev/tools/perl_test_runner.pl

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use JSON::PP;
99
use Data::Dumper;
1010
use POSIX qw(WNOHANG);
11+
use Config ();
1112

1213
# PerlOnJava Test Runner
1314
# Runs standard Perl tests against PerlOnJava and analyzes results
@@ -244,6 +245,30 @@ sub process_test_result {
244245
sub run_single_test {
245246
my ($test_file) = @_;
246247

248+
# Core thread distributions assume their own build-directory cwd. In
249+
# particular, threads and threads-shared resolve ../../t/test.pl when
250+
# PERL_CORE is set, while Thread-Queue and Thread-Semaphore load their
251+
# pure-Perl implementation from the distribution's lib directory. Keep
252+
# the upstream tests unchanged and reproduce that layout here.
253+
my $thread_distribution_root;
254+
my $thread_distribution_uses_core = 0;
255+
if ($test_file =~ m{^(perl5/dist/(threads|threads-shared))/t/}) {
256+
$thread_distribution_root = $1;
257+
$thread_distribution_uses_core = 1;
258+
} elsif ($test_file =~ m{^(perl5/dist/(?:Thread-Queue|Thread-Semaphore))/t/}) {
259+
$thread_distribution_root = $1;
260+
}
261+
262+
local $ENV{PERL_CORE} = $thread_distribution_uses_core ? 1 : $ENV{PERL_CORE};
263+
local $ENV{PERL5LIB} = $ENV{PERL5LIB};
264+
if ($thread_distribution_root && !$thread_distribution_uses_core) {
265+
my $distribution_lib = File::Spec->rel2abs("$thread_distribution_root/lib");
266+
my $separator = $Config::Config{path_sep} || ':';
267+
$ENV{PERL5LIB} = defined($ENV{PERL5LIB}) && length($ENV{PERL5LIB})
268+
? "$distribution_lib$separator$ENV{PERL5LIB}"
269+
: $distribution_lib;
270+
}
271+
247272
# A few subprocess- or CPU-heavy tests routinely use most of the default
248273
# deadline and can cross it when the full parallel corpus contends for CPU.
249274
# Give those known outliers a stable minimum wall-clock allowance while
@@ -321,7 +346,10 @@ sub run_single_test {
321346
# For perl5_t tests (especially Pod tests), change to the test directory
322347
# so they can find their test data files with relative paths
323348
my $local_test_dir = $test_dir;
324-
if ($test_file =~ m{^perl5_t/t/}) {
349+
if ($thread_distribution_root) {
350+
$local_test_dir = $thread_distribution_root;
351+
}
352+
elsif ($test_file =~ m{^perl5_t/t/}) {
325353
# For core Perl 5 tests in perl5_t/t/, chdir to perl5_t/t
326354
# so they can find TestInit.pm via require
327355
$local_test_dir = 'perl5_t/t';

src/main/java/org/perlonjava/app/cli/Main.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,12 @@ private static void run(String[] args) {
126126
try {
127127
PerlLanguageProvider.executePerlCode(parsedArgs, true);
128128

129+
int requestedThreadExit = PerlRuntime.current().threadRegistry()
130+
.requestedProcessExitOr(Integer.MIN_VALUE);
131+
if (requestedThreadExit != Integer.MIN_VALUE) {
132+
System.exit(requestedThreadExit);
133+
}
134+
129135
if (parsedArgs.compileOnly) {
130136
// Match system perl: `perl -c` prints this line to stderr (Test::Script relies on it).
131137
System.err.println(parsedArgs.fileName + " syntax OK");
@@ -143,7 +149,8 @@ private static void run(String[] args) {
143149
}
144150
} catch (PerlExitException e) {
145151
// Perl's exit() throws PerlExitException - convert to real System.exit() for CLI
146-
System.exit(e.getExitCode());
152+
System.exit(PerlRuntime.current().threadRegistry()
153+
.requestedProcessExitOr(e.getExitCode()));
147154
} catch (Throwable t) {
148155
if (parsedArgs.debugEnabled) {
149156
// Print full JVM stack

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3399,6 +3399,11 @@ void compileVariableDeclaration(OperatorNode node, String op) {
33993399
throwCompilerException("Unsupported variable type in list declaration: " + sigil);
34003400
}
34013401

3402+
// A captured declaration-list slot is retrieved from the
3403+
// persistent definition-time cell, but attributes still
3404+
// apply at runtime to that exact cell before its first use.
3405+
emitVarAttrsIfNeeded(node, reg, sigil);
3406+
34023407
varRegs.add(reg);
34033408
wrapWithRef.add(isDeclaredReference);
34043409
} else {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ static boolean isImmutableProxy(RuntimeBase val) {
5757
private static boolean lexicalAssignmentMustPreserveSlot(RuntimeBase val) {
5858
if (!(val instanceof RuntimeScalar scalar)) return false;
5959
return scalar instanceof ReadOnlyAlias
60+
|| scalar.threadShared
6061
|| scalar.captureCount > 0
6162
|| scalar.captureRefCountOwned > 0
6263
|| scalar.referencedByScalarReference

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

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -798,12 +798,12 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
798798
}
799799
}
800800
bytecodeCompiler.registerVariable(varName, varReg);
801-
// Outer `my ($a,$b) : ATTR` puts attributes on the `my` node, not on
802-
// each list slot; dispatching MODIFY_*_ once per RETRIEVE_BEGIN_* slot
803-
// would duplicate calls. Variable attributes + RETRIEVE_BEGIN_* are
804-
// fully handled for `my $x`, `my @x`, `my %x`, and `my $x=` forms above.
805801
bytecodeCompiler.emit(Opcodes.REGISTER_MY_VAR);
806802
bytecodeCompiler.emitReg(varReg);
803+
// Attributes on a list declaration belong to every declared slot.
804+
// The assignment-specific path constructs these slots directly, so
805+
// it must dispatch attributes here before SET_FROM_LIST populates them.
806+
bytecodeCompiler.emitVarAttrsIfNeeded(leftOp, varReg, sigil);
807807
} else {
808808
varReg = bytecodeCompiler.addVariable(varName, "my");
809809
switch (sigil) {
@@ -823,6 +823,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
823823
bytecodeCompiler.emitLexicalAlias(varReg, varName);
824824
bytecodeCompiler.emit(Opcodes.REGISTER_MY_VAR);
825825
bytecodeCompiler.emitReg(varReg);
826+
bytecodeCompiler.emitVarAttrsIfNeeded(leftOp, varReg, sigil);
826827
}
827828
varRegs.add(varReg);
828829
}

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,14 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
691691
// The repeat operator preserves list context on its left operand:
692692
// `(($expr) x 4)` repeats values, while scalar-context x repeats the
693693
// resulting string. All other ordinary binary operands are scalar.
694-
int leftCtx = node.operator.equals("x") ? outerCtx : RuntimeContextType.SCALAR;
694+
int leftCtx = switch (node.operator) {
695+
case "x" -> outerCtx;
696+
// Preserve the actual scalar slot: bless may publish metadata through
697+
// a threads::shared scalar and must not operate on a temporary copy.
698+
case "bless" -> isDirectScalarLvalue(node.left)
699+
? RuntimeContextType.LVALUE : RuntimeContextType.SCALAR;
700+
default -> RuntimeContextType.SCALAR;
701+
};
695702
bytecodeCompiler.compileNode(node.left, -1, leftCtx);
696703
int rs1 = bytecodeCompiler.lastResultReg;
697704

@@ -712,12 +719,33 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
712719
int rs2 = bytecodeCompiler.lastResultReg;
713720

714721
// Emit opcode based on operator (delegated to helper method)
715-
int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(bytecodeCompiler, node, rs1, rs2, node.getIndex());
722+
// In list context, both forms are range operators. The distinction
723+
// between `..` and `...` only belongs to scalar flip-flop semantics.
724+
// Keeping `...` here used to emit FLIP_FLOP even for array-slice
725+
// indices such as @array[1...4], collapsing the slice to one element.
726+
int rd = node.operator.equals("...") && outerCtx != RuntimeContextType.SCALAR
727+
? CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(
728+
bytecodeCompiler, "..", rs1, rs2, node.getIndex())
729+
: CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(
730+
bytecodeCompiler, node, rs1, rs2, node.getIndex());
716731

717732

718733
bytecodeCompiler.lastResultReg = rd;
719734
}
720735

736+
private static boolean isDirectScalarLvalue(Node node) {
737+
if (node instanceof OperatorNode operator) {
738+
return operator.operator.equals("$");
739+
}
740+
if (node instanceof BinaryOperatorNode binary) {
741+
return switch (binary.operator) {
742+
case "[", "{" -> true;
743+
default -> false;
744+
};
745+
}
746+
return false;
747+
}
748+
721749
private static void compileBinaryAsListOp(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) {
722750
if (node.left instanceof IdentifierNode idNode) {
723751
String name = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage());

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,12 @@ public static String disassemble(InterpretedCode interpretedCode) {
208208
src = interpretedCode.bytecode[pc++];
209209
sb.append("ASSIGN_LEXICAL_SCALAR r").append(rd).append(" = r").append(src).append("\n");
210210
break;
211+
case Opcodes.DISPATCH_VAR_ATTRS:
212+
rd = interpretedCode.bytecode[pc++];
213+
int attributeMetadataIdx = interpretedCode.bytecode[pc++];
214+
sb.append("DISPATCH_VAR_ATTRS r").append(rd)
215+
.append(" const[").append(attributeMetadataIdx).append("]\n");
216+
break;
211217
case Opcodes.RELEASE_CONSUMED_TEMP:
212218
src = interpretedCode.bytecode[pc++];
213219
rd = interpretedCode.bytecode[pc++];

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2355,9 +2355,9 @@ public class Opcodes {
23552355
/**
23562356
* Assign to an existing lexical scalar.
23572357
* Plain lexicals are replaced with a fresh RuntimeScalar, preserving the
2358-
* current interpreter behavior for local/alias restoration. Magical lexicals
2359-
* such as tied scalars and Internals::SvREADONLY scalars are assigned in
2360-
* place so STORE/read-only checks still fire.
2358+
* current interpreter behavior for local/alias restoration. Magical and
2359+
* shared lexicals are assigned in place so STORE/read-only checks and
2360+
* shared storage identity are preserved.
23612361
* Format: ASSIGN_LEXICAL_SCALAR rd rs
23622362
*/
23632363
public static final short ASSIGN_LEXICAL_SCALAR = 491;

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
import org.perlonjava.frontend.analysis.EmitterVisitor;
88
import org.perlonjava.frontend.astnode.BinaryOperatorNode;
99
import org.perlonjava.frontend.astnode.IdentifierNode;
10+
import org.perlonjava.frontend.astnode.Node;
1011
import org.perlonjava.frontend.astnode.NumberNode;
12+
import org.perlonjava.frontend.astnode.OperatorNode;
1113
import org.perlonjava.frontend.astnode.StringNode;
1214
import org.perlonjava.runtime.operators.OperatorHandler;
1315
import org.perlonjava.runtime.perlmodule.Strict;
@@ -30,6 +32,13 @@ private static boolean isIntegerEnabled(EmitterVisitor emitterVisitor, BinaryOpe
3032
static void handleBinaryOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node, OperatorHandler operatorHandler) {
3133
EmitterVisitor scalarVisitor =
3234
emitterVisitor.with(RuntimeContextType.SCALAR); // execute operands in scalar context
35+
// bless mutates the scalar slot as well as its referent. In particular,
36+
// threads::shared publishes a class change only when the operand is the
37+
// actual shared scalar, not a scalar-context copy of its reference.
38+
EmitterVisitor leftVisitor = node.operator.equals("bless")
39+
&& isDirectScalarLvalue(node.left)
40+
? emitterVisitor.with(RuntimeContextType.LVALUE)
41+
: scalarVisitor;
3342
if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("handleBinaryOperator: " + node.toString());
3443

3544
if (isIntegerEnabled(emitterVisitor, node)
@@ -200,7 +209,7 @@ && switch (node.operator) {
200209
}
201210

202211
MethodVisitor mv = emitterVisitor.ctx.mv;
203-
node.left.accept(scalarVisitor); // left parameter
212+
node.left.accept(leftVisitor); // left parameter
204213
int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot();
205214
boolean pooled = leftSlot >= 0;
206215
if (!pooled) {
@@ -219,6 +228,19 @@ && switch (node.operator) {
219228
emitOperator(node, emitterVisitor);
220229
}
221230

231+
private static boolean isDirectScalarLvalue(Node node) {
232+
if (node instanceof OperatorNode operator) {
233+
return operator.operator.equals("$");
234+
}
235+
if (node instanceof BinaryOperatorNode binary) {
236+
return switch (binary.operator) {
237+
case "[", "{" -> true;
238+
default -> false;
239+
};
240+
}
241+
return false;
242+
}
243+
222244
private static void emitIntegerBinaryOperator(EmitterVisitor emitterVisitor,
223245
EmitterVisitor scalarVisitor,
224246
BinaryOperatorNode node,

0 commit comments

Comments
 (0)