diff --git a/dev/design/concurrency.md b/dev/design/concurrency.md
index 42c66c4ec..4e151a548 100644
--- a/dev/design/concurrency.md
+++ b/dev/design/concurrency.md
@@ -1485,6 +1485,88 @@ and why it was reverted.)
*None yet.*
+---
+
+### Optimization Results (2026-04-10)
+
+**Branch:** `feature/multiplicity-opt` (created from `feature/multiplicity`)
+
+#### JFR Profiling Findings
+
+Profiled the closure benchmark (`dev/bench/benchmark_closure.pl`) with Java
+Flight Recorder to identify the dominant overhead sources. Key findings:
+
+| Category | JFR Samples | Source |
+|----------|-------------|--------|
+| `PerlRuntime.current()` / ThreadLocal | 143 | The ThreadLocal.get() call itself |
+| RuntimeRegex accessors | ~126 | **Dominant source** — 13 getters + 13 setters per sub call via RegexState save/restore |
+| DynamicVariableManager | ~90 | `local` variable save/restore (includes regex state push) |
+| pushCallerState (batch) | 10 | Caller bits/hints/hint-hash state |
+| pushSubState (batch) | 3 | Args + warning bits |
+| ArrayDeque.push | 13 | Stack operations |
+
+**Critical finding:** `RegexState` save/restore was calling 13 individual
+`RuntimeRegex` static accessors (each doing its own `PerlRuntime.current()`
+ThreadLocal lookup) on every subroutine entry AND exit — **26 ThreadLocal
+lookups per sub call** — even when the subroutine never uses regex.
+
+#### Optimizations Applied
+
+| Tier | Optimization | Files Changed | ThreadLocal Lookups Eliminated |
+|------|-------------|---------------|-------------------------------|
+| 1 | Cache `PerlRuntime.current()` in local variables | GlobalVariable.java, InheritanceResolver.java | ~12 per method cache miss |
+| 2 | Migrate WarningBits/HintHash/args stacks to PerlRuntime fields | WarningBitsRegistry.java, HintHashRegistry.java, RuntimeCode.java, PerlRuntime.java | 14-17 per sub call (separate ThreadLocals -> 1 ThreadLocal) |
+| 2b | Batch pushCallerState/popCallerState and pushSubState/popSubState | PerlRuntime.java, RuntimeCode.java | 8-12 per sub call -> 2 |
+| 2c | Batch RegexState save/restore | RegexState.java | **24 per sub call** (13 getters + 13 setters -> 2) |
+| 2d | Skip RegexState save/restore for subs without regex | EmitterMethodCreator.java, RegexUsageDetector.java | **Entire save/restore eliminated** for non-regex subs |
+| 3 | JVM-compile anonymous subs inside `eval STRING` | BytecodeCompiler.java, OpcodeHandlerExtended.java, JvmClosureTemplate.java (new) | N/A (execution speedup, not ThreadLocal reduction) |
+
+**Tier 3: JVM compilation of eval STRING anonymous subs.** Previously,
+`BytecodeCompiler.visitAnonymousSubroutine()` always compiled anonymous sub bodies
+to `InterpretedCode`. This meant hot closures created via `eval STRING` (e.g.,
+Benchmark.pm's `sub { for (1..$n) { &$c } }`) ran in the bytecode interpreter.
+The fix tries JVM compilation first via `EmitterMethodCreator.createClassWithMethod()`,
+falling back to the interpreter on any failure (e.g., ASM frame computation crash).
+A new `JvmClosureTemplate` class holds the JVM-compiled class and instantiates it
+with captured variables via reflection. Measured 4.5x speedup for eval STRING closures
+in isolation (6.4M iter/s vs 1.4M iter/s). The broader benchmark improvements (method
++36.7% vs pre-opt, global +10.8%) likely reflect this change improving Benchmark.pm's
+own infrastructure which is used by all benchmarks.
+
+#### Benchmark Results (updated 2026-04-10, after JVM eval STRING closures)
+
+| Benchmark | master | branch (pre-opt) | **Current** | vs master | vs pre-opt |
+|-----------|--------|-------------------|-------------|-----------|------------|
+| **closure** | 863 | 569 (-34.1%) | **1,220** | **+41.4%** | +114.4% |
+| **method** | 436 | 319 (-26.9%) | **436** | **0.0%** | +36.7% |
+| **lexical** | 394K | 375K (-4.9%) | **480K** | **+21.8%** | +28.0% |
+| global | 78K | 74K (-5.4%) | **82K** | +5.1% | +10.8% |
+| eval_string | 86K | 82K (-4.8%) | **89K** | +3.1% | +8.5% |
+| regex | 51K | 47K (-7.0%) | **46K** | -9.4% | -2.1% |
+| string | 29K | 31K (+6.5%) | **29K** | +0.3% | -5.6% |
+
+All benchmarks that originally regressed are now **at or above master**. Closure is
+41% faster than master, method matches master exactly, lexical is 22% faster, and
+global is 5% faster. The closure and lexical improvements come from eliminating
+unnecessary RegexState save/restore for subroutines that don't use regex — an
+overhead that existed on master too (via separate ThreadLocals) but was masked by
+lower per-lookup cost. The regex benchmark shows a small regression (~9%) from
+ThreadLocal routing overhead in regex-heavy code paths.
+
+#### Remaining Optimization Opportunities (not yet pursued)
+
+These are lower-priority since the main goal (closure/method within 10%) is exceeded:
+
+| Option | Effort | Expected Impact | Notes |
+|--------|--------|-----------------|-------|
+| Pass `PerlRuntime rt` from static apply to instance apply | Low | Eliminates 1 of 2 remaining lookups per sub call | Changes method signatures |
+| Cache warning bits on RuntimeCode field | Low | Avoids ConcurrentHashMap lookup per call | `getWarningBitsForCode()` in profile |
+| Batch RuntimeRegex field access in match methods | Medium | Eliminates ~10-15 lookups per regex match | Profile showed many individual accessors in RuntimeRegex.java; may help the -9% regex regression |
+| DynamicVariableManager.variableStack() caching | Low | 1 lookup per call eliminated | 10 samples in profile |
+
+Note: "Lazy regex state save (skip when sub doesn't use regex)" was listed here previously
+and has been implemented as Tier 2d above.
+
### Open Questions
- `runtimeEvalCounter` and `nextCallsiteId` remain static (shared across runtimes) —
acceptable for unique ID generation but may want per-runtime counters in future
diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java
index fee599e5f..f9ddf67df 100644
--- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java
+++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java
@@ -3,6 +3,9 @@
import org.perlonjava.backend.jvm.EmitterContext;
import org.perlonjava.backend.jvm.EmitterMethodCreator;
+import org.perlonjava.backend.jvm.InterpreterFallbackException;
+import org.perlonjava.backend.jvm.JavaClassInfo;
+import org.perlonjava.backend.jvm.JvmClosureTemplate;
import org.perlonjava.frontend.analysis.ConstantFoldingVisitor;
import org.perlonjava.frontend.analysis.FindDeclarationVisitor;
import org.perlonjava.frontend.analysis.RegexUsageDetector;
@@ -4909,6 +4912,10 @@ private void visitNamedSubroutine(SubroutineNode node) {
*
* Compiles the subroutine body to bytecode with closure support.
* Anonymous subs capture lexical variables from the enclosing scope.
+ *
+ * When compiled inside eval STRING with an EmitterContext available,
+ * attempts JVM compilation first for better runtime performance.
+ * Falls back to interpreter bytecode if JVM compilation fails.
*/
private void visitAnonymousSubroutine(SubroutineNode node) {
// Step 1: Collect closure variables.
@@ -4924,7 +4931,141 @@ private void visitAnonymousSubroutine(SubroutineNode node) {
closureCapturedVarNames.addAll(closureVarNames);
- // Step 3: Create a new BytecodeCompiler for the subroutine body
+ // Step 2: Try JVM compilation first if we have an EmitterContext (eval STRING path)
+ // Skip JVM attempt for defer blocks and map/grep blocks which have special control flow
+ Boolean isDeferBlock = (Boolean) node.getAnnotation("isDeferBlock");
+ Boolean isMapGrepBlock = (Boolean) node.getAnnotation("isMapGrepBlock");
+ boolean skipJvm = (isDeferBlock != null && isDeferBlock)
+ || (isMapGrepBlock != null && isMapGrepBlock);
+
+ if (this.emitterContext != null && !skipJvm) {
+ try {
+ emitJvmAnonymousSub(node, closureVarNames, closureVarIndices);
+ return; // JVM compilation succeeded
+ } catch (Exception e) {
+ // JVM compilation failed, fall through to interpreter path
+ if (System.getenv("JPERL_SHOW_FALLBACK") != null) {
+ System.err.println("JVM compilation failed for anonymous sub in eval STRING, using interpreter: "
+ + e.getClass().getSimpleName() + ": " + e.getMessage());
+ }
+ }
+ }
+
+ // Step 3: Interpreter compilation (existing path)
+ emitInterpretedAnonymousSub(node, closureVarNames, closureVarIndices);
+ }
+
+ /**
+ * Attempt to compile an anonymous sub body to JVM bytecode.
+ * Creates an EmitterContext, calls EmitterMethodCreator.createClassWithMethod(),
+ * and emits interpreter opcodes to instantiate the JVM class at runtime.
+ */
+ private void emitJvmAnonymousSub(SubroutineNode node,
+ List closureVarNames,
+ List closureVarIndices) {
+ // Build a ScopedSymbolTable for the sub body with captured variables
+ ScopedSymbolTable newSymbolTable = new ScopedSymbolTable();
+ newSymbolTable.enterScope();
+
+ // Add reserved variables first to occupy slots 0-2
+ // EmitterMethodCreator skips these (skipVariables=3) but they must be present
+ // in the symbol table to keep captured variable indices aligned at 3+
+ newSymbolTable.addVariable("this", "", getCurrentPackage(), null);
+ newSymbolTable.addVariable("@_", "", getCurrentPackage(), null);
+ newSymbolTable.addVariable("wantarray", "", getCurrentPackage(), null);
+
+ // Add captured variables to the symbol table
+ // They will be at indices 3, 4, 5, ... (after this/@_/wantarray)
+ for (String varName : closureVarNames) {
+ newSymbolTable.addVariable(varName, "my", getCurrentPackage(), null);
+ }
+
+ // Copy package and pragma flags from the current BytecodeCompiler state
+ newSymbolTable.setCurrentPackage(getCurrentPackage(), symbolTable.currentPackageIsClass());
+ newSymbolTable.strictOptionsStack.pop();
+ newSymbolTable.strictOptionsStack.push(symbolTable.strictOptionsStack.peek());
+ newSymbolTable.featureFlagsStack.pop();
+ newSymbolTable.featureFlagsStack.push(symbolTable.featureFlagsStack.peek());
+ newSymbolTable.warningFlagsStack.pop();
+ newSymbolTable.warningFlagsStack.push((java.util.BitSet) symbolTable.warningFlagsStack.peek().clone());
+ newSymbolTable.warningFatalStack.pop();
+ newSymbolTable.warningFatalStack.push((java.util.BitSet) symbolTable.warningFatalStack.peek().clone());
+ newSymbolTable.warningDisabledStack.pop();
+ newSymbolTable.warningDisabledStack.push((java.util.BitSet) symbolTable.warningDisabledStack.peek().clone());
+
+ // Reset variable index past the captured variables
+ String[] newEnv = newSymbolTable.getVariableNames();
+ int currentVarIndex = newSymbolTable.getCurrentLocalVariableIndex();
+ int resetTo = Math.max(newEnv.length, currentVarIndex);
+ newSymbolTable.resetLocalVariableIndex(resetTo);
+
+ // Create EmitterContext for JVM compilation
+ JavaClassInfo newJavaClassInfo = new JavaClassInfo();
+ EmitterContext subCtx = new EmitterContext(
+ newJavaClassInfo,
+ newSymbolTable,
+ null, // mv - will be set by EmitterMethodCreator
+ null, // cw - will be set by EmitterMethodCreator
+ RuntimeContextType.RUNTIME,
+ true,
+ this.errorUtil,
+ this.emitterContext.compilerOptions,
+ new RuntimeArray()
+ );
+
+ // Try JVM compilation - may throw InterpreterFallbackException or other exceptions
+ Class> generatedClass = EmitterMethodCreator.createClassWithMethod(
+ subCtx, node.block, false);
+
+ // Cache the generated class
+ RuntimeCode.getAnonSubs().put(subCtx.javaClassInfo.javaClassName, generatedClass);
+
+ // Emit interpreter opcodes to create the code reference at runtime
+ int codeReg = allocateRegister();
+ String packageName = getCurrentPackage();
+
+ if (closureVarIndices.isEmpty()) {
+ // No closures - instantiate JVM class at compile time
+ JvmClosureTemplate template = new JvmClosureTemplate(
+ generatedClass, node.prototype, packageName);
+ RuntimeScalar codeScalar = template.instantiateNoClosure();
+
+ // Handle attributes
+ if (node.attributes != null && !node.attributes.isEmpty() && packageName != null) {
+ RuntimeCode code = (RuntimeCode) codeScalar.value;
+ code.attributes = node.attributes;
+ Attributes.runtimeDispatchModifyCodeAttributes(packageName, codeScalar);
+ }
+
+ int constIdx = addToConstantPool(codeScalar);
+ emit(Opcodes.LOAD_CONST);
+ emitReg(codeReg);
+ emit(constIdx);
+ } else {
+ // Has closures - store JvmClosureTemplate in constant pool
+ // CREATE_CLOSURE opcode handles both InterpretedCode and JvmClosureTemplate
+ JvmClosureTemplate template = new JvmClosureTemplate(
+ generatedClass, node.prototype, packageName);
+ int templateIdx = addToConstantPool(template);
+ emit(Opcodes.CREATE_CLOSURE);
+ emitReg(codeReg);
+ emit(templateIdx);
+ emit(closureVarIndices.size());
+ for (int regIdx : closureVarIndices) {
+ emit(regIdx);
+ }
+ }
+
+ lastResultReg = codeReg;
+ }
+
+ /**
+ * Compile an anonymous sub to InterpretedCode (the fallback/default path).
+ * This is the original implementation of visitAnonymousSubroutine.
+ */
+ private void emitInterpretedAnonymousSub(SubroutineNode node,
+ List closureVarNames,
+ List closureVarIndices) {
// Build a variable registry from current scope to pass to sub-compiler
// This allows nested closures to see grandparent scope variables
Map parentRegistry = new HashMap<>();
diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java
index 91e81e872..830e10319 100644
--- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java
+++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java
@@ -1,5 +1,6 @@
package org.perlonjava.backend.bytecode;
+import org.perlonjava.backend.jvm.JvmClosureTemplate;
import org.perlonjava.runtime.operators.*;
import org.perlonjava.runtime.perlmodule.Attributes;
import org.perlonjava.runtime.regex.RuntimeRegex;
@@ -888,15 +889,15 @@ public static int executeMatchRegexNot(int[] bytecode, int pc, RuntimeBase[] reg
/**
* Execute create closure operation.
* Format: CREATE_CLOSURE rd template_idx num_captures reg1 reg2 ...
+ *
+ * Supports both InterpretedCode templates (interpreter-compiled subs)
+ * and JvmClosureTemplate (JVM-compiled subs from eval STRING).
*/
public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] registers, InterpretedCode code) {
int rd = bytecode[pc++];
int templateIdx = bytecode[pc++];
int numCaptures = bytecode[pc++];
- // Get the template InterpretedCode from constants
- InterpretedCode template = (InterpretedCode) code.constants[templateIdx];
-
// Capture the current register values
RuntimeBase[] capturedVars = new RuntimeBase[numCaptures];
for (int i = 0; i < numCaptures; i++) {
@@ -904,19 +905,27 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg
capturedVars[i] = registers[captureReg];
}
- // Create a new InterpretedCode with the captured variables
- InterpretedCode closureCode = template.withCapturedVars(capturedVars);
-
- // Wrap in RuntimeScalar and set __SUB__ for self-reference
- RuntimeScalar codeRef = new RuntimeScalar(closureCode);
- closureCode.__SUB__ = codeRef;
- registers[rd] = codeRef;
+ Object template = code.constants[templateIdx];
- // Dispatch MODIFY_CODE_ATTRIBUTES for anonymous subs with non-builtin attributes
- // Pass isClosure=true since CREATE_CLOSURE always creates a closure
- if (closureCode.attributes != null && !closureCode.attributes.isEmpty()
- && closureCode.packageName != null) {
- Attributes.runtimeDispatchModifyCodeAttributes(closureCode.packageName, codeRef, true);
+ if (template instanceof JvmClosureTemplate jvmTemplate) {
+ // JVM-compiled closure: instantiate the generated class with captured variables
+ registers[rd] = jvmTemplate.instantiate(capturedVars);
+ } else {
+ // InterpretedCode closure: create a new copy with captured variables
+ InterpretedCode interpTemplate = (InterpretedCode) template;
+ InterpretedCode closureCode = interpTemplate.withCapturedVars(capturedVars);
+
+ // Wrap in RuntimeScalar and set __SUB__ for self-reference
+ RuntimeScalar codeRef = new RuntimeScalar(closureCode);
+ closureCode.__SUB__ = codeRef;
+ registers[rd] = codeRef;
+
+ // Dispatch MODIFY_CODE_ATTRIBUTES for anonymous subs with non-builtin attributes
+ // Pass isClosure=true since CREATE_CLOSURE always creates a closure
+ if (closureCode.attributes != null && !closureCode.attributes.isEmpty()
+ && closureCode.packageName != null) {
+ Attributes.runtimeDispatchModifyCodeAttributes(closureCode.packageName, codeRef, true);
+ }
}
return pc;
}
diff --git a/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java b/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java
index 2354ff446..d8f389afb 100644
--- a/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java
+++ b/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java
@@ -4,6 +4,7 @@
import java.util.ArrayList;
import java.util.HashMap;
+import java.util.List;
import java.util.Map;
import java.util.TreeMap;
@@ -13,32 +14,45 @@
* resolution for stack traces at runtime.
*/
public class ByteCodeSourceMapper {
- // Maps source files to their debug information
- private static final Map sourceFiles = new HashMap<>();
- // Pool of package names to optimize memory usage
- private static final ArrayList packageNamePool = new ArrayList<>();
- private static final Map packageNameToId = new HashMap<>();
-
- // Pool of file names to optimize memory usage
- private static final ArrayList fileNamePool = new ArrayList<>();
- private static final Map fileNameToId = new HashMap<>();
+ /**
+ * Holds all mutable source-mapper state. One instance lives in each PerlRuntime
+ * for multiplicity thread-safety.
+ */
+ public static class State {
+ // Maps source files to their debug information
+ public final Map sourceFiles = new HashMap<>();
+
+ // Pool of package names to optimize memory usage
+ public final List packageNamePool = new ArrayList<>();
+ public final Map packageNameToId = new HashMap<>();
+
+ // Pool of file names to optimize memory usage
+ public final List fileNamePool = new ArrayList<>();
+ public final Map fileNameToId = new HashMap<>();
+
+ // Pool of subroutine names to optimize memory usage
+ public final List subroutineNamePool = new ArrayList<>();
+ public final Map subroutineNameToId = new HashMap<>();
+
+ public void resetAll() {
+ sourceFiles.clear();
+ packageNamePool.clear();
+ packageNameToId.clear();
+ fileNamePool.clear();
+ fileNameToId.clear();
+ subroutineNamePool.clear();
+ subroutineNameToId.clear();
+ }
+ }
- // Pool of subroutine names to optimize memory usage
- private static final ArrayList subroutineNamePool = new ArrayList<>();
- private static final Map subroutineNameToId = new HashMap<>();
+ /** Returns the current PerlRuntime's source-mapper state. */
+ private static State state() {
+ return org.perlonjava.runtime.runtimetypes.PerlRuntime.current().sourceMapperState;
+ }
public static void resetAll() {
- sourceFiles.clear();
-
- packageNamePool.clear();
- packageNameToId.clear();
-
- fileNamePool.clear();
- fileNameToId.clear();
-
- subroutineNamePool.clear();
- subroutineNameToId.clear();
+ state().resetAll();
}
/**
@@ -48,9 +62,10 @@ public static void resetAll() {
* @return The unique identifier for the package
*/
private static int getOrCreatePackageId(String packageName) {
- return packageNameToId.computeIfAbsent(packageName, name -> {
- packageNamePool.add(name);
- return packageNamePool.size() - 1;
+ State s = state();
+ return s.packageNameToId.computeIfAbsent(packageName, name -> {
+ s.packageNamePool.add(name);
+ return s.packageNamePool.size() - 1;
});
}
@@ -61,9 +76,10 @@ private static int getOrCreatePackageId(String packageName) {
* @return The unique identifier for the file
*/
private static int getOrCreateFileId(String fileName) {
- return fileNameToId.computeIfAbsent(fileName, name -> {
- fileNamePool.add(name);
- return fileNamePool.size() - 1;
+ State s = state();
+ return s.fileNameToId.computeIfAbsent(fileName, name -> {
+ s.fileNamePool.add(name);
+ return s.fileNamePool.size() - 1;
});
}
@@ -74,9 +90,10 @@ private static int getOrCreateFileId(String fileName) {
* @return The unique identifier for the subroutine name
*/
private static int getOrCreateSubroutineId(String subroutineName) {
- return subroutineNameToId.computeIfAbsent(subroutineName, name -> {
- subroutineNamePool.add(name);
- return subroutineNamePool.size() - 1;
+ State s = state();
+ return s.subroutineNameToId.computeIfAbsent(subroutineName, name -> {
+ s.subroutineNamePool.add(name);
+ return s.subroutineNamePool.size() - 1;
});
}
@@ -87,7 +104,7 @@ private static int getOrCreateSubroutineId(String subroutineName) {
*/
static void setDebugInfoFileName(EmitterContext ctx) {
int fileId = getOrCreateFileId(ctx.compilerOptions.fileName);
- sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new);
+ state().sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new);
ctx.cw.visitSource(ctx.compilerOptions.fileName, null);
}
@@ -126,13 +143,14 @@ static void setDebugInfoLineNumber(EmitterContext ctx, int tokenIndex) {
* @param tokenIndex The index of the token in the source code
*/
public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) {
+ State s = state();
// Use the ORIGINAL filename (compile-time) for the key, not the #line-adjusted one.
// This is because JVM stack traces report the original filename from visitSource().
// The #line-adjusted filename is stored separately in LineInfo for caller() reporting.
int fileId = getOrCreateFileId(ctx.compilerOptions.fileName);
// Get or create the SourceFileInfo object for the file
- SourceFileInfo info = sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new);
+ SourceFileInfo info = s.sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new);
// Get current subroutine name (empty string for main code)
String subroutineName = ctx.symbolTable.getCurrentSubroutine();
@@ -156,8 +174,8 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) {
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG saveSourceLocation: SKIP (exists) file=" + ctx.compilerOptions.fileName
+ " tokenIndex=" + tokenIndex + " existingLine=" + existingEntry.lineNumber()
- + " existingPkg=" + packageNamePool.get(existingEntry.packageNameId())
- + " existingSourceFile=" + fileNamePool.get(existingEntry.sourceFileNameId()));
+ + " existingPkg=" + s.packageNamePool.get(existingEntry.packageNameId())
+ + " existingSourceFile=" + s.fileNamePool.get(existingEntry.sourceFileNameId()));
}
return;
}
@@ -180,7 +198,7 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) {
// Look for nearby entry (within 50 tokens) that has #line-adjusted filename
var nearbyEntry = info.tokenToLineInfo.floorEntry(tokenIndex);
if (nearbyEntry != null && (tokenIndex - nearbyEntry.getKey()) < 50) {
- String nearbySourceFile = fileNamePool.get(nearbyEntry.getValue().sourceFileNameId());
+ String nearbySourceFile = s.fileNamePool.get(nearbyEntry.getValue().sourceFileNameId());
if (!nearbySourceFile.equals(ctx.compilerOptions.fileName)) {
// Nearby entry has #line-adjusted filename - inherit it
sourceFileName = nearbySourceFile;
@@ -220,7 +238,8 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) {
* @return The package name at that location, or null if not found
*/
public static String getPackageAtLocation(String fileName, int tokenIndex) {
- int fileId = fileNameToId.getOrDefault(fileName, -1);
+ State s = state();
+ int fileId = s.fileNameToId.getOrDefault(fileName, -1);
if (fileId == -1) {
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG getPackageAtLocation: NO FILE ID for fileName=" + fileName);
@@ -228,7 +247,7 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) {
return null;
}
- SourceFileInfo info = sourceFiles.get(fileId);
+ SourceFileInfo info = s.sourceFiles.get(fileId);
if (info == null) {
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG getPackageAtLocation: NO SOURCE INFO for fileName=" + fileName + " fileId=" + fileId);
@@ -244,7 +263,7 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) {
return null;
}
- String pkg = packageNamePool.get(entry.getValue().packageNameId());
+ String pkg = s.packageNamePool.get(entry.getValue().packageNameId());
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG getPackageAtLocation: fileName=" + fileName + " tokenIndex=" + tokenIndex
+ " foundTokenIndex=" + entry.getKey() + " pkg=" + pkg);
@@ -259,10 +278,11 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) {
* @return The corresponding source code location
*/
public static SourceLocation parseStackTraceElement(StackTraceElement element, HashMap locationToClassName) {
- int fileId = fileNameToId.getOrDefault(element.getFileName(), -1);
+ State s = state();
+ int fileId = s.fileNameToId.getOrDefault(element.getFileName(), -1);
int tokenIndex = element.getLineNumber();
- SourceFileInfo info = sourceFiles.get(fileId);
+ SourceFileInfo info = s.sourceFiles.get(fileId);
if (info == null) {
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG parseStackTraceElement: NO INFO for file=" + element.getFileName() + " fileId=" + fileId);
@@ -282,9 +302,9 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H
LineInfo lineInfo = entry.getValue();
// Get the #line directive-adjusted source filename for caller() reporting
- String sourceFileName = fileNamePool.get(lineInfo.sourceFileNameId());
+ String sourceFileName = s.fileNamePool.get(lineInfo.sourceFileNameId());
int lineNumber = lineInfo.lineNumber();
- String packageName = packageNamePool.get(lineInfo.packageNameId());
+ String packageName = s.packageNamePool.get(lineInfo.packageNameId());
// FIX: If the found entry's sourceFile equals the original file (no #line applied),
// check for nearby entries that have a #line-adjusted filename.
@@ -297,7 +317,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H
boolean foundLineDirective = false;
while (lowerEntry != null && (entry.getKey() - lowerEntry.getKey()) < 300) {
- String lowerSourceFile = fileNamePool.get(lowerEntry.getValue().sourceFileNameId());
+ String lowerSourceFile = s.fileNamePool.get(lowerEntry.getValue().sourceFileNameId());
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG parseStackTraceElement: checking lowerEntry key=" + lowerEntry.getKey() + " sourceFile=" + lowerSourceFile + " line=" + lowerEntry.getValue().lineNumber() + " entryKey=" + entry.getKey());
}
@@ -327,7 +347,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H
int estimatedExtraLines = tokenDistFromLineDirective / 6;
lineNumber = lowerEntry.getValue().lineNumber() + estimatedExtraLines;
- packageName = packageNamePool.get(lowerEntry.getValue().packageNameId());
+ packageName = s.packageNamePool.get(lowerEntry.getValue().packageNameId());
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG parseStackTraceElement: APPLYING lowerEntry sourceFile=" + sourceFileName + " adjustedLine=" + lineNumber + " tokenDist=" + tokenDistFromLineDirective);
}
@@ -342,7 +362,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H
int currentKey = entry.getKey();
var higherEntry = info.tokenToLineInfo.higherEntry(currentKey);
while (higherEntry != null && (higherEntry.getKey() - entry.getKey()) < 50) {
- String higherSourceFile = fileNamePool.get(higherEntry.getValue().sourceFileNameId());
+ String higherSourceFile = s.fileNamePool.get(higherEntry.getValue().sourceFileNameId());
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG parseStackTraceElement: checking higherEntry key=" + higherEntry.getKey() + " sourceFile=" + higherSourceFile + " entryKey=" + entry.getKey() + " currentKey=" + currentKey);
}
@@ -352,7 +372,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H
lineNumber = higherEntry.getValue().lineNumber() -
(higherEntry.getKey() - entry.getKey()); // Approximate adjustment
if (lineNumber < 1) lineNumber = 1;
- packageName = packageNamePool.get(higherEntry.getValue().packageNameId());
+ packageName = s.packageNamePool.get(higherEntry.getValue().packageNameId());
if (System.getenv("DEBUG_CALLER") != null) {
System.err.println("DEBUG parseStackTraceElement: APPLYING higherEntry sourceFile=" + sourceFileName + " adjustedLine=" + lineNumber);
}
@@ -377,7 +397,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H
}
// Retrieve subroutine name
- String subroutineName = subroutineNamePool.get(lineInfo.subroutineNameId());
+ String subroutineName = s.subroutineNamePool.get(lineInfo.subroutineNameId());
// If subroutine name is empty string (main code), convert to null
if (subroutineName != null && subroutineName.isEmpty()) {
subroutineName = null;
diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java
index 41abcdc54..d279bdad1 100644
--- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java
+++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java
@@ -14,6 +14,7 @@
import org.perlonjava.backend.bytecode.Disassemble;
import org.perlonjava.backend.bytecode.InterpretedCode;
import org.perlonjava.frontend.analysis.EmitterVisitor;
+import org.perlonjava.frontend.analysis.RegexUsageDetector;
import org.perlonjava.frontend.analysis.TempLocalCountVisitor;
import org.perlonjava.frontend.astnode.BlockNode;
import org.perlonjava.frontend.astnode.CompilerFlagNode;
@@ -629,8 +630,14 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean
// Store dynamicIndex so goto &sub can access it for cleanup before tail call
ctx.javaClassInfo.dynamicLevelSlot = dynamicIndex;
- mv.visitMethodInsn(Opcodes.INVOKESTATIC,
- "org/perlonjava/runtime/runtimetypes/RegexState", "save", "()V", false);
+ // Only save/restore regex state if the subroutine body contains regex
+ // operations (or eval STRING which may introduce them at runtime).
+ // Subroutines without regex don't modify regex state, and callees
+ // that use regex do their own save/restore.
+ if (RegexUsageDetector.containsRegexOperation(ast)) {
+ mv.visitMethodInsn(Opcodes.INVOKESTATIC,
+ "org/perlonjava/runtime/runtimetypes/RegexState", "save", "()V", false);
+ }
// Store the computed RuntimeList return value in a dedicated local slot.
// This keeps the operand stack empty at join labels (endCatch), avoiding
diff --git a/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java
new file mode 100644
index 000000000..bdb3c134b
--- /dev/null
+++ b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java
@@ -0,0 +1,131 @@
+package org.perlonjava.backend.jvm;
+
+import org.perlonjava.runtime.runtimetypes.PerlSubroutine;
+import org.perlonjava.runtime.runtimetypes.RuntimeBase;
+import org.perlonjava.runtime.runtimetypes.RuntimeCode;
+import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+
+/**
+ * Template for creating JVM-compiled closures from interpreter bytecode.
+ *
+ * When an anonymous sub inside eval STRING is compiled to a JVM class
+ * (instead of InterpretedCode), this template holds the generated class
+ * and knows how to instantiate it with captured variables at runtime.
+ *
+ * The CREATE_CLOSURE interpreter opcode checks for this type in the
+ * constant pool and delegates to {@link #instantiate(RuntimeBase[])}
+ * to create the closure.
+ */
+public class JvmClosureTemplate {
+
+ /**
+ * The JVM-compiled class implementing PerlSubroutine
+ */
+ public final Class> generatedClass;
+
+ /**
+ * Cached constructor for the generated class.
+ * Takes captured variables as typed parameters (RuntimeScalar, RuntimeArray, RuntimeHash).
+ */
+ public final Constructor> constructor;
+
+ /**
+ * Perl prototype string (e.g., "$$@"), or null
+ */
+ public final String prototype;
+
+ /**
+ * Package where the sub was compiled (CvSTASH)
+ */
+ public final String packageName;
+
+ /**
+ * Creates a JvmClosureTemplate for a generated class.
+ *
+ * @param generatedClass the JVM class implementing PerlSubroutine
+ * @param prototype Perl prototype string, or null
+ * @param packageName package where the sub was compiled
+ */
+ public JvmClosureTemplate(Class> generatedClass, String prototype, String packageName) {
+ this.generatedClass = generatedClass;
+ this.prototype = prototype;
+ this.packageName = packageName;
+
+ // Cache the constructor - there should be exactly one with closure parameters
+ Constructor>[] constructors = generatedClass.getDeclaredConstructors();
+ // Find the constructor that takes the most parameters (the closure one)
+ Constructor> best = null;
+ for (Constructor> c : constructors) {
+ if (best == null || c.getParameterCount() > best.getParameterCount()) {
+ best = c;
+ }
+ }
+ this.constructor = best;
+ }
+
+ /**
+ * Instantiate the JVM-compiled closure with captured variables.
+ *
+ * This is called at runtime by the interpreter's CREATE_CLOSURE opcode
+ * when it encounters a JvmClosureTemplate in the constant pool.
+ *
+ * The cost of reflection here is amortized: this is called once per
+ * closure creation (each time the sub {} expression is evaluated),
+ * not per call to the closure.
+ *
+ * @param capturedVars the captured variable values from the interpreter's registers
+ * @return a RuntimeScalar wrapping the RuntimeCode for this closure
+ */
+ public RuntimeScalar instantiate(RuntimeBase[] capturedVars) {
+ try {
+ // Convert RuntimeBase[] to Object[] for reflection
+ Object[] args = new Object[capturedVars.length];
+ System.arraycopy(capturedVars, 0, args, 0, capturedVars.length);
+
+ // Instantiate the JVM class with captured variables as constructor args
+ PerlSubroutine instance = (PerlSubroutine) constructor.newInstance(args);
+
+ // Create RuntimeCode wrapping and set __SUB__
+ RuntimeCode code = new RuntimeCode(instance, prototype);
+ if (packageName != null) {
+ code.packageName = packageName;
+ }
+ RuntimeScalar codeRef = new RuntimeScalar(code);
+
+ // Set __SUB__ on the generated class instance for self-reference
+ Field subField = generatedClass.getDeclaredField("__SUB__");
+ subField.set(instance, codeRef);
+
+ return codeRef;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to instantiate JVM closure: " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * Instantiate the JVM-compiled sub with no captured variables.
+ *
+ * @return a RuntimeScalar wrapping the RuntimeCode
+ */
+ public RuntimeScalar instantiateNoClosure() {
+ try {
+ PerlSubroutine instance = (PerlSubroutine) generatedClass.getDeclaredConstructor().newInstance();
+
+ RuntimeCode code = new RuntimeCode(instance, prototype);
+ if (packageName != null) {
+ code.packageName = packageName;
+ }
+ RuntimeScalar codeRef = new RuntimeScalar(code);
+
+ Field subField = generatedClass.getDeclaredField("__SUB__");
+ subField.set(instance, codeRef);
+
+ return codeRef;
+ } catch (Exception e) {
+ throw new RuntimeException("Failed to instantiate JVM sub: " + e.getMessage(), e);
+ }
+ }
+}
diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java
index 4f178dd6f..5f40c1f5d 100644
--- a/src/main/java/org/perlonjava/core/Configuration.java
+++ b/src/main/java/org/perlonjava/core/Configuration.java
@@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
- public static final String gitCommitId = "c30eeb487";
+ public static final String gitCommitId = "85dafcaaf";
/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
@@ -48,7 +48,7 @@ public final class Configuration {
* Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at"
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
- public static final String buildTimestamp = "Apr 10 2026 15:20:15";
+ public static final String buildTimestamp = "Apr 10 2026 22:38:49";
// Prevent instantiation
private Configuration() {
diff --git a/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java b/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java
index dd7814604..e9227b528 100644
--- a/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java
+++ b/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java
@@ -21,10 +21,11 @@
public class RegexUsageDetector {
/**
- * Unary operators that perform regex matching/substitution.
+ * Unary operators that perform regex matching/substitution,
+ * or that may dynamically introduce regex operations (eval STRING).
*/
private static final java.util.Set REGEX_OPERATORS =
- java.util.Set.of("matchRegex", "replaceRegex");
+ java.util.Set.of("matchRegex", "replaceRegex", "eval");
/**
* Binary operators that perform regex matching (=~, !~) or use regex internally (split).
*/
diff --git a/src/main/java/org/perlonjava/runtime/HintHashRegistry.java b/src/main/java/org/perlonjava/runtime/HintHashRegistry.java
index df21571dc..8d5106b1f 100644
--- a/src/main/java/org/perlonjava/runtime/HintHashRegistry.java
+++ b/src/main/java/org/perlonjava/runtime/HintHashRegistry.java
@@ -2,6 +2,7 @@
import org.perlonjava.runtime.runtimetypes.GlobalContext;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
+import org.perlonjava.runtime.runtimetypes.PerlRuntime;
import org.perlonjava.runtime.runtimetypes.RuntimeHash;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
@@ -22,6 +23,9 @@
* 2. Per-call-site tracking using snapshot IDs: registerSnapshot() captures %^H
* at compile time, and setCallSiteHintHashId()/pushCallerHintHash()/
* getCallerHintHashAtFrame() bridge compile-time state to runtime caller()[10].
+ *
+ * Per-call-site stacks are per-PerlRuntime instance fields (accessed via
+ * PerlRuntime.current()) instead of separate ThreadLocals.
*/
public class HintHashRegistry {
@@ -38,15 +42,6 @@ public class HintHashRegistry {
new ConcurrentHashMap<>();
private static final AtomicInteger nextSnapshotId = new AtomicInteger(0);
- // ThreadLocal tracking the current call site's snapshot ID.
- // Updated at runtime from emitted bytecode.
- private static final ThreadLocal callSiteSnapshotId =
- ThreadLocal.withInitial(() -> 0);
-
- // ThreadLocal stack saving caller's snapshot ID across subroutine calls.
- private static final ThreadLocal> callerSnapshotIdStack =
- ThreadLocal.withInitial(ArrayDeque::new);
-
// ---- Compile-time %^H scoping ----
/**
@@ -110,7 +105,7 @@ public static int snapshotCurrentHintHash() {
* @param id the snapshot ID (0 = empty/no hints)
*/
public static void setCallSiteHintHashId(int id) {
- callSiteSnapshotId.set(id);
+ PerlRuntime.current().hintCallSiteSnapshotId = id;
}
/**
@@ -120,11 +115,11 @@ public static void setCallSiteHintHashId(int id) {
* Called by RuntimeCode.apply() before entering a subroutine.
*/
public static void pushCallerHintHash() {
- int currentId = callSiteSnapshotId.get();
- callerSnapshotIdStack.get().push(currentId);
+ PerlRuntime rt = PerlRuntime.current();
+ rt.hintCallerSnapshotIdStack.push(rt.hintCallSiteSnapshotId);
// Reset callsite for the callee - it should not inherit the caller's hints.
// The callee's own CompilerFlagNodes will set the correct ID if needed.
- callSiteSnapshotId.set(0);
+ rt.hintCallSiteSnapshotId = 0;
}
/**
@@ -133,12 +128,10 @@ public static void pushCallerHintHash() {
* Called by RuntimeCode.apply() after a subroutine returns.
*/
public static void popCallerHintHash() {
- Deque stack = callerSnapshotIdStack.get();
+ PerlRuntime rt = PerlRuntime.current();
+ Deque stack = rt.hintCallerSnapshotIdStack;
if (!stack.isEmpty()) {
- int restoredId = stack.pop();
- // Restore the callsite ID so eval STRING and subsequent code
- // see the correct hint hash, not one clobbered by the callee.
- callSiteSnapshotId.set(restoredId);
+ rt.hintCallSiteSnapshotId = stack.pop();
}
}
@@ -150,7 +143,7 @@ public static void popCallerHintHash() {
* @return The hint hash map, or null if not available
*/
public static Map getCallerHintHashAtFrame(int frame) {
- Deque stack = callerSnapshotIdStack.get();
+ Deque stack = PerlRuntime.current().hintCallerSnapshotIdStack;
if (stack.isEmpty()) {
return null;
}
@@ -172,7 +165,7 @@ public static Map getCallerHintHashAtFrame(int frame) {
* @return the hint hash map, or null if empty/not set
*/
public static Map getCurrentCallSiteHintHash() {
- int id = callSiteSnapshotId.get();
+ int id = PerlRuntime.current().hintCallSiteSnapshotId;
if (id == 0) return null;
return snapshotRegistry.get(id);
}
@@ -185,7 +178,8 @@ public static void clear() {
compileTimeStack.clear();
snapshotRegistry.clear();
nextSnapshotId.set(0);
- callSiteSnapshotId.set(0);
- callerSnapshotIdStack.get().clear();
+ PerlRuntime rt = PerlRuntime.current();
+ rt.hintCallSiteSnapshotId = 0;
+ rt.hintCallerSnapshotIdStack.clear();
}
}
diff --git a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java
index 7e6a4898f..02c259e19 100644
--- a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java
+++ b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java
@@ -1,11 +1,12 @@
package org.perlonjava.runtime;
-import java.util.ArrayDeque;
import java.util.Deque;
+import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.perlonjava.runtime.runtimetypes.GlobalContext;
import org.perlonjava.runtime.runtimetypes.GlobalVariable;
+import org.perlonjava.runtime.runtimetypes.PerlRuntime;
import org.perlonjava.runtime.runtimetypes.RuntimeHash;
import org.perlonjava.runtime.runtimetypes.RuntimeScalar;
@@ -20,54 +21,17 @@
*
* At runtime, caller() looks up warning bits by class name.
*
- * Additionally, a ThreadLocal stack tracks the "current" warning bits
- * for runtime code that needs to check FATAL warnings.
+ * Warning/hints stacks are per-PerlRuntime instance fields (accessed
+ * via PerlRuntime.current()) instead of separate ThreadLocals, reducing
+ * the number of ThreadLocal lookups per subroutine call.
*/
public class WarningBitsRegistry {
// Map from fully-qualified class name to warning bits string
+ // This is shared across runtimes (immutable after registration)
private static final ConcurrentHashMap registry =
new ConcurrentHashMap<>();
- // ThreadLocal stack of warning bits for the current execution context
- // This allows runtime code to find warning bits even at top-level (no subroutine frame)
- private static final ThreadLocal> currentBitsStack =
- ThreadLocal.withInitial(ArrayDeque::new);
-
- // ThreadLocal tracking the warning bits at the current call site.
- // Updated at runtime when 'use warnings' / 'no warnings' pragmas are encountered.
- // This provides per-statement warning bits (like Perl 5's per-COP bits).
- private static final ThreadLocal callSiteBits =
- ThreadLocal.withInitial(() -> null);
-
- // ThreadLocal stack saving caller's call-site bits across subroutine calls.
- // Each apply() pushes the current callSiteBits before calling the subroutine,
- // and pops it when the subroutine returns. This allows caller()[9] to return
- // the correct per-call-site warning bits.
- private static final ThreadLocal> callerBitsStack =
- ThreadLocal.withInitial(ArrayDeque::new);
-
- // ThreadLocal tracking the compile-time $^H (hints) at the current call site.
- // Updated at runtime when pragmas (use strict, etc.) are encountered.
- // This provides per-statement hints for caller()[8].
- private static final ThreadLocal callSiteHints =
- ThreadLocal.withInitial(() -> 0);
-
- // ThreadLocal stack saving caller's $^H hints across subroutine calls.
- // Mirrors callerBitsStack but for $^H instead of warning bits.
- private static final ThreadLocal> callerHintsStack =
- ThreadLocal.withInitial(ArrayDeque::new);
-
- // ThreadLocal tracking the compile-time %^H (hints hash) at the current call site.
- // Updated at runtime when pragmas modify %^H.
- // This provides per-statement hints hash for caller()[10].
- private static final ThreadLocal> callSiteHintHash =
- ThreadLocal.withInitial(java.util.HashMap::new);
-
- // ThreadLocal stack saving caller's %^H across subroutine calls.
- private static final ThreadLocal>> callerHintHashStack =
- ThreadLocal.withInitial(ArrayDeque::new);
-
/**
* Registers the warning bits for a class.
* Called at class load time (static initializer) for JVM backend,
@@ -104,7 +68,7 @@ public static String get(String className) {
*/
public static void pushCurrent(String bits) {
if (bits != null) {
- currentBitsStack.get().push(bits);
+ PerlRuntime.current().warningCurrentBitsStack.push(bits);
}
}
@@ -113,7 +77,7 @@ public static void pushCurrent(String bits) {
* Called when exiting a subroutine or code block.
*/
public static void popCurrent() {
- Deque stack = currentBitsStack.get();
+ Deque stack = PerlRuntime.current().warningCurrentBitsStack;
if (!stack.isEmpty()) {
stack.pop();
}
@@ -126,7 +90,7 @@ public static void popCurrent() {
* @return The current warning bits string, or null if stack is empty
*/
public static String getCurrent() {
- Deque stack = currentBitsStack.get();
+ Deque stack = PerlRuntime.current().warningCurrentBitsStack;
return stack.isEmpty() ? null : stack.peek();
}
@@ -136,13 +100,14 @@ public static String getCurrent() {
*/
public static void clear() {
registry.clear();
- currentBitsStack.get().clear();
- callSiteBits.remove();
- callerBitsStack.get().clear();
- callSiteHints.remove();
- callerHintsStack.get().clear();
- callSiteHintHash.get().clear();
- callerHintHashStack.get().clear();
+ PerlRuntime rt = PerlRuntime.current();
+ rt.warningCurrentBitsStack.clear();
+ rt.warningCallSiteBits = null;
+ rt.warningCallerBitsStack.clear();
+ rt.warningCallSiteHints = 0;
+ rt.warningCallerHintsStack.clear();
+ rt.warningCallSiteHintHash.clear();
+ rt.warningCallerHintHashStack.clear();
}
/**
@@ -153,7 +118,7 @@ public static void clear() {
* @param bits The warning bits string for the current call site
*/
public static void setCallSiteBits(String bits) {
- callSiteBits.set(bits);
+ PerlRuntime.current().warningCallSiteBits = bits;
}
/**
@@ -162,7 +127,7 @@ public static void setCallSiteBits(String bits) {
* @return The current call-site warning bits, or null if not set
*/
public static String getCallSiteBits() {
- return callSiteBits.get();
+ return PerlRuntime.current().warningCallSiteBits;
}
/**
@@ -171,8 +136,9 @@ public static String getCallSiteBits() {
* This preserves the caller's warning bits so caller()[9] can retrieve them.
*/
public static void pushCallerBits() {
- String bits = callSiteBits.get();
- callerBitsStack.get().push(bits != null ? bits : "");
+ PerlRuntime rt = PerlRuntime.current();
+ String bits = rt.warningCallSiteBits;
+ rt.warningCallerBitsStack.push(bits != null ? bits : "");
}
/**
@@ -180,7 +146,7 @@ public static void pushCallerBits() {
* Called by RuntimeCode.apply() after a subroutine returns.
*/
public static void popCallerBits() {
- Deque stack = callerBitsStack.get();
+ Deque stack = PerlRuntime.current().warningCallerBitsStack;
if (!stack.isEmpty()) {
stack.pop();
}
@@ -195,7 +161,7 @@ public static void popCallerBits() {
* @return The warning bits string, or null if not available
*/
public static String getCallerBitsAtFrame(int frame) {
- Deque stack = callerBitsStack.get();
+ Deque stack = PerlRuntime.current().warningCallerBitsStack;
if (stack.isEmpty()) {
return null;
}
@@ -214,7 +180,7 @@ public static String getCallerBitsAtFrame(int frame) {
* Returns the number of registered classes.
* Useful for debugging and testing.
*
- * @return The number of registered class → bits mappings
+ * @return The number of registered class -> bits mappings
*/
public static int size() {
return registry.size();
@@ -229,7 +195,7 @@ public static int size() {
* @param hints The $^H bitmask
*/
public static void setCallSiteHints(int hints) {
- callSiteHints.set(hints);
+ PerlRuntime.current().warningCallSiteHints = hints;
}
/**
@@ -238,7 +204,7 @@ public static void setCallSiteHints(int hints) {
* @return The current call-site $^H value
*/
public static int getCallSiteHints() {
- return callSiteHints.get();
+ return PerlRuntime.current().warningCallSiteHints;
}
/**
@@ -246,7 +212,8 @@ public static int getCallSiteHints() {
* Called by RuntimeCode.apply() before entering a subroutine.
*/
public static void pushCallerHints() {
- callerHintsStack.get().push(callSiteHints.get());
+ PerlRuntime rt = PerlRuntime.current();
+ rt.warningCallerHintsStack.push(rt.warningCallSiteHints);
}
/**
@@ -254,7 +221,7 @@ public static void pushCallerHints() {
* Called by RuntimeCode.apply() after a subroutine returns.
*/
public static void popCallerHints() {
- Deque stack = callerHintsStack.get();
+ Deque stack = PerlRuntime.current().warningCallerHintsStack;
if (!stack.isEmpty()) {
stack.pop();
}
@@ -269,7 +236,7 @@ public static void popCallerHints() {
* @return The $^H value, or -1 if not available
*/
public static int getCallerHintsAtFrame(int frame) {
- Deque stack = callerHintsStack.get();
+ Deque stack = PerlRuntime.current().warningCallerHintsStack;
if (stack.isEmpty()) {
return -1;
}
@@ -292,7 +259,7 @@ public static int getCallerHintsAtFrame(int frame) {
* @param hintHash A snapshot of the %^H hash elements
*/
public static void setCallSiteHintHash(java.util.Map hintHash) {
- callSiteHintHash.set(hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>());
+ PerlRuntime.current().warningCallSiteHintHash = hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>();
}
/**
@@ -309,7 +276,8 @@ public static void snapshotCurrentHintHash() {
* Called by RuntimeCode.apply() before entering a subroutine.
*/
public static void pushCallerHintHash() {
- callerHintHashStack.get().push(new java.util.HashMap<>(callSiteHintHash.get()));
+ PerlRuntime rt = PerlRuntime.current();
+ rt.warningCallerHintHashStack.push(new java.util.HashMap<>(rt.warningCallSiteHintHash));
}
/**
@@ -317,7 +285,7 @@ public static void pushCallerHintHash() {
* Called by RuntimeCode.apply() after a subroutine returns.
*/
public static void popCallerHintHash() {
- Deque> stack = callerHintHashStack.get();
+ Deque> stack = PerlRuntime.current().warningCallerHintHashStack;
if (!stack.isEmpty()) {
stack.pop();
}
@@ -332,7 +300,7 @@ public static void popCallerHintHash() {
* @return A copy of the %^H hash elements, or null if not available
*/
public static java.util.Map getCallerHintHashAtFrame(int frame) {
- Deque> stack = callerHintHashStack.get();
+ Deque> stack = PerlRuntime.current().warningCallerHintHashStack;
if (stack.isEmpty()) {
return null;
}
diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java
index 62ac1dcfc..2c7f5c267 100644
--- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java
+++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java
@@ -93,12 +93,13 @@ public static MROAlgorithm getPackageMRO(String packageName) {
* @return A list of class names in the order of method resolution.
*/
public static List linearizeHierarchy(String className) {
+ PerlRuntime rt = PerlRuntime.current();
// Check if ISA has changed and invalidate cache if needed
- if (hasIsaChanged(className)) {
- invalidateCacheForClass(className);
+ if (hasIsaChanged(className, rt)) {
+ invalidateCacheForClass(className, rt);
}
- Map> cache = getLinearizedClassesCache();
+ Map> cache = rt.linearizedClassesCache;
// Check cache first
List cached = cache.get(className);
if (cached != null) {
@@ -106,7 +107,7 @@ public static List linearizeHierarchy(String className) {
return new ArrayList<>(cached);
}
- MROAlgorithm mro = getPackageMRO(className);
+ MROAlgorithm mro = rt.packageMRO.getOrDefault(className, rt.currentMRO);
List result;
switch (mro) {
@@ -129,6 +130,10 @@ public static List linearizeHierarchy(String className) {
* Checks if the @ISA array for a class has changed since last cached.
*/
private static boolean hasIsaChanged(String className) {
+ return hasIsaChanged(className, PerlRuntime.current());
+ }
+
+ private static boolean hasIsaChanged(String className, PerlRuntime rt) {
RuntimeArray isaArray = GlobalVariable.getGlobalArray(className + "::ISA");
// Build current ISA list
@@ -140,7 +145,7 @@ private static boolean hasIsaChanged(String className) {
}
}
- Map> isCache = getIsaStateCache();
+ Map> isCache = rt.isaStateCache;
List cachedIsa = isCache.get(className);
// If ISA changed, update cache and return true
@@ -156,8 +161,12 @@ private static boolean hasIsaChanged(String className) {
* Invalidate cache for a specific class and its dependents.
*/
private static void invalidateCacheForClass(String className) {
- Map> linCache = getLinearizedClassesCache();
- Map mCache = getMethodCache();
+ invalidateCacheForClass(className, PerlRuntime.current());
+ }
+
+ private static void invalidateCacheForClass(String className, PerlRuntime rt) {
+ Map> linCache = rt.linearizedClassesCache;
+ Map mCache = rt.methodCache;
// Remove exact class and subclasses from linearization cache
linCache.remove(className);
@@ -175,10 +184,11 @@ private static void invalidateCacheForClass(String className) {
* This should be called whenever the class hierarchy or method definitions change.
*/
public static void invalidateCache() {
- getMethodCache().clear();
- getLinearizedClassesCache().clear();
- getOverloadContextCache().clear();
- getIsaStateCache().clear();
+ PerlRuntime rt = PerlRuntime.current();
+ rt.methodCache.clear();
+ rt.linearizedClassesCache.clear();
+ rt.overloadContextCache.clear();
+ rt.isaStateCache.clear();
// Also clear the inline method cache in RuntimeCode
RuntimeCode.clearInlineMethodCache();
}
@@ -302,6 +312,8 @@ private static void populateIsaMapHelper(String className,
* @return RuntimeScalar representing the found method, or null if not found
*/
public static RuntimeScalar findMethodInHierarchy(String methodName, String perlClassName, String cacheKey, int startFromIndex) {
+ PerlRuntime rt = PerlRuntime.current();
+
if (TRACE_METHOD_RESOLUTION) {
System.err.println("TRACE InheritanceResolver.findMethodInHierarchy:");
System.err.println(" methodName: '" + methodName + "'");
@@ -321,12 +333,12 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
}
// Check if ISA changed for this class - if so, invalidate relevant caches
- if (hasIsaChanged(perlClassName)) {
- invalidateCacheForClass(perlClassName);
+ if (hasIsaChanged(perlClassName, rt)) {
+ invalidateCacheForClass(perlClassName, rt);
}
// Check the method cache - handles both found and not-found cases
- Map mCache = getMethodCache();
+ Map mCache = rt.methodCache;
if (mCache.containsKey(cacheKey)) {
if (TRACE_METHOD_RESOLUTION) {
System.err.println(" Found in cache: " + (mCache.get(cacheKey) != null ? "YES" : "NULL"));
@@ -362,7 +374,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
if (!codeRef.getDefinedBoolean()) {
continue;
}
- cacheMethod(cacheKey, codeRef);
+ mCache.put(cacheKey, codeRef);
if (TRACE_METHOD_RESOLUTION) {
System.err.println(" FOUND method!");
System.err.flush();
@@ -374,7 +386,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
// Second pass — method not found anywhere, check AUTOLOAD in class hierarchy.
// This matches Perl semantics: AUTOLOAD is only tried after the full MRO
// search (including UNIVERSAL) fails to find the method.
- if (isAutoloadEnabled() && !methodName.startsWith("(")) {
+ if (rt.autoloadEnabled && !methodName.startsWith("(")) {
for (int i = startFromIndex; i < linearizedClasses.size(); i++) {
String className = linearizedClasses.get(i);
String effectiveClassName = GlobalVariable.resolveStashAlias(className);
@@ -393,7 +405,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl
} else {
autoloadCode.autoloadVariableName = autoloadName;
}
- cacheMethod(cacheKey, autoload);
+ mCache.put(cacheKey, autoload);
return autoload;
}
}
diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java b/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java
index b516c2d0d..a536b24c8 100644
--- a/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java
+++ b/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java
@@ -12,15 +12,11 @@
* The Mro class provides Perl's mro (Method Resolution Order) module functionality.
* It allows switching between different MRO algorithms (DFS and C3) and provides
* utilities for introspecting the inheritance hierarchy.
+ *
+ * All mutable state is per-PerlRuntime for multiplicity thread-safety.
*/
public class Mro extends PerlModuleBase {
- // Package generation counters
- private static final Map packageGenerations = new HashMap<>();
-
- // Reverse ISA cache (which classes inherit from a given class)
- private static final Map> isaRevCache = new HashMap<>();
-
/**
* Constructor for Mro.
* Initializes the module with the name "mro".
@@ -233,27 +229,28 @@ public static RuntimeList get_mro(RuntimeArray args, int ctx) {
* Builds the reverse ISA cache by dynamically scanning all packages with @ISA arrays.
*/
private static void buildIsaRevCache() {
- isaRevCache.clear();
+ Map> cache = PerlRuntime.current().mroIsaRevCache;
+ cache.clear();
// Dynamically scan all @ISA arrays from global variables
Map allIsaArrays = GlobalVariable.getAllIsaArrays();
for (String key : allIsaArrays.keySet()) {
// Key format: "ClassName::ISA" → extract class name
String className = key.substring(0, key.length() - 5); // remove "::ISA"
- buildIsaRevForClass(className);
+ buildIsaRevForClass(className, cache);
}
}
/**
* Build reverse ISA relationships for a specific class.
*/
- private static void buildIsaRevForClass(String className) {
+ private static void buildIsaRevForClass(String className, Map> cache) {
if (GlobalVariable.existsGlobalArray(className + "::ISA")) {
RuntimeArray isaArray = GlobalVariable.getGlobalArray(className + "::ISA");
for (RuntimeBase parent : isaArray.elements) {
String parentName = parent.toString();
if (parentName != null && !parentName.isEmpty()) {
- isaRevCache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className);
+ cache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className);
}
}
}
@@ -272,14 +269,15 @@ public static RuntimeList get_isarev(RuntimeArray args, int ctx) {
}
String className = args.get(0).toString();
+ Map> cache = PerlRuntime.current().mroIsaRevCache;
// Build reverse ISA cache if empty
- if (isaRevCache.isEmpty()) {
+ if (cache.isEmpty()) {
buildIsaRevCache();
}
RuntimeArray result = new RuntimeArray();
- Set inheritors = isaRevCache.getOrDefault(className, new HashSet<>());
+ Set inheritors = cache.getOrDefault(className, new HashSet<>());
// Add all classes that inherit from this one, including indirectly
Set allInheritors = new HashSet<>();
@@ -301,7 +299,8 @@ private static void collectAllInheritors(String className, Set result, S
}
visited.add(className);
- Set directInheritors = isaRevCache.getOrDefault(className, new HashSet<>());
+ Map> cache = PerlRuntime.current().mroIsaRevCache;
+ Set directInheritors = cache.getOrDefault(className, new HashSet<>());
for (String inheritor : directInheritors) {
result.add(inheritor);
collectAllInheritors(inheritor, result, visited);
@@ -347,11 +346,12 @@ public static RuntimeList is_universal(RuntimeArray args, int ctx) {
* @return A RuntimeList.
*/
public static RuntimeList invalidate_all_method_caches(RuntimeArray args, int ctx) {
+ PerlRuntime rt = PerlRuntime.current();
InheritanceResolver.invalidateCache();
- isaRevCache.clear();
+ rt.mroIsaRevCache.clear();
// Increment all package generations
- for (String pkg : new HashSet<>(packageGenerations.keySet())) {
+ for (String pkg : new HashSet<>(rt.mroPackageGenerations.keySet())) {
incrementPackageGeneration(pkg);
}
@@ -371,16 +371,17 @@ public static RuntimeList method_changed_in(RuntimeArray args, int ctx) {
}
String className = args.get(0).toString();
+ Map> cache = PerlRuntime.current().mroIsaRevCache;
// Invalidate the method cache
InheritanceResolver.invalidateCache();
// Build isarev if needed and invalidate dependent classes
- if (isaRevCache.isEmpty()) {
+ if (cache.isEmpty()) {
buildIsaRevCache();
}
- Set dependents = isaRevCache.getOrDefault(className, new HashSet<>());
+ Set dependents = cache.getOrDefault(className, new HashSet<>());
dependents.add(className); // Include the class itself
// Increment package generation for all dependent classes
@@ -391,9 +392,6 @@ public static RuntimeList method_changed_in(RuntimeArray args, int ctx) {
return new RuntimeList();
}
- // Cached @ISA state per package — used to detect @ISA changes in get_pkg_gen
- private static final Map> pkgGenIsaState = new HashMap<>();
-
/**
* Returns the package generation number.
*
@@ -407,6 +405,7 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) {
}
String className = args.get(0).toString();
+ PerlRuntime rt = PerlRuntime.current();
// Lazily detect @ISA changes and auto-increment pkg_gen
if (GlobalVariable.existsGlobalArray(className + "::ISA")) {
@@ -418,15 +417,15 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) {
currentIsa.add(parentName);
}
}
- List cachedIsa = pkgGenIsaState.get(className);
+ List cachedIsa = rt.mroPkgGenIsaState.get(className);
if (cachedIsa != null && !currentIsa.equals(cachedIsa)) {
incrementPackageGeneration(className);
}
- pkgGenIsaState.put(className, currentIsa);
+ rt.mroPkgGenIsaState.put(className, currentIsa);
}
// Return current generation, starting from 1
- Integer gen = packageGenerations.getOrDefault(className, 1);
+ Integer gen = rt.mroPackageGenerations.getOrDefault(className, 1);
return new RuntimeScalar(gen).getList();
}
@@ -437,7 +436,8 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) {
* @param packageName The name of the package.
*/
public static void incrementPackageGeneration(String packageName) {
- Integer current = packageGenerations.getOrDefault(packageName, 1);
- packageGenerations.put(packageName, current + 1);
+ Map generations = PerlRuntime.current().mroPackageGenerations;
+ Integer current = generations.getOrDefault(packageName, 1);
+ generations.put(packageName, current + 1);
}
}
diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java
index 1a80d93e4..addf0dafa 100644
--- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java
+++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java
@@ -5,10 +5,7 @@
import org.perlonjava.runtime.perlmodule.Utf8;
import org.perlonjava.runtime.runtimetypes.*;
-import java.util.Iterator;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -36,15 +33,15 @@ public class RuntimeRegex extends RuntimeBase implements RuntimeScalarReference
private static final int DOTALL = Pattern.DOTALL;
// Maximum size for the regex cache
private static final int MAX_REGEX_CACHE_SIZE = 1000;
- // Cache to store compiled regex patterns
- private static final Map regexCache = new LinkedHashMap(MAX_REGEX_CACHE_SIZE, 0.75f, true) {
- @Override
- protected boolean removeEldestEntry(Map.Entry eldest) {
- return size() > MAX_REGEX_CACHE_SIZE;
- }
- };
- // Cache for /o modifier - maps callsite ID to compiled regex (only first compilation is used)
- private static final Map optimizedRegexCache = new LinkedHashMap<>();
+ // Cache to store compiled regex patterns (synchronized for multiplicity thread-safety)
+ private static final Map regexCache = Collections.synchronizedMap(
+ new LinkedHashMap(MAX_REGEX_CACHE_SIZE, 0.75f, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ return size() > MAX_REGEX_CACHE_SIZE;
+ }
+ });
+ // Cache for /o modifier is now per-PerlRuntime (regexOptimizedCache field)
// ---- Regex match state accessors (delegating to PerlRuntime.current()) ----
@@ -464,15 +461,16 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS
// Check if /o modifier is present
if (modifierStr.contains("o")) {
+ Map cache = PerlRuntime.current().regexOptimizedCache;
// Check if we already have a cached regex for this callsite
- RuntimeScalar cached = optimizedRegexCache.get(callsiteId);
+ RuntimeScalar cached = cache.get(callsiteId);
if (cached != null) {
return cached;
}
// Compile the regex and cache it
RuntimeScalar result = getQuotedRegex(patternString, modifiers);
- optimizedRegexCache.put(callsiteId, result);
+ cache.put(callsiteId, result);
return result;
}
@@ -1254,9 +1252,12 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar
*/
public static void reset() {
// Iterate over the regexCache and reset the `matched` flag for each cached regex
- for (Map.Entry entry : regexCache.entrySet()) {
- RuntimeRegex regex = entry.getValue();
- regex.matched = false; // Reset the matched field
+ // Synchronized because Collections.synchronizedMap requires manual sync for iteration
+ synchronized (regexCache) {
+ for (Map.Entry entry : regexCache.entrySet()) {
+ RuntimeRegex regex = entry.getValue();
+ regex.matched = false; // Reset the matched field
+ }
}
}
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java
index 836766d4c..dc69a365e 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java
@@ -190,8 +190,9 @@ public static void clearStashAlias(String namespace) {
}
public static String resolveStashAlias(String namespace) {
+ PerlRuntime rt = PerlRuntime.current();
String key = namespace.endsWith("::") ? namespace : namespace + "::";
- String aliased = PerlRuntime.current().stashAliases.get(key);
+ String aliased = rt.stashAliases.get(key);
if (aliased == null) {
return namespace;
}
@@ -209,13 +210,14 @@ public static String resolveStashAlias(String namespace) {
public static void setGlobAlias(String fromGlob, String toGlob) {
// Find the canonical name for toGlob (in case it's already an alias)
String canonical = resolveGlobAlias(toGlob);
+ PerlRuntime rt = PerlRuntime.current();
// Don't create self-loops
if (!fromGlob.equals(canonical)) {
- PerlRuntime.current().globAliases.put(fromGlob, canonical);
+ rt.globAliases.put(fromGlob, canonical);
}
// Also ensure toGlob points to the canonical name (unless it would create a self-loop)
if (!toGlob.equals(canonical) && !toGlob.equals(fromGlob)) {
- PerlRuntime.current().globAliases.put(toGlob, canonical);
+ rt.globAliases.put(toGlob, canonical);
}
}
@@ -256,7 +258,8 @@ public static java.util.List getGlobAliasGroup(String globName) {
* @return The RuntimeScalar representing the global variable.
*/
public static RuntimeScalar getGlobalVariable(String key) {
- RuntimeScalar var = PerlRuntime.current().globalVariables.get(key);
+ PerlRuntime rt = PerlRuntime.current();
+ RuntimeScalar var = rt.globalVariables.get(key);
if (var == null) {
// Need to initialize global variable
Matcher matcher = regexVariablePattern.matcher(key);
@@ -272,14 +275,15 @@ public static RuntimeScalar getGlobalVariable(String key) {
// Normal "non-magic" global variable
var = new RuntimeScalar();
}
- PerlRuntime.current().globalVariables.put(key, var);
+ rt.globalVariables.put(key, var);
}
return var;
}
public static RuntimeScalar aliasGlobalVariable(String key, String to) {
- RuntimeScalar var = PerlRuntime.current().globalVariables.get(to);
- PerlRuntime.current().globalVariables.put(key, var);
+ PerlRuntime rt = PerlRuntime.current();
+ RuntimeScalar var = rt.globalVariables.get(to);
+ rt.globalVariables.put(key, var);
return var;
}
@@ -337,10 +341,11 @@ public static RuntimeScalar removeGlobalVariable(String key) {
* @return The RuntimeArray representing the global array.
*/
public static RuntimeArray getGlobalArray(String key) {
- RuntimeArray var = PerlRuntime.current().globalArrays.get(key);
+ PerlRuntime rt = PerlRuntime.current();
+ RuntimeArray var = rt.globalArrays.get(key);
if (var == null) {
var = new RuntimeArray();
- PerlRuntime.current().globalArrays.put(key, var);
+ rt.globalArrays.put(key, var);
}
return var;
}
@@ -372,7 +377,8 @@ public static RuntimeArray removeGlobalArray(String key) {
* @return The RuntimeHash representing the global hash.
*/
public static RuntimeHash getGlobalHash(String key) {
- RuntimeHash var = PerlRuntime.current().globalHashes.get(key);
+ PerlRuntime rt = PerlRuntime.current();
+ RuntimeHash var = rt.globalHashes.get(key);
if (var == null) {
// Check if this is a package stash (ends with ::)
if (key.endsWith("::")) {
@@ -380,7 +386,7 @@ public static RuntimeHash getGlobalHash(String key) {
} else {
var = new RuntimeHash();
}
- PerlRuntime.current().globalHashes.put(key, var);
+ rt.globalHashes.put(key, var);
}
return var;
}
@@ -417,17 +423,18 @@ public static RuntimeScalar getGlobalCodeRef(String key) {
if (key == null) {
return new RuntimeScalar();
}
+ PerlRuntime rt = PerlRuntime.current();
// First check if we have a pinned reference that survives stash deletion
- RuntimeScalar pinned = PerlRuntime.current().pinnedCodeRefs.get(key);
+ RuntimeScalar pinned = rt.pinnedCodeRefs.get(key);
if (pinned != null) {
// Return the pinned ref so compiled code keeps working, but do NOT
- // re-add to PerlRuntime.current().globalCodeRefs. If it was deleted from the stash (e.g., by
+ // re-add to rt.globalCodeRefs. If it was deleted from the stash (e.g., by
// namespace::clean), that deletion should be respected for method
// resolution via can() and the inheritance hierarchy.
return pinned;
}
- RuntimeScalar var = PerlRuntime.current().globalCodeRefs.get(key);
+ RuntimeScalar var = rt.globalCodeRefs.get(key);
if (var == null) {
var = new RuntimeScalar();
var.type = RuntimeScalarType.CODE; // value is null
@@ -448,11 +455,11 @@ public static RuntimeScalar getGlobalCodeRef(String key) {
// It will be set specifically for \&{string} patterns in createCodeReference
var.value = runtimeCode;
- PerlRuntime.current().globalCodeRefs.put(key, var);
+ rt.globalCodeRefs.put(key, var);
}
// Pin the RuntimeScalar so it survives stash deletion
- PerlRuntime.current().pinnedCodeRefs.put(key, var);
+ rt.pinnedCodeRefs.put(key, var);
return var;
}
@@ -468,9 +475,10 @@ public static RuntimeScalar getGlobalCodeRef(String key) {
*/
public static RuntimeScalar defineGlobalCodeRef(String key) {
RuntimeScalar ref = getGlobalCodeRef(key);
- // Ensure it's in PerlRuntime.current().globalCodeRefs so method resolution finds it
- if (!PerlRuntime.current().globalCodeRefs.containsKey(key)) {
- PerlRuntime.current().globalCodeRefs.put(key, ref);
+ PerlRuntime rt = PerlRuntime.current();
+ // Ensure it's in rt.globalCodeRefs so method resolution finds it
+ if (!rt.globalCodeRefs.containsKey(key)) {
+ rt.globalCodeRefs.put(key, ref);
}
return ref;
}
@@ -494,8 +502,9 @@ public static boolean existsGlobalCodeRef(String key) {
* @param codeRef The new RuntimeScalar to pin (typically a new empty one).
*/
static void replacePinnedCodeRef(String key, RuntimeScalar codeRef) {
- if (PerlRuntime.current().pinnedCodeRefs.containsKey(key)) {
- PerlRuntime.current().pinnedCodeRefs.put(key, codeRef);
+ Map pinned = PerlRuntime.current().pinnedCodeRefs;
+ if (pinned.containsKey(key)) {
+ pinned.put(key, codeRef);
}
}
@@ -641,8 +650,9 @@ public static void clearPackageCache() {
* @return true if any methods exist in the class namespace
*/
public static boolean isPackageLoaded(String className) {
+ PerlRuntime rt = PerlRuntime.current();
// Check cache first
- Boolean cached = PerlRuntime.current().packageExistsCache.get(className);
+ Boolean cached = rt.packageExistsCache.get(className);
if (cached != null) {
return cached;
}
@@ -654,11 +664,11 @@ public static boolean isPackageLoaded(String className) {
// A key like "Foo::Bar::baz" belongs to package "Foo::Bar", not "Foo".
// After stripping the prefix, the remaining part must NOT contain "::"
// to be a direct member of this package.
- boolean exists = PerlRuntime.current().globalCodeRefs.keySet().stream()
+ boolean exists = rt.globalCodeRefs.keySet().stream()
.anyMatch(key -> key.startsWith(prefix) && !key.substring(prefix.length()).contains("::"));
// Cache the result
- PerlRuntime.current().packageExistsCache.put(className, exists);
+ rt.packageExistsCache.put(className, exists);
return exists;
}
@@ -703,10 +713,11 @@ public static String resolveStashHashRedirect(String fullName) {
*/
public static RuntimeGlob getGlobalIO(String key) {
String resolvedKey = resolveStashHashRedirect(key);
- RuntimeGlob glob = PerlRuntime.current().globalIORefs.get(resolvedKey);
+ PerlRuntime rt = PerlRuntime.current();
+ RuntimeGlob glob = rt.globalIORefs.get(resolvedKey);
if (glob == null) {
glob = new RuntimeGlob(resolvedKey);
- PerlRuntime.current().globalIORefs.put(resolvedKey, glob);
+ rt.globalIORefs.put(resolvedKey, glob);
}
return glob;
}
@@ -793,39 +804,41 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName
return RuntimeScalarCache.scalarTrue;
}
+ PerlRuntime rt = PerlRuntime.current();
+
// Check if glob was explicitly assigned
- if (PerlRuntime.current().globalGlobs.getOrDefault(varName, false)) {
+ if (rt.globalGlobs.getOrDefault(varName, false)) {
return RuntimeScalarCache.scalarTrue;
}
// Check scalar slot - slot existence makes glob defined (not value definedness)
// In Perl, `defined *FOO` is true if $FOO exists, even if $FOO is undef
- if (PerlRuntime.current().globalVariables.containsKey(varName)) {
+ if (rt.globalVariables.containsKey(varName)) {
return RuntimeScalarCache.scalarTrue;
}
// Check array slot - exists = defined (even if empty)
- if (PerlRuntime.current().globalArrays.containsKey(varName)) {
+ if (rt.globalArrays.containsKey(varName)) {
return RuntimeScalarCache.scalarTrue;
}
// Check hash slot - exists = defined (even if empty)
- if (PerlRuntime.current().globalHashes.containsKey(varName)) {
+ if (rt.globalHashes.containsKey(varName)) {
return RuntimeScalarCache.scalarTrue;
}
// Check code slot - slot existence makes glob defined
- if (PerlRuntime.current().globalCodeRefs.containsKey(varName)) {
+ if (rt.globalCodeRefs.containsKey(varName)) {
return RuntimeScalarCache.scalarTrue;
}
- // Check IO slot (via PerlRuntime.current().globalIORefs)
- if (PerlRuntime.current().globalIORefs.containsKey(varName)) {
+ // Check IO slot (via rt.globalIORefs)
+ if (rt.globalIORefs.containsKey(varName)) {
return RuntimeScalarCache.scalarTrue;
}
// Check format slot
- if (PerlRuntime.current().globalFormatRefs.containsKey(varName)) {
+ if (rt.globalFormatRefs.containsKey(varName)) {
return RuntimeScalarCache.scalarTrue;
}
@@ -839,10 +852,11 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName
* @return The RuntimeFormat representing the global format reference.
*/
public static RuntimeFormat getGlobalFormatRef(String key) {
- RuntimeFormat format = PerlRuntime.current().globalFormatRefs.get(key);
+ PerlRuntime rt = PerlRuntime.current();
+ RuntimeFormat format = rt.globalFormatRefs.get(key);
if (format == null) {
format = new RuntimeFormat(key);
- PerlRuntime.current().globalFormatRefs.put(key, format);
+ rt.globalFormatRefs.put(key, format);
}
return format;
}
@@ -888,8 +902,9 @@ public static boolean isGlobalFormatDefined(String key) {
}
public static RuntimeScalar definedGlobalFormatAsScalar(String key) {
- return PerlRuntime.current().globalFormatRefs.containsKey(key) ?
- (PerlRuntime.current().globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse;
+ PerlRuntime rt = PerlRuntime.current();
+ return rt.globalFormatRefs.containsKey(key) ?
+ (rt.globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse;
}
public static RuntimeScalar definedGlobalFormatAsScalar(RuntimeScalar key) {
@@ -903,8 +918,9 @@ public static RuntimeScalar definedGlobalFormatAsScalar(RuntimeScalar key) {
* @param currentPackage The current package name with "::" suffix
*/
public static void resetGlobalVariables(Set resetChars, String currentPackage) {
+ PerlRuntime rt = PerlRuntime.current();
// Reset scalar variables
- for (Map.Entry entry : PerlRuntime.current().globalVariables.entrySet()) {
+ for (Map.Entry entry : rt.globalVariables.entrySet()) {
String key = entry.getKey();
if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) {
@@ -914,7 +930,7 @@ public static void resetGlobalVariables(Set resetChars, String curren
}
// Reset array variables
- for (Map.Entry entry : PerlRuntime.current().globalArrays.entrySet()) {
+ for (Map.Entry entry : rt.globalArrays.entrySet()) {
String key = entry.getKey();
if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) {
@@ -924,7 +940,7 @@ public static void resetGlobalVariables(Set resetChars, String curren
}
// Reset hash variables
- for (Map.Entry entry : PerlRuntime.current().globalHashes.entrySet()) {
+ for (Map.Entry entry : rt.globalHashes.entrySet()) {
String key = entry.getKey();
if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) {
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java
index 49950bedc..bc40c7ef5 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java
@@ -15,12 +15,6 @@
*/
public class OutputFieldSeparator extends RuntimeScalar {
- /**
- * The internal OFS value that print reads.
- * Only updated by OutputFieldSeparator.set() calls.
- */
- private static String internalOFS = "";
-
/**
* Stack for save/restore during local $, and for $, (list).
* Now held per-PerlRuntime.
@@ -35,9 +29,10 @@ public OutputFieldSeparator() {
/**
* Returns the internal OFS value for use by print.
+ * Now per-PerlRuntime for multiplicity thread-safety.
*/
public static String getInternalOFS() {
- return internalOFS;
+ return PerlRuntime.current().internalOFS;
}
/**
@@ -45,7 +40,8 @@ public static String getInternalOFS() {
* Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $,.
*/
public static void saveInternalOFS() {
- ofsStack().push(internalOFS);
+ PerlRuntime rt = PerlRuntime.current();
+ ofsStack().push(rt.internalOFS);
}
/**
@@ -54,49 +50,49 @@ public static void saveInternalOFS() {
*/
public static void restoreInternalOFS() {
if (!ofsStack().isEmpty()) {
- internalOFS = ofsStack().pop();
+ PerlRuntime.current().internalOFS = ofsStack().pop();
}
}
@Override
public RuntimeScalar set(RuntimeScalar value) {
super.set(value);
- internalOFS = this.toString();
+ PerlRuntime.current().internalOFS = this.toString();
return this;
}
@Override
public RuntimeScalar set(String value) {
super.set(value);
- internalOFS = this.toString();
+ PerlRuntime.current().internalOFS = this.toString();
return this;
}
@Override
public RuntimeScalar set(int value) {
super.set(value);
- internalOFS = this.toString();
+ PerlRuntime.current().internalOFS = this.toString();
return this;
}
@Override
public RuntimeScalar set(long value) {
super.set(value);
- internalOFS = this.toString();
+ PerlRuntime.current().internalOFS = this.toString();
return this;
}
@Override
public RuntimeScalar set(boolean value) {
super.set(value);
- internalOFS = this.toString();
+ PerlRuntime.current().internalOFS = this.toString();
return this;
}
@Override
public RuntimeScalar set(Object value) {
super.set(value);
- internalOFS = this.toString();
+ PerlRuntime.current().internalOFS = this.toString();
return this;
}
}
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java
index 86942ab05..e8cd691ed 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java
@@ -20,12 +20,6 @@
*/
public class OutputRecordSeparator extends RuntimeScalar {
- /**
- * The internal ORS value that print reads.
- * Only updated by OutputRecordSeparator.set() calls.
- */
- private static String internalORS = "";
-
/**
* Stack for save/restore during local $\ and for $\ (list).
* Now held per-PerlRuntime.
@@ -40,9 +34,10 @@ public OutputRecordSeparator() {
/**
* Returns the internal ORS value for use by print.
+ * Now per-PerlRuntime for multiplicity thread-safety.
*/
public static String getInternalORS() {
- return internalORS;
+ return PerlRuntime.current().internalORS;
}
/**
@@ -50,7 +45,8 @@ public static String getInternalORS() {
* Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $\.
*/
public static void saveInternalORS() {
- orsStack().push(internalORS);
+ PerlRuntime rt = PerlRuntime.current();
+ orsStack().push(rt.internalORS);
}
/**
@@ -59,49 +55,49 @@ public static void saveInternalORS() {
*/
public static void restoreInternalORS() {
if (!orsStack().isEmpty()) {
- internalORS = orsStack().pop();
+ PerlRuntime.current().internalORS = orsStack().pop();
}
}
@Override
public RuntimeScalar set(RuntimeScalar value) {
super.set(value);
- internalORS = this.toString();
+ PerlRuntime.current().internalORS = this.toString();
return this;
}
@Override
public RuntimeScalar set(String value) {
super.set(value);
- internalORS = this.toString();
+ PerlRuntime.current().internalORS = this.toString();
return this;
}
@Override
public RuntimeScalar set(int value) {
super.set(value);
- internalORS = this.toString();
+ PerlRuntime.current().internalORS = this.toString();
return this;
}
@Override
public RuntimeScalar set(long value) {
super.set(value);
- internalORS = this.toString();
+ PerlRuntime.current().internalORS = this.toString();
return this;
}
@Override
public RuntimeScalar set(boolean value) {
super.set(value);
- internalORS = this.toString();
+ PerlRuntime.current().internalORS = this.toString();
return this;
}
@Override
public RuntimeScalar set(Object value) {
super.set(value);
- internalORS = this.toString();
+ PerlRuntime.current().internalORS = this.toString();
return this;
}
}
diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java
index 39f7cff24..3fa4fa437 100644
--- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java
+++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java
@@ -1,6 +1,7 @@
package org.perlonjava.runtime.runtimetypes;
import org.perlonjava.backend.jvm.CustomClassLoader;
+import org.perlonjava.runtime.io.IOHandle;
import org.perlonjava.runtime.io.StandardIO;
import org.perlonjava.runtime.mro.InheritanceResolver;
import org.perlonjava.runtime.regex.RuntimeRegex;
@@ -11,6 +12,7 @@
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.Stack;
@@ -153,12 +155,24 @@ public final class PerlRuntime {
*/
final Stack orsStack = new Stack<>();
+ /**
+ * Internal ORS value that print reads — migrated from OutputRecordSeparator.internalORS
+ * for multiplicity thread-safety.
+ */
+ public String internalORS = "";
+
/**
* OFS stack for OutputFieldSeparator "local $," save/restore —
* migrated from OutputFieldSeparator.ofsStack.
*/
final Stack ofsStack = new Stack<>();
+ /**
+ * Internal OFS value that print reads — migrated from OutputFieldSeparator.internalOFS
+ * for multiplicity thread-safety.
+ */
+ public String internalOFS = "";
+
/**
* Errno stacks for ErrnoVariable "local $!" save/restore —
* migrated from ErrnoVariable.errnoStack and messageStack.
@@ -248,6 +262,39 @@ public final class PerlRuntime {
*/
public InheritanceResolver.MROAlgorithm currentMRO = InheritanceResolver.MROAlgorithm.DFS;
+ // ---- MRO state — migrated from Mro static fields for multiplicity ----
+
+ /** Package generation counters for mro::get_pkg_gen(). */
+ public final Map mroPackageGenerations = new HashMap<>();
+
+ /** Reverse ISA cache (which classes inherit from a given class). */
+ public final Map> mroIsaRevCache = new HashMap<>();
+
+ /** Cached @ISA state per package — used to detect @ISA changes. */
+ public final Map> mroPkgGenIsaState = new HashMap<>();
+
+ // ---- IO state — migrated from RuntimeIO static fields for multiplicity ----
+
+ /** Maximum number of file handles to keep in the LRU cache. */
+ private static final int MAX_OPEN_HANDLES = 100;
+
+ /** LRU cache of open file handles — per-runtime for multiplicity. */
+ public final Map openHandles =
+ new LinkedHashMap(MAX_OPEN_HANDLES, 0.75f, true) {
+ @Override
+ protected boolean removeEldestEntry(Map.Entry eldest) {
+ if (size() > MAX_OPEN_HANDLES) {
+ try {
+ eldest.getKey().flush();
+ } catch (Exception e) {
+ // Handle exception if needed
+ }
+ return true;
+ }
+ return false;
+ }
+ };
+
// ---- Symbol table state — migrated from GlobalVariable static fields ----
/** Global scalar variables (%main:: scalar namespace). */
@@ -339,6 +386,15 @@ public final class PerlRuntime {
/** Preserves BYTE_STRING type on captures. */
public boolean regexLastMatchWasByteString = false;
+ /** Cache for /o modifier — maps callsite ID to compiled regex (only first compilation is used). */
+ public final Map regexOptimizedCache = new HashMap<>();
+
+ // ---- ByteCodeSourceMapper state — migrated for multiplicity ----
+
+ /** Per-runtime source-mapper state (file→line→package mappings for stack traces). */
+ public final org.perlonjava.backend.jvm.ByteCodeSourceMapper.State sourceMapperState =
+ new org.perlonjava.backend.jvm.ByteCodeSourceMapper.State();
+
// ---- RuntimeCode compilation state — migrated from RuntimeCode static fields ----
/** Tracks eval BEGIN block IDs during compilation. */
@@ -395,6 +451,45 @@ protected boolean removeEldestEntry(Map.Entry, java.lang.invoke.MethodH
public final int[] inlineCacheMethodHash = new int[METHOD_CALL_CACHE_SIZE];
public final RuntimeCode[] inlineCacheCode = new RuntimeCode[METHOD_CALL_CACHE_SIZE];
+ // ---- Warning/Hints stacks — migrated from WarningBitsRegistry ThreadLocals ----
+
+ /** Stack of warning bits for the current execution context. */
+ public final Deque warningCurrentBitsStack = new ArrayDeque<>();
+
+ /** Warning bits at the current call site. */
+ public String warningCallSiteBits = null;
+
+ /** Stack saving caller's call-site warning bits across subroutine calls. */
+ public final Deque warningCallerBitsStack = new ArrayDeque<>();
+
+ /** Compile-time $^H (hints) at the current call site. */
+ public int warningCallSiteHints = 0;
+
+ /** Stack saving caller's $^H hints across subroutine calls. */
+ public final Deque warningCallerHintsStack = new ArrayDeque<>();
+
+ /** Compile-time %^H (hints hash) snapshot at the current call site. */
+ public Map warningCallSiteHintHash = new HashMap<>();
+
+ /** Stack saving caller's %^H across subroutine calls. */
+ public final Deque