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> warningCallerHintHashStack = new ArrayDeque<>(); + + // ---- HintHashRegistry stacks — migrated from HintHashRegistry ThreadLocals ---- + + /** Current call site's hint hash snapshot ID. */ + public int hintCallSiteSnapshotId = 0; + + /** Stack saving caller's hint hash snapshot ID across subroutine calls. */ + public final Deque hintCallerSnapshotIdStack = new ArrayDeque<>(); + + // ---- RuntimeCode stacks — migrated from RuntimeCode ThreadLocals ---- + + /** Eval runtime context (used during eval STRING compilation). */ + public Object evalRuntimeContext = null; + + /** Stack of @_ argument arrays across subroutine calls. */ + public final Deque argsStack = new ArrayDeque<>(); + // ---- Static accessors ---- /** @@ -456,6 +551,83 @@ public static PerlRuntime initialize() { return rt; } + // ---- Batch push/pop methods for subroutine call hot path ---- + + /** + * Pushes all caller state in one shot for the static apply() dispatch path. + * Replaces 4 separate PerlRuntime.current() lookups with 1. + * Called from RuntimeCode's static apply() methods. + * + * @param warningBits warning bits for the code being called, or null + */ + public void pushCallerState(String warningBits) { + if (warningBits != null) { + warningCurrentBitsStack.push(warningBits); + } + // Save caller's call-site warning bits (for caller()[9]) + warningCallerBitsStack.push(warningCallSiteBits != null ? warningCallSiteBits : ""); + // Save caller's $^H (for caller()[8]) + warningCallerHintsStack.push(warningCallSiteHints); + // Save caller's hint hash snapshot ID and reset for callee + hintCallerSnapshotIdStack.push(hintCallSiteSnapshotId); + hintCallSiteSnapshotId = 0; + } + + /** + * Pops all caller state in one shot for the static apply() dispatch path. + * Replaces 4 separate PerlRuntime.current() lookups with 1. + * + * @param hadWarningBits true if warningBits was non-null on the matching push + */ + public void popCallerState(boolean hadWarningBits) { + // Restore hint hash snapshot ID + if (!hintCallerSnapshotIdStack.isEmpty()) { + hintCallSiteSnapshotId = hintCallerSnapshotIdStack.pop(); + } + // Restore caller hints + if (!warningCallerHintsStack.isEmpty()) { + warningCallerHintsStack.pop(); + } + // Restore caller bits + if (!warningCallerBitsStack.isEmpty()) { + warningCallerBitsStack.pop(); + } + // Restore warning bits + if (hadWarningBits && !warningCurrentBitsStack.isEmpty()) { + warningCurrentBitsStack.pop(); + } + } + + /** + * Pushes subroutine entry state (args + warning bits) in one shot. + * Replaces 2 separate PerlRuntime.current() lookups with 1. + * Called from RuntimeCode's instance apply() methods. + * + * @param args the @_ arguments array + * @param warningBits warning bits for the code being called, or null + */ + public void pushSubState(RuntimeArray args, String warningBits) { + argsStack.push(args); + if (warningBits != null) { + warningCurrentBitsStack.push(warningBits); + } + } + + /** + * Pops subroutine exit state (args + warning bits) in one shot. + * Replaces 2 separate PerlRuntime.current() lookups with 1. + * + * @param hadWarningBits true if warningBits was non-null on the matching push + */ + public void popSubState(boolean hadWarningBits) { + if (hadWarningBits && !warningCurrentBitsStack.isEmpty()) { + warningCurrentBitsStack.pop(); + } + if (!argsStack.isEmpty()) { + argsStack.pop(); + } + } + /** * Creates a new independent PerlRuntime (not bound to any thread). * Call {@link #setCurrent(PerlRuntime)} to bind it to a thread before use. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java index 37311c604..fe3783b5f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java @@ -4,6 +4,7 @@ import java.util.regex.Matcher; + /** * Snapshot of regex-related global state (Perl's $1, $&, $`, $', etc.). * @@ -28,19 +29,21 @@ public class RegexState implements DynamicState { private final boolean lastMatchWasByteString; public RegexState() { - this.globalMatcher = RuntimeRegex.getGlobalMatcher(); - this.globalMatchString = RuntimeRegex.getGlobalMatchString(); - this.lastMatchedString = RuntimeRegex.getLastMatchedString(); - this.lastMatchStart = RuntimeRegex.getLastMatchStart(); - this.lastMatchEnd = RuntimeRegex.getLastMatchEnd(); - this.lastSuccessfulMatchedString = RuntimeRegex.getLastSuccessfulMatchedString(); - this.lastSuccessfulMatchStart = RuntimeRegex.getLastSuccessfulMatchStart(); - this.lastSuccessfulMatchEnd = RuntimeRegex.getLastSuccessfulMatchEnd(); - this.lastSuccessfulMatchString = RuntimeRegex.getLastSuccessfulMatchString(); - this.lastSuccessfulPattern = RuntimeRegex.getLastSuccessfulPattern(); - this.lastMatchUsedPFlag = RuntimeRegex.getLastMatchUsedPFlag(); - this.lastCaptureGroups = RuntimeRegex.getLastCaptureGroups(); - this.lastMatchWasByteString = RuntimeRegex.getLastMatchWasByteString(); + // Single PerlRuntime.current() lookup instead of 13 separate ones + PerlRuntime rt = PerlRuntime.current(); + this.globalMatcher = rt.regexGlobalMatcher; + this.globalMatchString = rt.regexGlobalMatchString; + this.lastMatchedString = rt.regexLastMatchedString; + this.lastMatchStart = rt.regexLastMatchStart; + this.lastMatchEnd = rt.regexLastMatchEnd; + this.lastSuccessfulMatchedString = rt.regexLastSuccessfulMatchedString; + this.lastSuccessfulMatchStart = rt.regexLastSuccessfulMatchStart; + this.lastSuccessfulMatchEnd = rt.regexLastSuccessfulMatchEnd; + this.lastSuccessfulMatchString = rt.regexLastSuccessfulMatchString; + this.lastSuccessfulPattern = rt.regexLastSuccessfulPattern; + this.lastMatchUsedPFlag = rt.regexLastMatchUsedPFlag; + this.lastCaptureGroups = rt.regexLastCaptureGroups; + this.lastMatchWasByteString = rt.regexLastMatchWasByteString; } public static void save() { @@ -57,18 +60,20 @@ public void restore() { @Override public void dynamicRestoreState() { - RuntimeRegex.setGlobalMatcher(this.globalMatcher); - RuntimeRegex.setGlobalMatchString(this.globalMatchString); - RuntimeRegex.setLastMatchedString(this.lastMatchedString); - RuntimeRegex.setLastMatchStart(this.lastMatchStart); - RuntimeRegex.setLastMatchEnd(this.lastMatchEnd); - RuntimeRegex.setLastSuccessfulMatchedString(this.lastSuccessfulMatchedString); - RuntimeRegex.setLastSuccessfulMatchStart(this.lastSuccessfulMatchStart); - RuntimeRegex.setLastSuccessfulMatchEnd(this.lastSuccessfulMatchEnd); - RuntimeRegex.setLastSuccessfulMatchString(this.lastSuccessfulMatchString); - RuntimeRegex.setLastSuccessfulPattern(this.lastSuccessfulPattern); - RuntimeRegex.setLastMatchUsedPFlag(this.lastMatchUsedPFlag); - RuntimeRegex.setLastCaptureGroups(this.lastCaptureGroups); - RuntimeRegex.setLastMatchWasByteString(this.lastMatchWasByteString); + // Single PerlRuntime.current() lookup instead of 13 separate ones + PerlRuntime rt = PerlRuntime.current(); + rt.regexGlobalMatcher = this.globalMatcher; + rt.regexGlobalMatchString = this.globalMatchString; + rt.regexLastMatchedString = this.lastMatchedString; + rt.regexLastMatchStart = this.lastMatchStart; + rt.regexLastMatchEnd = this.lastMatchEnd; + rt.regexLastSuccessfulMatchedString = this.lastSuccessfulMatchedString; + rt.regexLastSuccessfulMatchStart = this.lastSuccessfulMatchStart; + rt.regexLastSuccessfulMatchEnd = this.lastSuccessfulMatchEnd; + rt.regexLastSuccessfulMatchString = this.lastSuccessfulMatchString; + rt.regexLastSuccessfulPattern = this.lastSuccessfulPattern; + rt.regexLastMatchUsedPFlag = this.lastMatchUsedPFlag; + rt.regexLastCaptureGroups = this.lastCaptureGroups; + rt.regexLastMatchWasByteString = this.lastMatchWasByteString; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index fab9809d0..7c6fd569a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -101,10 +101,10 @@ public static IdentityHashMap getEvalBeginIds() { * - runtimeValues: Object[] of captured variable values * - capturedEnv: String[] of captured variable names (matching array indices) *

