Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cfd437c
fix: Use 4-byte offsets for EVAL_TRY to support large code
fglock Feb 16, 2026
3a0fc31
feat: Add CompiledCode class and unified createRuntimeCode() API
fglock Feb 16, 2026
43e56d1
test: Add large subroutine test infrastructure
fglock Feb 16, 2026
6736c14
feat: Add debug output for compilation fallback paths
fglock Feb 16, 2026
00b299b
wip: Interpreter fallback - debugging lazy compilation issue
fglock Feb 16, 2026
0fe391f
wip: Eager compilation temporarily breaks tests
fglock Feb 16, 2026
0b8f1bb
wip: Hybrid lazy/eager compilation - debugging execution issue
fglock Feb 16, 2026
614f949
fix: Properly manage RuntimeScalar references for interpreter fallback
fglock Feb 16, 2026
7388bd0
fix: Add interpreter fallback for main script compilation
fglock Feb 16, 2026
3c831f6
refactor: Unify code execution API with RuntimeCode return type
fglock Feb 16, 2026
81a4305
feat: Add PROTOTYPE opcode to interpreter
fglock Feb 16, 2026
d0509dd
feat: Add QUOTE_REGEX, LE_NUM, and GE_NUM opcodes to interpreter
fglock Feb 16, 2026
1b0c393
feat: Add missing operators to interpreter for op/signatures.t
fglock Feb 16, 2026
5adf305
feat: Add more interpreter operators (block deref, regex, chomp)
fglock Feb 16, 2026
6c9adfc
feat(interpreter): Add lvalue subroutine assignment and unary + operator
fglock Feb 16, 2026
5c6233f
fix(interpreter): Add missing disassembly cases for SET_SCALAR and sp…
fglock Feb 16, 2026
eacd507
docs(interpreter): Compress SKILL.md and add operator implementation …
fglock Feb 16, 2026
b2ad23d
fix(interpreter): Add disassembly for array operations and scope opcodes
fglock Feb 16, 2026
dd1f8dc
fix(interpreter): Add more disassembly cases and verify tableswitch
fglock Feb 16, 2026
3cca229
fix(interpreter): Fix critical PROTOTYPE PC advancement bug
fglock Feb 16, 2026
a63d076
fix: Ensure function arguments compiled in LIST context
fglock Feb 16, 2026
7aa1a01
fix: Separate SHOW_FALLBACK from USE_INTERPRETER_FALLBACK flags
fglock Feb 16, 2026
de327b3
fix: Use captured placeholder variable in lazy compilation Supplier
fglock Feb 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2,005 changes: 233 additions & 1,772 deletions dev/interpreter/SKILL.md

Large diffs are not rendered by default.

38 changes: 38 additions & 0 deletions dev/tools/gen_large_sub_test.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env perl
# Script to generate a Perl test file with a very large subroutine
# This is used to test the interpreter fallback mechanism

use strict;
use warnings;

my $num_statements = $ARGV[0] || 10000;
my $output_file = $ARGV[1] || "large_sub_test.pl";

open my $fh, '>', $output_file or die "Cannot open $output_file: $!";

print $fh "# Test file with large subroutine ($num_statements statements)\n";
print $fh "# Generated by gen_large_sub_test.pl\n\n";
print $fh "print \"1..2\\n\";\n\n";
print $fh "sub large_sub {\n";
print $fh " my \$sum = 0;\n";

for my $i (1..$num_statements) {
print $fh " \$sum += $i;\n";
}

print $fh " return \$sum;\n";
print $fh "}\n\n";

# Calculate expected sum: sum of 1 to n = n*(n+1)/2
my $expected = $num_statements * ($num_statements + 1) / 2;

print $fh "my \$result = large_sub();\n";
print $fh "print \"not \" unless \$result == $expected;\n";
print $fh "print \"ok 1 - large subroutine computed correct sum\\n\";\n\n";
print $fh "print \"not \" unless defined(&large_sub);\n";
print $fh "print \"ok 2 - large subroutine is defined\\n\";\n";

