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
36 changes: 30 additions & 6 deletions dev/design/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,8 +219,10 @@ Acceptance:
### Phase 2 — Serialized compilation

Add one documented `ReentrantLock` around every initial and runtime compilation
path. Preserve reentrant nested compilation and release the lock before code
execution.
path, including lazy named-sub materialization and verifier fallback. Preserve
reentrant nested compilation and release each entry point's own hold before
ordinary code execution. `executePerlAST` remains locked because it runs
compile-time `BEGIN` wrappers inside an enclosing parse.

Acceptance: concurrent JVM/bytecode compilations, nested `BEGIN`/`require`, and
both `eval STRING` paths are deterministic and deadlock-free.
Expand Down Expand Up @@ -442,7 +444,7 @@ measured benefit over platform threads/full clone.

## 7. Progress Tracking

### Current Status: Phase 1 complete; awaiting review
### Current Status: Phase 2 complete; awaiting review

### Completed Phases

Expand All @@ -454,6 +456,27 @@ measured benefit over platform threads/full clone.
- Added concurrent generated-class-name coverage.
- Validation: `make` passed; `make test-all` completed with the recorded
compatibility baseline (core 82.9%, bundled modules 80.8%).
- Merged as PR #915 on 2026-08-11.
- [x] Phase 2: Serialized compilation (2026-08-11)
- Added one reentrant compilation lock with an idempotent, one-hold guard.
- Serialized initial source/AST compilation, both interpreter eval roots,
JVM eval compilation, lazy named-sub materialization, reset, and verifier
fallback compilation.
- Kept ordinary program/eval execution outside each entry point's own hold;
compile-time `BEGIN` wrapper execution remains inside the enclosing hold.
- Restored compiler scope, eval aliases, and hint state under the lock and
made lexer/parser failures release their exact acquisition.
- Added deterministic queueing, reentrancy, failure-cleanup, mixed-backend
concurrency, nested `BEGIN`/eval, and unlocked-execution tests.
- Files: `PerlLanguageProvider.java`, `EvalStringHandler.java`,
`RuntimeCode.java`, `SubroutineParser.java`, and `CompilationLockTest.java`.
- Validation: `make` passed; focused eval regressions passed under system
Perl and both PerlOnJava backends; Scalar::Util 1.70 and Moo 2.005005 loaded
successfully on both backends; `make test-all` completed at core 83.0% and
bundled modules 80.8%.
- Audit delta: no runtime state moved and no production worker was added; the
new lock is the only mutable static and covers the newly identified lazy-sub
and verifier-fallback compiler escape paths.

### Phase 1 Work Completed (2026-08-10)

Expand All @@ -472,9 +495,10 @@ measured benefit over platform threads/full clone.

### Next Steps

1. Complete Phase 1 validation and open its PR.
2. Merge Phase 1 before starting Phase 2.
3. Implement the global reentrant compilation boundary in Phase 2.
1. Open and merge the independent Phase 2 PR.
2. Start Phase 3 from updated `master` only after Phase 2 merges.
3. Introduce the `PerlRuntime` shell and scoped explicit binding without moving
mutable runtime state yet.

### Open Questions

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.concurrent.locks.ReentrantLock;

