diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 59dcac611..0651522e4 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3,6 +3,8 @@ import org.perlonjava.backend.jvm.EmitterContext; import org.perlonjava.backend.jvm.EmitterMethodCreator; +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; @@ -93,6 +95,13 @@ public class BytecodeCompiler implements Visitor { Set currentSubroutineClosureVars = new HashSet<>(); // Variables captured from outer scope // EmitterContext for strict checks and other compile-time options private EmitterContext emitterContext; + // Limit JVM class generation for anonymous subs in eval STRING to prevent OOM. + // Each generated JVM class stays in memory permanently (classloader can't unload them). + // Programs like Benchmark.pm create hundreds of unique eval STRINGs, each with + // anonymous subs, causing unbounded class accumulation. + // Set to 0 to disable (avoids JIT deoptimization from extra loaded classes). + private static final int MAX_EVAL_JVM_CLASSES = 0; + private static int evalJvmClassCount = 0; // Register allocation private int nextRegister = 3; // 0=this, 1=@_, 2=wantarray private int baseRegisterForStatement = 3; // Reset point after each statement @@ -5014,7 +5023,157 @@ 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 && evalJvmClassCount < MAX_EVAL_JVM_CLASSES) { + // Skip JVM compilation for closures that capture non-scalar variables + // (arrays, hashes) because the interpreter stores them as RuntimeBase references + // but the JVM constructor expects specific types (RuntimeArray, RuntimeHash) + boolean hasNonScalarCaptures = false; + for (String varName : closureVarNames) { + if (varName.startsWith("@") || varName.startsWith("%")) { + hasNonScalarCaptures = true; + break; + } + } + if (!hasNonScalarCaptures) { + try { + emitJvmAnonymousSub(node, closureVarNames, closureVarIndices); + evalJvmClassCount++; + return; // JVM compilation succeeded + } catch (Exception | LinkageError e) { + // JVM compilation failed, fall through to interpreter path + // Note: LinkageError catches VerifyError from bytecode verification failures + 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); + + // Note: We intentionally don't cache in RuntimeCode.anonSubs here. + // The generated class is kept alive by the JvmClosureTemplate in the constant pool. + + // 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 = new java.util.ArrayList<>(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 + java.util.List attrs = (node.attributes != null && !node.attributes.isEmpty()) + ? new java.util.ArrayList<>(node.attributes) : null; + JvmClosureTemplate template = new JvmClosureTemplate( + generatedClass, node.prototype, packageName, attrs); + 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<>(); @@ -5064,7 +5223,7 @@ private void visitAnonymousSubroutine(SubroutineNode node) { // Sub-compiler will use parentRegistry to resolve captured variables InterpretedCode subCode = subCompiler.compile(node.block); subCode.prototype = node.prototype; - subCode.attributes = node.attributes; + subCode.attributes = node.attributes != null ? new java.util.ArrayList<>(node.attributes) : null; subCode.packageName = getCurrentPackage(); // Copy the isMapGrepBlock flag to the runtime code object so that diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index dd8eb0a26..7eae90c7c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -314,7 +314,7 @@ public InterpretedCode withCapturedVars(RuntimeBase[] capturedVars) { this.warningBitsString ); copy.prototype = this.prototype; - copy.attributes = this.attributes; + copy.attributes = this.attributes != null ? new java.util.ArrayList<>(this.attributes) : null; copy.subName = this.subName; copy.packageName = this.packageName; // Preserve compiler-set fields that are not passed through the constructor diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index c218df163..d46554187 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; @@ -894,8 +895,8 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg int templateIdx = bytecode[pc++]; int numCaptures = bytecode[pc++]; - // Get the template InterpretedCode from constants - InterpretedCode template = (InterpretedCode) code.constants[templateIdx]; + // Get the template from constants (can be InterpretedCode or JvmClosureTemplate) + Object template = code.constants[templateIdx]; // Capture the current register values RuntimeBase[] capturedVars = new RuntimeBase[numCaptures]; @@ -904,35 +905,49 @@ 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); - - // Track captureCount on captured RuntimeScalar variables. - // This mirrors what RuntimeCode.makeCodeObject() does for JVM-compiled closures. - // Without this, scopeExitCleanup() doesn't know the variable is still alive - // via this closure, and may prematurely clear weak references to its value. - java.util.List capturedScalars = new java.util.ArrayList<>(); - for (RuntimeBase captured : capturedVars) { - if (captured instanceof RuntimeScalar s) { - capturedScalars.add(s); - s.captureCount++; + if (template instanceof JvmClosureTemplate jvmTemplate) { + // JVM-compiled closure: instantiate the generated class with captured variables + RuntimeScalar codeRef = jvmTemplate.instantiate(capturedVars); + registers[rd] = codeRef; + + // Dispatch MODIFY_CODE_ATTRIBUTES for JVM closures with non-builtin attributes + if (jvmTemplate.attributes != null && !jvmTemplate.attributes.isEmpty()) { + RuntimeCode jvmCode = (RuntimeCode) codeRef.value; + if (jvmCode.packageName != null) { + Attributes.runtimeDispatchModifyCodeAttributes(jvmCode.packageName, codeRef, true); + } + } + } else { + // InterpretedCode closure: create a new copy with captured variables + InterpretedCode closureCode = ((InterpretedCode) template).withCapturedVars(capturedVars); + + // Track captureCount on captured RuntimeScalar variables. + // This mirrors what RuntimeCode.makeCodeObject() does for JVM-compiled closures. + // Without this, scopeExitCleanup() doesn't know the variable is still alive + // via this closure, and may prematurely clear weak references to its value. + java.util.List capturedScalars = new java.util.ArrayList<>(); + for (RuntimeBase captured : capturedVars) { + if (captured instanceof RuntimeScalar s) { + capturedScalars.add(s); + s.captureCount++; + } + } + if (!capturedScalars.isEmpty()) { + closureCode.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]); + closureCode.refCount = 0; } - } - if (!capturedScalars.isEmpty()) { - closureCode.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]); - closureCode.refCount = 0; - } - // Wrap in RuntimeScalar and set __SUB__ for self-reference - RuntimeScalar codeRef = new RuntimeScalar(closureCode); - closureCode.__SUB__ = codeRef; - registers[rd] = codeRef; + // 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); + // 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/JvmClosureTemplate.java b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java new file mode 100644 index 000000000..156292b79 --- /dev/null +++ b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java @@ -0,0 +1,153 @@ +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; +import java.util.List; + +/** + * 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; + + /** + * Perl attributes (e.g., "method", "lvalue"), or null + */ + public final List attributes; + + /** + * 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, prototype, packageName, null); + } + + /** + * Creates a JvmClosureTemplate for a generated class with attributes. + * + * @param generatedClass the JVM class implementing PerlSubroutine + * @param prototype Perl prototype string, or null + * @param packageName package where the sub was compiled + * @param attributes Perl attributes list, or null + */ + public JvmClosureTemplate(Class generatedClass, String prototype, String packageName, List attributes) { + this.generatedClass = generatedClass; + this.prototype = prototype; + this.packageName = packageName; + this.attributes = attributes; + + // 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; + } + if (attributes != null && !attributes.isEmpty()) { + code.attributes = new java.util.ArrayList<>(attributes); + } + 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 ae081404f..88a948a27 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,14 +33,14 @@ 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 = "ffc466124"; + public static final String gitCommitId = "7c925ee3b"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitDate = "2026-04-10"; + public static final String gitCommitDate = "2026-04-11"; /** * Build timestamp in Perl 5 "Compiled at" format (e.g., "Apr 7 2026 11:20:00"). @@ -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 22:16:43"; + public static final String buildTimestamp = "Apr 11 2026 12:39:15"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/operators/Time.java b/src/main/java/org/perlonjava/runtime/operators/Time.java index 5ad6c8102..d9c71649c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Time.java +++ b/src/main/java/org/perlonjava/runtime/operators/Time.java @@ -249,12 +249,15 @@ public static RuntimeScalar alarm(int ctx, RuntimeBase... args) { alarmTargetThread = Thread.currentThread(); currentAlarmTask = alarmScheduler.schedule(() -> { - RuntimeScalar sig = getGlobalHash("main::SIG").get("ALRM"); - if (sig.getDefinedBoolean()) { - // Queue the signal for processing in the target thread - PerlSignalQueue.enqueue("ALRM", sig); - // Interrupt the target thread to break out of blocking operations - alarmTargetThread.interrupt(); + try { + RuntimeScalar sig = getGlobalHash("main::SIG").get("ALRM"); + if (sig.getDefinedBoolean()) { + // Queue the signal for processing in the target thread + PerlSignalQueue.enqueue("ALRM", sig); + // Interrupt the target thread to break out of blocking operations + alarmTargetThread.interrupt(); + } + } finally { } }, seconds, TimeUnit.SECONDS);