- * Thread-safety: Each thread's eval compilation uses its own ThreadLocal storage, so parallel + * Thread-safety: Each thread's eval compilation uses its own PerlRuntime storage, so parallel * eval compilations don't interfere with each other. */ - private static final ThreadLocal evalRuntimeContext = new ThreadLocal<>(); + // evalRuntimeContext migrated to PerlRuntime; access via getEvalRuntimeContext() // evalCache migrated to PerlRuntime; access via getEvalCache() private static Map> getEvalCache() { return PerlRuntime.current().evalCache; @@ -150,9 +150,9 @@ public static void decrementEvalDepth() { * * Push/pop is handled by RuntimeCode.apply() methods. * Access via getCurrentArgs() for Java-implemented functions that need caller's @_. + * + * Migrated to PerlRuntime.argsStack for reduced ThreadLocal overhead. */ - private static final ThreadLocal> argsStack = - ThreadLocal.withInitial(ArrayDeque::new); /** * Get the current subroutine's @_ array. @@ -162,7 +162,7 @@ public static void decrementEvalDepth() { * @return The current @_ array, or null if not in a subroutine */ public static RuntimeArray getCurrentArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; return stack.isEmpty() ? null : stack.peek(); } @@ -178,7 +178,7 @@ public static RuntimeArray getCurrentArgs() { * @return The caller's @_ array, or null if not available */ public static RuntimeArray getCallerArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; if (stack.size() < 2) { return null; } @@ -192,7 +192,7 @@ public static RuntimeArray getCallerArgs() { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void pushArgs(RuntimeArray args) { - argsStack.get().push(args); + PerlRuntime.current().argsStack.push(args); } /** @@ -200,7 +200,7 @@ public static void pushArgs(RuntimeArray args) { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void popArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -403,7 +403,7 @@ public static boolean hasAutoload(RuntimeCode code) { * @return The current eval runtime context, or null if not in eval STRING compilation */ public static EvalRuntimeContext getEvalRuntimeContext() { - return evalRuntimeContext.get(); + return (EvalRuntimeContext) PerlRuntime.current().evalRuntimeContext; } /** @@ -424,7 +424,7 @@ public static void clearCaches() { rt.anonSubs.clear(); rt.interpretedSubs.clear(); rt.evalContext.clear(); - evalRuntimeContext.remove(); + rt.evalRuntimeContext = null; } public static void copy(RuntimeCode code, RuntimeCode codeFrom) { @@ -517,7 +517,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje ctx.capturedEnv, // Variable names in same order as runtimeValues evalTag ); - evalRuntimeContext.set(runtimeCtx); + PerlRuntime.current().evalRuntimeContext = runtimeCtx; try { // Check if the eval string contains non-ASCII characters @@ -829,7 +829,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // IMPORTANT: Always clean up ThreadLocal in finally block to ensure it's removed // even if compilation fails. Failure to do so could cause memory leaks in // long-running applications with thread pools. - evalRuntimeContext.remove(); + PerlRuntime.current().evalRuntimeContext = null; } } finally { @@ -1004,7 +1004,7 @@ public static RuntimeList evalStringWithInterpreter( ctx.capturedEnv, evalTag ); - evalRuntimeContext.set(runtimeCtx); + PerlRuntime.current().evalRuntimeContext = runtimeCtx; InterpretedCode interpretedCode = null; RuntimeList result; @@ -1345,8 +1345,8 @@ public static RuntimeList evalStringWithInterpreter( storeSourceLines(code.toString(), evalFilename, ast, tokens); } - // Clean up ThreadLocal - evalRuntimeContext.remove(); + // Clean up eval runtime context + PerlRuntime.current().evalRuntimeContext = null; // Release the compile lock if still held (error path — success path releases it earlier). // Use a boolean flag instead of isHeldByCurrentThread() to avoid over-decrementing @@ -2097,15 +2097,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Look up warning bits for the code's class and push to context stack // This enables FATAL warnings to work even at top-level (no caller frame) String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() RuntimeList result = code.apply(a, callContext); @@ -2126,12 +2120,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -2330,15 +2319,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2350,12 +2333,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -2496,15 +2474,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2516,12 +2488,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -2904,13 +2871,11 @@ public RuntimeList apply(RuntimeArray a, int callContext) { DebugHooks.enterSubroutine(debugSubName); } // Always push args for getCurrentArgs() support (used by List::Util::any/all/etc.) - pushArgs(a); - // Push warning bits for FATAL warnings support + // Batch push: args + warning bits in one PerlRuntime.current() call String warningBits = getWarningBitsForCode(this); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } + PerlRuntime rt = PerlRuntime.current(); + rt.pushSubState(a, warningBits); try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -2923,10 +2888,7 @@ public RuntimeList apply(RuntimeArray a, int callContext) { } return result; } finally { - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } - popArgs(); + rt.popSubState(warningBits != null); if (DebugState.debugMode) { DebugHooks.exitSubroutine(); DebugState.popArgs(); @@ -3001,13 +2963,11 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) DebugHooks.enterSubroutine(debugSubName); } // Always push args for getCurrentArgs() support (used by List::Util::any/all/etc.) - pushArgs(a); - // Push warning bits for FATAL warnings support + // Batch push: args + warning bits in one PerlRuntime.current() call String warningBits = getWarningBitsForCode(this); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } + PerlRuntime rt = PerlRuntime.current(); + rt.pushSubState(a, warningBits); try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -3020,10 +2980,7 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) } return result; } finally { - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } - popArgs(); + rt.popSubState(warningBits != null); if (DebugState.debugMode) { DebugHooks.exitSubroutine(); DebugState.popArgs(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index 47ca877a1..b6d6d0201 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -91,31 +91,12 @@ public class RuntimeIO extends RuntimeScalar { private static final Map> MODE_OPTIONS = new HashMap<>(); /** - * Maximum number of file handles to keep in the LRU cache. - * Older handles are flushed (not closed) when this limit is exceeded. + * Returns the per-runtime LRU cache of open file handles. + * Migrated from a static field for multiplicity thread-safety. */ - private static final int MAX_OPEN_HANDLES = 100; - - /** - * LRU (Least Recently Used) cache for managing open file handles. - * This helps prevent resource exhaustion by limiting open handles and - * automatically flushing less recently used ones. - */ - private static final Map openHandles = new LinkedHashMap(MAX_OPEN_HANDLES, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - if (size() > MAX_OPEN_HANDLES) { - try { - // Flush but don't close the eldest handle - eldest.getKey().flush(); - } catch (Exception e) { - // Handle exception if needed - } - return true; - } - return false; - } - }; + private static Map openHandles() { + return PerlRuntime.current().openHandles; + } private static final Map childProcesses = new java.util.concurrent.ConcurrentHashMap<>(); @@ -982,8 +963,9 @@ public static void flushFileHandles() { * @param handle the IOHandle to cache */ public static void addHandle(IOHandle handle) { - synchronized (openHandles) { - openHandles.put(handle, Boolean.TRUE); + Map handles = openHandles(); + synchronized (handles) { + handles.put(handle, Boolean.TRUE); } } @@ -993,8 +975,9 @@ public static void addHandle(IOHandle handle) { * @param handle the IOHandle to remove */ public static void removeHandle(IOHandle handle) { - synchronized (openHandles) { - openHandles.remove(handle); + Map handles = openHandles(); + synchronized (handles) { + handles.remove(handle); } } @@ -1003,8 +986,9 @@ public static void removeHandle(IOHandle handle) { * This ensures all buffered data is written without closing files. */ public static void flushAllHandles() { - synchronized (openHandles) { - for (IOHandle handle : openHandles.keySet()) { + Map handles = openHandles(); + synchronized (handles) { + for (IOHandle handle : handles.keySet()) { handle.flush(); } } @@ -1017,8 +1001,9 @@ public static void flushAllHandles() { */ public static void closeAllHandles() { flushAllHandles(); - synchronized (openHandles) { - for (IOHandle handle : openHandles.keySet()) { + Map handles = openHandles(); + synchronized (handles) { + for (IOHandle handle : handles.keySet()) { try { handle.close(); handle = new ClosedIOHandle(); @@ -1026,7 +1011,7 @@ public static void closeAllHandles() { // Handle exception if needed } } - openHandles.clear(); // Clear the cache after closing all handles + handles.clear(); // Clear the cache after closing all handles } }