import static org.perlonjava.runtime.runtimetypes.GlobalVariable.resetAllGlobals;
import static org.perlonjava.runtime.runtimetypes.SpecialBlock.*;
Expand All @@ -55,17 +56,56 @@
*/
public class PerlLanguageProvider {

/**
* Serializes source parsing and code generation while compiler state is still
* process-global. The lock is deliberately reentrant because BEGIN, use,
* require, and eval STRING can compile recursively on the parser thread.
*/
public static final ReentrantLock COMPILE_LOCK = new ReentrantLock();

/** Acquire exactly one compilation-lock hold. */
public static CompilationLockGuard acquireCompilationLock() {
COMPILE_LOCK.lock();
return new CompilationLockGuard();
}

/**
* An idempotent lexical owner for one lock hold. Idempotence lets mixed
* compile/execute methods release before ordinary execution while retaining
* reliable cleanup on every exceptional path.
*/
public static final class CompilationLockGuard implements AutoCloseable {
private boolean closed;

private CompilationLockGuard() {
}

public boolean isClosed() {
return closed;
}

@Override
public void close() {
if (!closed) {
closed = true;
COMPILE_LOCK.unlock();
}
}
}

private static boolean globalInitialized = false;

public static void resetAll() {
globalInitialized = false;
GlobalContext.setThreadTaintMode(false);
resetAllGlobals();
// A prior script may have closed its Perl-level STDIN glob. Script
// engine resets run multiple top-level programs in one JVM, so give
// the next program a fresh wrapper around the process standard input.
RuntimeIO.stdin = new RuntimeIO(new StandardIO(System.in));
DataSection.reset();
try (CompilationLockGuard ignored = acquireCompilationLock()) {
globalInitialized = false;
GlobalContext.setThreadTaintMode(false);
resetAllGlobals();
// A prior script may have closed its Perl-level STDIN glob. Script
// engine resets run multiple top-level programs in one JVM, so give
// the next program a fresh wrapper around the process standard input.
RuntimeIO.stdin = new RuntimeIO(new StandardIO(System.in));
DataSection.reset();
}
}

/**
Expand Down Expand Up @@ -93,6 +133,12 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions,
boolean isTopLevelScript,
int callerContext) throws Exception {

CompilationLockGuard compilationLock = acquireCompilationLock();
ScopedSymbolTable savedCurrentScope = null;
RuntimeCode.EvalRuntimeContext savedEvalRuntimeContext = null;
boolean evalRuntimeContextSaved = false;
try {

// The compiler options are also the source of truth for nested loads.
// ModuleOperators creates fresh CompilerOptions instances for require/use
// and reads RuntimeCode.USE_INTERPRETER when choosing their backend. If
Expand All @@ -115,15 +161,15 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions,

// Save the current scope so we can restore it after execution.
// This is critical because require/do should not leak their scope to the caller.
ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope();
savedCurrentScope = SpecialBlockParser.getCurrentScope();

// Save and clear the eval runtime context so that modules loaded via require/do
// during eval STRING execution don't see the eval's captured variables.
// Without this, SpecialBlockParser.runSpecialBlock would incorrectly alias
// local variables in required modules to the eval's captured variables when
// they share the same name (e.g., $caller in constant.pm vs $caller in eval scope).
RuntimeCode.EvalRuntimeContext savedEvalRuntimeContext =
RuntimeCode.saveAndClearEvalRuntimeContextAndAliases();
savedEvalRuntimeContext = RuntimeCode.saveAndClearEvalRuntimeContextAndAliases();
evalRuntimeContextSaved = true;

// Store the isMainProgram flag in CompilerOptions for use during code generation
compilerOptions.isMainProgram = isTopLevelScript;
Expand Down Expand Up @@ -261,21 +307,35 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions,
ctx.symbolTable = ctx.symbolTable.snapShot();
SpecialBlockParser.setCurrentScope(ctx.symbolTable);

try {
// Compile to executable (compiler or interpreter based on flag)
RuntimeCode runtimeCode = compileToExecutable(ast, ctx);
// Compile to executable (compiler or interpreter based on flag)
RuntimeCode runtimeCode = compileToExecutable(ast, ctx);

// Execute (unified path for both backends)
return executeCode(runtimeCode, ast, ctx, isTopLevelScript, callerContext);
// Ordinary program execution is not compiler work. Release this
// invocation's hold; an enclosing BEGIN compilation, if any, keeps
// its own reentrant hold until that compilation completes.
compilationLock.close();

// Execute (unified path for both backends)
return executeCode(runtimeCode, ast, ctx, isTopLevelScript, callerContext);
} finally {
// Scope restoration mutates compiler-global state. Reacquire when
// ordinary execution already released this invocation's hold.
COMPILE_LOCK.lock();
try {
// Restore the caller's scope so require/do doesn't leak its scope to the caller.
// But do NOT restore for top-level scripts - we want the main script's pragmas to persist.
if (savedCurrentScope != null && !isTopLevelScript) {
SpecialBlockParser.setCurrentScope(savedCurrentScope);
}
// Restore the eval runtime context so the caller's eval STRING compilation
// can continue with its captured variables.
RuntimeCode.restoreEvalRuntimeContext(savedEvalRuntimeContext);
if (evalRuntimeContextSaved) {
RuntimeCode.restoreEvalRuntimeContext(savedEvalRuntimeContext);
}
} finally {
COMPILE_LOCK.unlock();
compilationLock.close();
}
}
}

Expand Down Expand Up @@ -308,6 +368,8 @@ public static RuntimeList executePerlAST(Node ast,
CompilerOptions compilerOptions,
int contextType) throws Exception {

try (CompilationLockGuard ignored = acquireCompilationLock()) {

// Keep AST execution consistent with source execution. ASTs are used
// by BEGIN-block wrappers, and those wrappers can themselves execute
// require/use during compilation.
Expand Down Expand Up @@ -400,6 +462,7 @@ public static RuntimeList executePerlAST(Node ast,
// Restore the eval runtime context
RuntimeCode.restoreEvalRuntimeContext(savedEvalRuntimeContext);
}
}
}

/**
Expand Down Expand Up @@ -497,11 +560,14 @@ private static RuntimeList executeCodeImpl(RuntimeCode runtimeCode, Node ast, Em
if (CompilerOptions.DEBUG_ENABLED) {
ctx.logDebug("Falling back to bytecode interpreter after runtime verify error: " + t);
}
BytecodeCompiler compiler = new BytecodeCompiler(
ctx.compilerOptions.fileName,
1,
ctx.errorUtil);
InterpretedCode interpretedCode = compiler.compile(ast, ctx);
InterpretedCode interpretedCode;
try (CompilationLockGuard ignored = acquireCompilationLock()) {
BytecodeCompiler compiler = new BytecodeCompiler(
ctx.compilerOptions.fileName,
1,
ctx.errorUtil);
interpretedCode = compiler.compile(ast, ctx);
}
result = interpretedCode.apply(new RuntimeArray(), executionContext);
} else {
throw t;
Expand Down Expand Up @@ -737,6 +803,9 @@ private static boolean needsInterpreterFallback(Throwable e) {
* @throws Exception if compilation fails
*/
public static Object compilePerlCode(CompilerOptions compilerOptions) throws Exception {
try (CompilationLockGuard ignored = acquireCompilationLock()) {
ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope();
try {
ArgumentParser.applyPerlShebangSwitches(compilerOptions.code, compilerOptions);
GlobalContext.setThreadTaintMode(compilerOptions.taintMode);
ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable();
Expand Down Expand Up @@ -785,5 +854,9 @@ public static Object compilePerlCode(CompilerOptions compilerOptions) throws Exc

// Use unified compilation path (works for JSR 223 too!)
return compileToExecutable(ast, ctx);
} finally {
SpecialBlockParser.setCurrentScope(savedCurrentScope);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package org.perlonjava.backend.bytecode;

import org.perlonjava.app.cli.CompilerOptions;
import org.perlonjava.app.scriptengine.PerlLanguageProvider;
import org.perlonjava.backend.jvm.EmitterContext;
import org.perlonjava.backend.jvm.EmitterMethodCreator;
import org.perlonjava.backend.jvm.JavaClassInfo;
Expand All @@ -10,7 +11,6 @@
import org.perlonjava.frontend.parser.Parser;
import org.perlonjava.frontend.parser.SpecialBlockParser;
import org.perlonjava.frontend.semantic.ScopedSymbolTable;
import org.perlonjava.runtime.perlmodule.BHooksEndOfScope;
import org.perlonjava.runtime.operators.WarnDie;
import org.perlonjava.runtime.perlmodule.BHooksEndOfScope;
import org.perlonjava.runtime.runtimetypes.*;
Expand Down Expand Up @@ -203,6 +203,8 @@ private static RuntimeList evalStringList(String perlCode,
int siteStrictOptions,
int siteFeatureFlags,
boolean isEvalbytes) {
PerlLanguageProvider.CompilationLockGuard compilationLock =
PerlLanguageProvider.acquireCompilationLock();
List<EvalSeedAlias> seedAliases = new ArrayList<>();
ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope();
ScopedSymbolTable compileTimeMutationScope = SpecialBlockParser.getCompileTimeMutationScope();
Expand Down Expand Up @@ -455,10 +457,14 @@ private static RuntimeList evalStringList(String perlCode,
evalCode = evalCode.withCapturedVars(currentCode.capturedVars);
}

// These aliases are parser/compile-time helpers only. Direct eval
// body references use captured registers, and named subs have
// already captured the aliased cells by this point.
// Compiler-only aliases and scope must be restored before another
// compiler is admitted. Ordinary eval execution runs unlocked.
deactivateEvalSeedAliases(seedAliases);
if (compileTimeMutationScope != savedCurrentScope) {
savedCurrentScope.copyFlagsFrom(compileTimeMutationScope);
}
SpecialBlockParser.setCurrentScope(savedCurrentScope);
compilationLock.close();

// Step 6: Execute the compiled code.
// IMPORTANT: Scope InterpreterState.currentPackage around eval execution.
Expand Down Expand Up @@ -491,11 +497,17 @@ private static RuntimeList evalStringList(String perlCode,
WarnDie.catchEval(e);
return new RuntimeList(new RuntimeScalar());
} finally {
deactivateEvalSeedAliases(seedAliases);
if (compileTimeMutationScope != savedCurrentScope) {
savedCurrentScope.copyFlagsFrom(compileTimeMutationScope);
PerlLanguageProvider.COMPILE_LOCK.lock();
try {
deactivateEvalSeedAliases(seedAliases);
if (compileTimeMutationScope != savedCurrentScope) {
savedCurrentScope.copyFlagsFrom(compileTimeMutationScope);
}
SpecialBlockParser.setCurrentScope(savedCurrentScope);
} finally {
PerlLanguageProvider.COMPILE_LOCK.unlock();
compilationLock.close();
}
SpecialBlockParser.setCurrentScope(savedCurrentScope);
}
}

Expand Down Expand Up @@ -541,6 +553,8 @@ public static RuntimeScalar evalString(String perlCode,
RuntimeBase[] capturedVars,
String sourceName,
int sourceLine) {
PerlLanguageProvider.CompilationLockGuard compilationLock =
PerlLanguageProvider.acquireCompilationLock();
ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope();
ScopedSymbolTable compileTimeMutationScope = SpecialBlockParser.getCompileTimeMutationScope();
try {
Expand Down Expand Up @@ -612,6 +626,12 @@ public static RuntimeScalar evalString(String perlCode,
// Attach captured variables
evalCode = evalCode.withCapturedVars(capturedVars);

if (compileTimeMutationScope != savedCurrentScope) {
savedCurrentScope.copyFlagsFrom(compileTimeMutationScope);
}
SpecialBlockParser.setCurrentScope(savedCurrentScope);
compilationLock.close();

// Scope currentPackage around eval — see Step 6 comment in evalStringHelper above.
int pkgLevel = DynamicVariableManager.getLocalLevel();
String savedPkg = InterpreterState.currentPackage.get().toString();
Expand All @@ -633,10 +653,16 @@ public static RuntimeScalar evalString(String perlCode,
WarnDie.catchEval(e);
return RuntimeScalarCache.scalarUndef;
} finally {
if (compileTimeMutationScope != savedCurrentScope) {
savedCurrentScope.copyFlagsFrom(compileTimeMutationScope);
PerlLanguageProvider.COMPILE_LOCK.lock();
try {
if (compileTimeMutationScope != savedCurrentScope) {
savedCurrentScope.copyFlagsFrom(compileTimeMutationScope);
}
SpecialBlockParser.setCurrentScope(savedCurrentScope);
} finally {
PerlLanguageProvider.COMPILE_LOCK.unlock();
compilationLock.close();
}
SpecialBlockParser.setCurrentScope(savedCurrentScope);
}
}

Expand Down
Loading
Loading