close $fh;

print "Generated $output_file with $num_statements statements\n";
print "Expected sum: $expected\n";
57 changes: 57 additions & 0 deletions src/main/java/org/perlonjava/codegen/CompiledCode.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.perlonjava.codegen;

import org.perlonjava.runtime.RuntimeCode;

import java.lang.invoke.MethodHandle;

/**
* Compiled bytecode that extends RuntimeCode.
*
* This class represents Perl code that has been compiled to JVM bytecode.
* It wraps the generated Class<?> and provides the same RuntimeCode interface
* as InterpretedCode, enabling seamless switching between compiler and interpreter.
*
* DESIGN: Following the InterpretedCode pattern:
* - InterpretedCode stores bytecode[] and overrides apply() to call BytecodeInterpreter
* - CompiledCode stores Class<?> and uses parent apply() to call MethodHandle
*
* This allows the EmitterMethodCreator.createRuntimeCode() factory to return either
* CompiledCode or InterpretedCode based on whether compilation succeeded or fell
* back to the interpreter.
*/
public class CompiledCode extends RuntimeCode {
// The generated JVM class (useful for debugging and EmitSubroutine bytecode generation)
public final Class<?> generatedClass;

// The compiler context used to create this code (may be useful for debugging)
public final EmitterContext compileContext;

/**
* Constructor for CompiledCode.
*
* @param methodHandle The MethodHandle for the apply() method
* @param codeObject The instance of the generated class (with closure variables)
* @param prototype The subroutine prototype (e.g., "$" for one scalar parameter)
* @param generatedClass The compiled JVM class
* @param compileContext The compiler context (optional, for debugging)
*/
public CompiledCode(MethodHandle methodHandle, Object codeObject,
String prototype, Class<?> generatedClass,
EmitterContext compileContext) {
super(methodHandle, codeObject, prototype);
this.generatedClass = generatedClass;
this.compileContext = compileContext;
}

// No need to override apply() - parent RuntimeCode implementation works perfectly
// The MethodHandle dispatches to compiled JVM bytecode automatically

@Override
public String toString() {
return "CompiledCode{" +
"class=" + (generatedClass != null ? generatedClass.getName() : "null") +
", prototype='" + prototype + '\'' +
", defined=" + defined() +
'}';
}
}
190 changes: 187 additions & 3 deletions src/main/java/org/perlonjava/codegen/EmitterMethodCreator.java
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,28 @@ public static Class<?> createClassWithMethod(EmitterContext ctx, Node ast, boole

public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCatch) {
boolean asmDebug = System.getenv("JPERL_ASM_DEBUG") != null;
boolean showFallback = System.getenv("JPERL_SHOW_FALLBACK") != null ||
System.getenv("JPERL_USE_INTERPRETER_FALLBACK") != null;
boolean useInterpreterFallback = System.getenv("JPERL_USE_INTERPRETER_FALLBACK") != null;

try {
return getBytecodeInternal(ctx, ast, useTryCatch, false);
} catch (MethodTooLargeException tooLarge) {
// Automatic retry with refactoring on "Method too large" error
// When interpreter fallback is enabled, skip AST splitter and let exception propagate
// The interpreter has no size limits, so AST splitting is unnecessary
if (useInterpreterFallback) {
if (showFallback) {
System.err.println("Note: Method too large, skipping AST splitter (interpreter fallback enabled).");
}
throw tooLarge; // Propagate to createRuntimeCode() which will use interpreter
}

// Automatic retry with AST splitting when interpreter fallback is not enabled
try {
// Notify user that automatic refactoring is happening
// System.err.println("Note: Method too large, retrying with automatic refactoring.");
if (showFallback) {
System.err.println("Note: Method too large, retrying with AST splitter (automatic refactoring).");
}

// First, try depth-first literal refactoring (refactors nested structures first)
org.perlonjava.astvisitor.DepthFirstLiteralRefactorVisitor.refactor(ast);
Expand All @@ -372,9 +387,16 @@ public static byte[] getBytecode(EmitterContext ctx, Node ast, boolean useTryCat
ctx.clearContextCache();
}

return getBytecodeInternal(ctx, ast, useTryCatch, false);
byte[] result = getBytecodeInternal(ctx, ast, useTryCatch, false);
if (showFallback) {
System.err.println("Note: AST splitter succeeded.");
}
return result;
} catch (MethodTooLargeException retryTooLarge) {
// Refactoring didn't help enough - give up
if (showFallback) {
System.err.println("Note: AST splitter failed, propagating exception.");
}
throw retryTooLarge;
} catch (Throwable retryError) {
// Refactoring caused a different error - report both
Expand Down Expand Up @@ -1451,6 +1473,168 @@ public static Class<?> loadBytecode(EmitterContext ctx, byte[] classData) {
return loader.defineClass(javaClassNameDot, classData);
}

// Feature flag for interpreter fallback
private static final boolean USE_INTERPRETER_FALLBACK =
System.getenv("JPERL_USE_INTERPRETER_FALLBACK") != null;
private static final boolean SHOW_FALLBACK =
System.getenv("JPERL_SHOW_FALLBACK") != null;

/**
* Unified factory method that returns RuntimeCode (either CompiledCode or InterpretedCode).
*
* This is the NEW API that replaces createClassWithMethod() for most use cases.
* It handles the "Method too large" exception by falling back to the interpreter
* when JPERL_USE_INTERPRETER_FALLBACK environment variable is set.
*
* DESIGN:
* - Try compiler first (createClassWithMethod)
* - On MethodTooLargeException: fall back to interpreter if flag enabled
* - Return CompiledCode or InterpretedCode (both extend RuntimeCode)
* - Call sites work with RuntimeCode interface, don't need to know which backend was used
*
* @param ctx The emitter context containing information for code generation
* @param ast The abstract syntax tree representing the method body
* @param useTryCatch Flag to enable try-catch in the generated class (for eval operator)
* @return RuntimeCode that can be either CompiledCode or InterpretedCode
*/
public static org.perlonjava.runtime.RuntimeCode createRuntimeCode(
EmitterContext ctx, Node ast, boolean useTryCatch) {
try {
// Try compiler path
Class<?> generatedClass = createClassWithMethod(ctx, ast, useTryCatch);
if (SHOW_FALLBACK) {
System.err.println("Note: JVM compilation succeeded.");
}
return wrapAsCompiledCode(generatedClass, ctx);

} catch (MethodTooLargeException e) {
if (USE_INTERPRETER_FALLBACK) {
// Fall back to interpreter
System.err.println("Note: Method too large after AST splitting, using interpreter backend.");
return compileToInterpreter(ast, ctx, useTryCatch);
}

// If interpreter fallback disabled, re-throw to use existing AST splitter logic
throw e;
}
}

/**
* Wrap a compiled Class<?> as CompiledCode.
*
* This performs the same reflection steps that SubroutineParser.java currently does:
* 1. Get constructor
* 2. Create instance (codeObject)
* 3. Get MethodHandle for apply method
* 4. Set __SUB__ field
* 5. Return CompiledCode wrapper
*
* @param generatedClass The compiled JVM class
* @param ctx The compiler context
* @return CompiledCode wrapping the compiled class
*/
private static CompiledCode wrapAsCompiledCode(Class<?> generatedClass, EmitterContext ctx) {
try {
// Get the constructor (may have parameters for captured variables)
String[] env = (ctx.capturedEnv != null) ? ctx.capturedEnv : ctx.symbolTable.getVariableNames();

// Build parameter types for constructor
Class<?>[] parameterTypes = new Class<?>[Math.max(0, env.length - skipVariables)];
for (int i = skipVariables; i < env.length; i++) {
String descriptor = getVariableDescriptor(env[i]);
String className = descriptor.substring(1, descriptor.length() - 1).replace('/', '.');
parameterTypes[i - skipVariables] = Class.forName(className);
}

Constructor<?> constructor = generatedClass.getConstructor(parameterTypes);

// For now, we don't instantiate - that happens later when captured vars are available
// This is used for the factory pattern where the caller provides the parameters
// So we return a CompiledCode with null codeObject and null methodHandle
// The caller will instantiate it with the captured variables

// Actually, let's check if there are NO captured variables, then we can instantiate now
Object codeObject = null;
java.lang.invoke.MethodHandle methodHandle = null;

if (parameterTypes.length == 0) {
// No captured variables, can instantiate now
codeObject = constructor.newInstance();

// Get MethodHandle for apply method
methodHandle = org.perlonjava.runtime.RuntimeCode.lookup.findVirtual(
generatedClass, "apply", org.perlonjava.runtime.RuntimeCode.methodType
);

// Set __SUB__ field
java.lang.reflect.Field field = generatedClass.getDeclaredField("__SUB__");
org.perlonjava.runtime.RuntimeScalar selfRef = new org.perlonjava.runtime.RuntimeScalar();
selfRef.type = org.perlonjava.runtime.RuntimeScalarType.CODE;
// Note: ctx doesn't have prototype field, it's set separately by caller
selfRef.value = new CompiledCode(methodHandle, codeObject, null, generatedClass, ctx);
field.set(codeObject, selfRef);

return (CompiledCode) selfRef.value;
} else {
// Has captured variables - caller must instantiate later
// Return a CompiledCode with null codeObject/methodHandle
// The caller will fill these in via reflection (see SubroutineParser pattern)
return new CompiledCode(null, null, null, generatedClass, ctx);
}

} catch (Exception e) {
throw new org.perlonjava.runtime.PerlCompilerException(
"Failed to wrap compiled class: " + e.getMessage());
}
}

/**
* Compile AST to interpreter bytecode.
*
* This is the fallback path when JVM bytecode generation hits the 65535 byte limit.
* The interpreter has no size limits because it doesn't generate JVM bytecode.
*
* @param ast The AST to compile
* @param ctx The compiler context
* @param useTryCatch Whether to use try-catch (for eval)
* @return InterpretedCode ready to execute
*/
private static org.perlonjava.interpreter.InterpretedCode compileToInterpreter(
Node ast, EmitterContext ctx, boolean useTryCatch) {

// Create bytecode compiler
org.perlonjava.interpreter.BytecodeCompiler compiler =
new org.perlonjava.interpreter.BytecodeCompiler(
ctx.errorUtil.getFileName(),
1, // line number
ctx.errorUtil
);

// Compile AST to interpreter bytecode
org.perlonjava.interpreter.InterpretedCode code = compiler.compile(ast);

// Handle captured variables if needed (for closures)
if (ctx.capturedEnv != null && ctx.capturedEnv.length > skipVariables) {
// Extract captured variables from context
// Note: This is a simplified version - full implementation would need to
// access the actual RuntimeBase objects from the symbol table
org.perlonjava.runtime.RuntimeBase[] capturedVars =
new org.perlonjava.runtime.RuntimeBase[ctx.capturedEnv.length - skipVariables];

// For now, initialize with undef (actual values will be set by caller)
for (int i = 0; i < capturedVars.length; i++) {
capturedVars[i] = new org.perlonjava.runtime.RuntimeScalar();
}

code = code.withCapturedVars(capturedVars);
}

// Note: prototype will be set by caller if needed
// code.prototype is set via RuntimeCode fields

return code;
}

public static void debugInspectClass(Class<?> generatedClass) {
System.out.println("Class Information for: " + generatedClass.getName());
System.out.println("===========================================");
Expand Down
Loading