From a175c271e464305a8d4dc1b179af7910e7c89001 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 12:29:14 +0100 Subject: [PATCH 01/10] Add Compilable interface to PerlScriptEngine and unified benchmark MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement JSR 223 Compilable interface for compile-once-execute-many pattern: - PerlScriptEngine now implements Compilable interface - PerlCompiledScript stores compiled code and uses MethodHandle for execution - PerlLanguageProvider.compilePerlCode() compiles without executing - Avoids ClassLoader issues by using Object + MethodHandle instead of casting Update ForLoopBenchmark to compare interpreter vs compiler in single run: - Uses JSR 223 Compilable to compile once and execute 10,000 times - Eliminates compilation overhead from benchmark measurements - Shows both benchmarks with identical JVM warmup conditions - Displays performance comparison and checks against target (2-5x slower) Benchmark results: - Interpreter: 47.05 million ops/sec - Compiler: 91.64 million ops/sec - Ratio: 1.95x slower (within 2-5x target ✓) - Interpreter achieves 51.3% of compiler speed Benefits: 1. Fair performance comparison (both use same JVM session) 2. No subprocess overhead 3. Compile once, execute many (realistic usage pattern) 4. Validates no performance regression from recent changes Co-Authored-By: Claude Opus 4.6 --- .../interpreter/ForLoopBenchmark.java | 156 ++++++++++++++---- .../scriptengine/PerlCompiledScript.java | 86 ++++++++++ .../scriptengine/PerlLanguageProvider.java | 65 ++++++++ .../scriptengine/PerlScriptEngine.java | 65 +++++++- 4 files changed, 333 insertions(+), 39 deletions(-) create mode 100644 src/main/java/org/perlonjava/scriptengine/PerlCompiledScript.java diff --git a/src/main/java/org/perlonjava/interpreter/ForLoopBenchmark.java b/src/main/java/org/perlonjava/interpreter/ForLoopBenchmark.java index cf3db0c01..ac69f6dce 100644 --- a/src/main/java/org/perlonjava/interpreter/ForLoopBenchmark.java +++ b/src/main/java/org/perlonjava/interpreter/ForLoopBenchmark.java @@ -10,13 +10,21 @@ import org.perlonjava.CompilerOptions; import org.perlonjava.symbols.ScopedSymbolTable; +import javax.script.ScriptEngine; +import javax.script.ScriptEngineManager; +import javax.script.ScriptException; +import javax.script.Compilable; +import javax.script.CompiledScript; import java.util.List; /** - * Benchmark for loop performance: Interpreter + * Benchmark for loop performance: Interpreter vs Compiler * - * This benchmark measures interpreter performance on loop-heavy code. - * Compare with compiler by running: ./jperl -MTime::HiRes /tmp/bench.pl + * This benchmark measures both interpreter and compiler performance on loop-heavy code + * using the same JVM session for fair comparison. + * + * The compiler is invoked via JSR 223 (Java Scripting API), ensuring identical + * warmup and execution conditions. */ public class ForLoopBenchmark { @@ -44,58 +52,140 @@ private static InterpretedCode compileCode(String perlCode) throws Exception { } public static void main(String[] args) throws Exception { - System.out.println("=== For Loop Benchmark: Interpreter ===\n"); + System.out.println("=== For Loop Benchmark: Interpreter vs Compiler ===\n"); - // Test code: for loop with 1000 iterations (enough for JVM warmup) + // Test code: for loop with 1000 iterations String code = "my $sum = 0; for (my $i = 0; $i < 1000; $i++) { $sum = $sum + $i; } $sum"; + int iterations = 10000; // 10k iterations for stable measurement + int loop_size = 1000; // 1000 operations per iteration + + // ===================================================================== + // INTERPRETER BENCHMARK + // ===================================================================== + + System.out.println("--- Interpreter Benchmark ---\n"); + // Compile once InterpretedCode interpretedCode = compileCode(code); if (DEBUG) { - // Print disassembly to analyze overhead System.out.println(interpretedCode.disassemble()); - - // Show raw bytecode to verify opcodes - System.out.println("Raw bytecode (hex):"); - for (int i = 0; i < Math.min(interpretedCode.bytecode.length, 60); i++) { - System.out.printf("%02x ", interpretedCode.bytecode[i] & 0xFF); - if ((i + 1) % 16 == 0) System.out.println(); - } - System.out.println("\n"); } - // Warm up JIT (more iterations for better optimization) - System.out.println("Warming up JIT..."); + // Warm up JIT + System.out.println("Warming up interpreter JIT..."); RuntimeArray emptyArgs = new RuntimeArray(); for (int i = 0; i < 1000; i++) { interpretedCode.apply(emptyArgs, RuntimeContextType.SCALAR); } - // Actual benchmark - System.out.println("Running benchmark...\n"); - - int iterations = 10000; // 10x more iterations for stable measurement - int loop_size = 1000; // Increased from 100 to 1000 for better JIT warmup - + // Benchmark interpreter + System.out.println("Running interpreter benchmark...\n"); long start = System.nanoTime(); for (int iter = 0; iter < iterations; iter++) { interpretedCode.apply(emptyArgs, RuntimeContextType.SCALAR); } - long elapsed = System.nanoTime() - start; - long elapsed_interpreter = elapsed; // Save for comparison + long elapsed_interpreter = System.nanoTime() - start; - double seconds = elapsed / 1_000_000_000.0; + double seconds_interp = elapsed_interpreter / 1_000_000_000.0; long total_ops = (long) iterations * loop_size; - double ops_per_sec = total_ops / seconds; + double ops_per_sec_interp = total_ops / seconds_interp; + + System.out.printf("Interpreter Results:\n"); + System.out.printf(" Iterations: %,d\n", iterations); + System.out.printf(" Loop size: %,d\n", loop_size); + System.out.printf(" Total operations: %,d\n", total_ops); + System.out.printf(" Elapsed time: %.6f seconds\n", seconds_interp); + System.out.printf(" Operations/sec: %.2f million\n\n", ops_per_sec_interp / 1_000_000); + + // ===================================================================== + // COMPILER BENCHMARK (via JSR 223 Compilable) + // ===================================================================== + + System.out.println("--- Compiler Benchmark (JSR 223 Compilable) ---\n"); + + // Get Perl script engine + ScriptEngineManager manager = new ScriptEngineManager(); + ScriptEngine engine = manager.getEngineByName("perl"); - System.out.printf("Iterations: %d\n", iterations); - System.out.printf("Loop size: %d\n", loop_size); - System.out.printf("Total operations: %d\n", total_ops); - System.out.printf("Elapsed time: %.6f seconds\n", seconds); - System.out.printf("Operations/sec: %.2f million\n\n", ops_per_sec / 1_000_000); + if (engine == null) { + System.err.println("ERROR: Perl script engine not found!"); + System.err.println("Make sure PerlScriptEngineFactory is registered."); + System.exit(1); + } + + // Compile once using Compilable interface + System.out.println("Compiling code once via Compilable interface..."); + CompiledScript compiledScript = null; + if (engine instanceof Compilable) { + try { + compiledScript = ((Compilable) engine).compile(code); + System.out.println("Compilation successful\n"); + } catch (ScriptException e) { + System.err.println("Compilation failed: " + e.getMessage()); + throw e; + } + } else { + System.err.println("ERROR: Engine doesn't support Compilable interface"); + System.exit(1); + } + + // Warm up compiler JIT + System.out.println("Warming up compiler JIT..."); + for (int i = 0; i < 1000; i++) { + try { + compiledScript.eval(); + } catch (ScriptException e) { + System.err.println("Compiler warmup failed: " + e.getMessage()); + throw e; + } + } - System.out.println("Compare with compiler:"); - System.out.println(" ./jperl -MTime::HiRes /tmp/bench.pl"); + // Benchmark compiled code + System.out.println("Running compiler benchmark...\n"); + long start_compiler = System.nanoTime(); + for (int iter = 0; iter < iterations; iter++) { + try { + compiledScript.eval(); + } catch (ScriptException e) { + System.err.println("Compiler benchmark failed: " + e.getMessage()); + throw e; + } + } + long elapsed_compiler = System.nanoTime() - start_compiler; + + double seconds_compiler = elapsed_compiler / 1_000_000_000.0; + double ops_per_sec_compiler = total_ops / seconds_compiler; + + System.out.printf("Compiler Results:\n"); + System.out.printf(" Iterations: %,d\n", iterations); + System.out.printf(" Loop size: %,d\n", loop_size); + System.out.printf(" Total operations: %,d\n", total_ops); + System.out.printf(" Elapsed time: %.6f seconds\n", seconds_compiler); + System.out.printf(" Operations/sec: %.2f million\n\n", ops_per_sec_compiler / 1_000_000); + + // ===================================================================== + // COMPARISON + // ===================================================================== + + System.out.println("=== Performance Comparison ===\n"); + + double speedup = ops_per_sec_compiler / ops_per_sec_interp; + double interpreter_percent = (ops_per_sec_interp / ops_per_sec_compiler) * 100; + + System.out.printf("Compiler: %.2f million ops/sec\n", ops_per_sec_compiler / 1_000_000); + System.out.printf("Interpreter: %.2f million ops/sec\n", ops_per_sec_interp / 1_000_000); + System.out.printf("\n"); + System.out.printf("Interpreter is %.2fx slower than compiler\n", speedup); + System.out.printf("Interpreter achieves %.1f%% of compiler speed\n", interpreter_percent); + System.out.printf("\n"); + + // Check against target + if (speedup <= 5.0) { + System.out.println("✓ Within target range (2-5x slower)"); + } else { + System.out.println("✗ Outside target range (2-5x slower)"); + } } } diff --git a/src/main/java/org/perlonjava/scriptengine/PerlCompiledScript.java b/src/main/java/org/perlonjava/scriptengine/PerlCompiledScript.java new file mode 100644 index 000000000..4be220d63 --- /dev/null +++ b/src/main/java/org/perlonjava/scriptengine/PerlCompiledScript.java @@ -0,0 +1,86 @@ +package org.perlonjava.scriptengine; + +import org.perlonjava.runtime.RuntimeArray; +import org.perlonjava.runtime.RuntimeList; +import org.perlonjava.runtime.RuntimeContextType; +import org.perlonjava.runtime.RuntimeCode; + +import javax.script.CompiledScript; +import javax.script.ScriptContext; +import javax.script.ScriptEngine; +import javax.script.ScriptException; +import java.lang.invoke.MethodHandle; + +/** + * PerlCompiledScript represents a pre-compiled Perl script that can be executed multiple times. + * + * This class implements the CompiledScript interface from JSR 223, allowing Perl code to be + * compiled once and executed many times, which significantly improves performance when the same + * script needs to be run repeatedly. + * + * The compiled code is stored as a generated class instance and invoked via MethodHandle to + * avoid ClassLoader issues. + */ +public class PerlCompiledScript extends CompiledScript { + + private final PerlScriptEngine engine; + private final Object compiledInstance; // Compiled code instance + private final MethodHandle invoker; // MethodHandle to invoke apply() + + /** + * Creates a new PerlCompiledScript. + * + * @param engine The script engine that compiled this script + * @param compiledInstance The compiled code instance + * @throws Exception if MethodHandle creation fails + */ + public PerlCompiledScript(PerlScriptEngine engine, Object compiledInstance) throws Exception { + this.engine = engine; + this.compiledInstance = compiledInstance; + + // Create MethodHandle for apply() method + Class instanceClass = compiledInstance.getClass(); + this.invoker = RuntimeCode.lookup.findVirtual(instanceClass, "apply", RuntimeCode.methodType); + } + + /** + * Executes the compiled script. + * + * @param context The script context (bindings, readers, writers) + * @return The result of script execution + * @throws ScriptException if execution fails + */ + @Override + public Object eval(ScriptContext context) throws ScriptException { + try { + // Execute the compiled code with empty arguments via MethodHandle + RuntimeArray args = new RuntimeArray(); + RuntimeList result = (RuntimeList) invoker.invoke(compiledInstance, args, RuntimeContextType.SCALAR); + return result != null ? result.toString() : null; + } catch (Throwable t) { + ScriptException scriptException = new ScriptException("Error executing compiled Perl script: " + t.getMessage()); + scriptException.initCause(t); + throw scriptException; + } + } + + /** + * Returns the script engine that created this compiled script. + * + * @return The PerlScriptEngine instance + */ + @Override + public ScriptEngine getEngine() { + return engine; + } + + /** + * Gets the underlying compiled code instance. + * This can be useful for advanced use cases. + * + * @return The compiled code instance + */ + public Object getCompiledInstance() { + return compiledInstance; + } +} diff --git a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java index 10d900dba..3d9d78194 100644 --- a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java @@ -302,5 +302,70 @@ private static RuntimeList executeGeneratedClass(Class generatedClass, Emitte RuntimeIO.flushAllHandles(); return result; } + + /** + * Compiles Perl code to RuntimeCode without executing it. + * This allows compilation once and execution multiple times for better performance. + * + * @param compilerOptions Compiler flags, file name and source code + * @return The compiled code instance (can be used with RuntimeCode.apply via MethodHandle) + * @throws Exception if compilation fails + */ + public static Object compilePerlCode(CompilerOptions compilerOptions) throws Exception { + ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable(); + globalSymbolTable.enterScope(); + globalSymbolTable.addVariable("this", "", null); // anon sub instance is local variable 0 + globalSymbolTable.addVariable("@_", "our", null); // Argument list is local variable 1 + globalSymbolTable.addVariable("wantarray", "", null); // Call context is local variable 2 + + if (compilerOptions.codeHasEncoding) { + globalSymbolTable.enableStrictOption(Strict.HINT_UTF8); + } + + EmitterContext ctx = new EmitterContext( + new JavaClassInfo(), + globalSymbolTable.snapShot(), + null, + null, + RuntimeContextType.SCALAR, // Default to SCALAR context + true, + null, + compilerOptions, + new RuntimeArray() + ); + + if (!globalInitialized) { + GlobalContext.initializeGlobals(compilerOptions); + globalInitialized = true; + } + + // Tokenize + Lexer lexer = new Lexer(compilerOptions.code); + List tokens = lexer.tokenize(); + compilerOptions.code = null; // Free memory + + // Parse + ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); + Parser parser = new Parser(ctx, tokens); + parser.isTopLevelScript = false; // Not top-level for compiled script + Node ast = parser.parse(); + + // Compile to class + ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); + ctx.symbolTable = ctx.symbolTable.snapShot(); + SpecialBlockParser.setCurrentScope(ctx.symbolTable); + Class generatedClass = EmitterMethodCreator.createClassWithMethod( + ctx, + ast, + false // no try-catch + ); + + // Create instance and return it (no cast to avoid ClassLoader issues) + Constructor constructor = generatedClass.getConstructor(); + Object instance = constructor.newInstance(); + + // Return instance directly (will be used with MethodHandle) + return instance; + } } diff --git a/src/main/java/org/perlonjava/scriptengine/PerlScriptEngine.java b/src/main/java/org/perlonjava/scriptengine/PerlScriptEngine.java index 3600eb719..f9d4a3a21 100644 --- a/src/main/java/org/perlonjava/scriptengine/PerlScriptEngine.java +++ b/src/main/java/org/perlonjava/scriptengine/PerlScriptEngine.java @@ -8,7 +8,9 @@ import java.io.StringWriter; /** - * The PerlScriptEngine class is a custom implementation of the AbstractScriptEngine. + * The PerlScriptEngine class is a custom implementation of the AbstractScriptEngine + * that supports the Compilable interface for improved performance. + * * It allows the execution of Perl scripts within the Java environment using the Java Scripting API (JSR 223). *

* This class provides the necessary methods to evaluate Perl scripts, manage script contexts, and integrate @@ -16,13 +18,14 @@ *

* Key functionalities include: * - Evaluating Perl scripts from strings or readers. + * - Compiling Perl scripts once and executing multiple times (via Compilable interface). * - Managing script contexts to handle variable bindings and input/output streams. * - Providing a factory method to obtain the associated ScriptEngineFactory. *

- * By extending AbstractScriptEngine, PerlScriptEngine inherits basic script engine functionalities and focuses on - * implementing the specifics of Perl script execution. + * By extending AbstractScriptEngine and implementing Compilable, PerlScriptEngine provides both + * direct evaluation and pre-compilation capabilities for optimal performance. */ -public class PerlScriptEngine extends AbstractScriptEngine { +public class PerlScriptEngine extends AbstractScriptEngine implements Compilable { private final ScriptEngineFactory factory; @@ -63,6 +66,57 @@ public Object eval(Reader reader, ScriptContext context) throws ScriptException return eval(writer.toString(), context); } + /** + * Compile a Perl script for reuse. + * This allows compilation once and execution multiple times, improving performance. + * + * @param script The Perl code to compile + * @return A CompiledScript that can be executed multiple times + * @throws ScriptException if compilation fails + */ + @Override + public CompiledScript compile(String script) throws ScriptException { + try { + CompilerOptions options = new CompilerOptions(); + options.fileName = ""; + options.code = script; + + // Compile to code instance (Object to avoid ClassLoader issues) + Object compiledCode = PerlLanguageProvider.compilePerlCode(options); + + // Create PerlCompiledScript with MethodHandle for invocation + return new PerlCompiledScript(this, compiledCode); + } catch (Throwable t) { + ScriptException scriptException = new ScriptException("Error compiling Perl script: " + t.getMessage()); + scriptException.initCause(t); + throw scriptException; + } + } + + /** + * Compile a Perl script from a Reader. + * + * @param reader The Reader containing Perl code + * @return A CompiledScript that can be executed multiple times + * @throws ScriptException if compilation fails + */ + @Override + public CompiledScript compile(Reader reader) throws ScriptException { + StringWriter writer = new StringWriter(); + char[] buffer = new char[1024]; + int numRead; + try { + while ((numRead = reader.read(buffer)) != -1) { + writer.write(buffer, 0, numRead); + } + } catch (Exception e) { + ScriptException scriptException = new ScriptException("Error reading script"); + scriptException.initCause(e); + throw scriptException; + } + return compile(writer.toString()); + } + @Override public Bindings createBindings() { return new SimpleBindings(); @@ -72,5 +126,4 @@ public Bindings createBindings() { public ScriptEngineFactory getFactory() { return factory; } -} - +} \ No newline at end of file From 421b0033c5a25d84c95aca4aaf41af39dc6a6845 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 12:53:00 +0100 Subject: [PATCH 02/10] Add --interpreter switch for bytecode interpreter backend selection Implement unified execution architecture leveraging RuntimeCode API for seamless switching between JVM compiler and bytecode interpreter. Changes: - Add CompilerOptions.useInterpreter flag for backend selection - Add --interpreter command-line switch in ArgumentParser - Refactor PerlLanguageProvider to use unified compilation/execution: - New compileToExecutable() method that returns RuntimeCode-compatible instances (InterpretedCode or compiled class instance) - Refactor executeGeneratedClass() to executeCode() that accepts Object and uses MethodHandle to invoke apply() on both backend types - Update executePerlCode(), executePerlAST(), and compilePerlCode() to use unified path Key Benefits: - Zero code duplication for BEGIN/UNITCHECK/CHECK/INIT/END blocks - Transparent backend switching via single flag - JSR 223 Compilable interface automatically supports both backends - InterpretedCode extends RuntimeCode, providing identical API Testing: - All unit tests pass (make test-unit) - Compiler backend works: ./jperl -e 'code' - Interpreter backend works: ./jperl --interpreter -e 'code' - Both backends execute C-style for loops correctly Co-Authored-By: Claude Opus 4.6 --- .../java/org/perlonjava/ArgumentParser.java | 5 + .../java/org/perlonjava/CompilerOptions.java | 3 + .../scriptengine/PerlLanguageProvider.java | 93 ++++++++++++------- 3 files changed, 66 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/perlonjava/ArgumentParser.java b/src/main/java/org/perlonjava/ArgumentParser.java index 4a2b50662..be5652e8d 100644 --- a/src/main/java/org/perlonjava/ArgumentParser.java +++ b/src/main/java/org/perlonjava/ArgumentParser.java @@ -918,6 +918,10 @@ private static int processLongSwitches(String[] args, CompilerOptions parsedArgs validateExclusiveOptions(parsedArgs, "disassemble"); parsedArgs.disassembleEnabled = true; break; + case "--interpreter": + // Use bytecode interpreter instead of JVM compiler + parsedArgs.useInterpreter = true; + break; case "--help": // Print help message and exit printHelp(); @@ -1066,6 +1070,7 @@ private static void printHelp() { System.out.println(" --tokenize tokenize the input code"); System.out.println(" --parse parse the input code"); System.out.println(" --disassemble disassemble the generated code"); + System.out.println(" --interpreter use bytecode interpreter instead of JVM compiler"); System.out.println(" -h, --help displays this help message"); System.out.println(); // System.out.println("Unicode/encoding flags for -C:"); diff --git a/src/main/java/org/perlonjava/CompilerOptions.java b/src/main/java/org/perlonjava/CompilerOptions.java index f1ee74b87..d79b2d715 100644 --- a/src/main/java/org/perlonjava/CompilerOptions.java +++ b/src/main/java/org/perlonjava/CompilerOptions.java @@ -18,6 +18,7 @@ * Fields: * - debugEnabled: Enables debug mode, providing detailed logging during compilation. * - disassembleEnabled: If true, the compiler will disassemble the generated bytecode. + * - useInterpreter: If true, use the bytecode interpreter instead of JVM compiler. * - tokenizeOnly: If true, the compiler will only tokenize the input and stop. * - parseOnly: If true, the compiler will only parse the input and stop. * - compileOnly: If true, the compiler will compile the input but won't execute it. @@ -34,6 +35,7 @@ public class CompilerOptions implements Cloneable { public boolean debugEnabled = false; public boolean disassembleEnabled = false; + public boolean useInterpreter = false; public boolean tokenizeOnly = false; public boolean parseOnly = false; public boolean compileOnly = false; @@ -94,6 +96,7 @@ public String toString() { return "CompilerOptions{\n" + " debugEnabled=" + debugEnabled + ",\n" + " disassembleEnabled=" + disassembleEnabled + ",\n" + + " useInterpreter=" + useInterpreter + ",\n" + " tokenizeOnly=" + tokenizeOnly + ",\n" + " parseOnly=" + parseOnly + ",\n" + " compileOnly=" + compileOnly + ",\n" + diff --git a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java index 3d9d78194..91f3cf031 100644 --- a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java @@ -5,6 +5,8 @@ import org.perlonjava.codegen.EmitterContext; import org.perlonjava.codegen.EmitterMethodCreator; import org.perlonjava.codegen.JavaClassInfo; +import org.perlonjava.interpreter.BytecodeCompiler; +import org.perlonjava.interpreter.InterpretedCode; import org.perlonjava.lexer.Lexer; import org.perlonjava.lexer.LexerToken; import org.perlonjava.parser.DataSection; @@ -179,12 +181,12 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, // loses those declarations and causes strict-vars failures during codegen. ctx.symbolTable = ctx.symbolTable.snapShot(); SpecialBlockParser.setCurrentScope(ctx.symbolTable); - Class generatedClass = EmitterMethodCreator.createClassWithMethod( - ctx, - ast, - false // no try-catch - ); - return executeGeneratedClass(generatedClass, ctx, isTopLevelScript, callerContext); + + // Compile to executable (compiler or interpreter based on flag) + Object codeInstance = compileToExecutable(ast, ctx); + + // Execute (unified path for both backends) + return executeCode(codeInstance, ctx, isTopLevelScript, callerContext); } /** @@ -231,26 +233,25 @@ public static RuntimeList executePerlAST(Node ast, // Snapshot the symbol table as seen by the parser (includes lexical decls + pragma state). ctx.symbolTable = ctx.symbolTable.snapShot(); SpecialBlockParser.setCurrentScope(ctx.symbolTable); - Class generatedClass = EmitterMethodCreator.createClassWithMethod( - ctx, - ast, - false - ); + + // Compile to executable (compiler or interpreter based on flag) + Object codeInstance = compileToExecutable(ast, ctx); // executePerlAST is always called from BEGIN blocks which use VOID context - return executeGeneratedClass(generatedClass, ctx, false, RuntimeContextType.VOID); + return executeCode(codeInstance, ctx, false, RuntimeContextType.VOID); } /** - * Common method to execute the generated class and return the result. + * Common method to execute compiled code and return the result. + * Works with both interpreter (InterpretedCode) and compiler (generated class instance). * - * @param generatedClass The generated Java class. - * @param ctx The emitter context. - * @param isMainProgram Indicates if this is the main program. - * @param callerContext The calling context (VOID, SCALAR, LIST) or -1 for default + * @param codeInstance The compiled code instance (InterpretedCode or generated class) + * @param ctx The emitter context. + * @param isMainProgram Indicates if this is the main program. + * @param callerContext The calling context (VOID, SCALAR, LIST) or -1 for default * @return The result of the Perl code execution. */ - private static RuntimeList executeGeneratedClass(Class generatedClass, EmitterContext ctx, boolean isMainProgram, int callerContext) throws Exception { + private static RuntimeList executeCode(Object codeInstance, EmitterContext ctx, boolean isMainProgram, int callerContext) throws Exception { runUnitcheckBlocks(ctx.unitcheckBlocks); if (isMainProgram) { runCheckBlocks(); @@ -260,10 +261,9 @@ private static RuntimeList executeGeneratedClass(Class generatedClass, Emitte return null; } - Constructor constructor = generatedClass.getConstructor(); - Object instance = constructor.newInstance(); - - MethodHandle invoker = RuntimeCode.lookup.findVirtual(generatedClass, "apply", RuntimeCode.methodType); + // Get MethodHandle for apply() - works for both RuntimeCode subclasses and generated classes + Class codeClass = codeInstance.getClass(); + MethodHandle invoker = RuntimeCode.lookup.findVirtual(codeClass, "apply", RuntimeCode.methodType); RuntimeList result; try { @@ -274,7 +274,7 @@ private static RuntimeList executeGeneratedClass(Class generatedClass, Emitte // Use the caller's context if specified, otherwise use default behavior int executionContext = callerContext >= 0 ? callerContext : (isMainProgram ? RuntimeContextType.VOID : RuntimeContextType.SCALAR); - result = (RuntimeList) invoker.invoke(instance, new RuntimeArray(), executionContext); + result = (RuntimeList) invoker.invoke(codeInstance, new RuntimeArray(), executionContext); try { if (isMainProgram) { @@ -303,6 +303,38 @@ private static RuntimeList executeGeneratedClass(Class generatedClass, Emitte return result; } + /** + * Compiles Perl AST to an executable instance (compiler or interpreter). + * This method provides a unified compilation path that chooses the backend + * based on CompilerOptions.useInterpreter. + * + * @param ast The abstract syntax tree to compile + * @param ctx The emitter context + * @return Object that has apply() method - either InterpretedCode or compiled class instance + * @throws Exception if compilation fails + */ + private static Object compileToExecutable(Node ast, EmitterContext ctx) throws Exception { + if (ctx.compilerOptions.useInterpreter) { + // Interpreter path - returns InterpretedCode (extends RuntimeCode) + ctx.logDebug("Compiling to bytecode interpreter"); + BytecodeCompiler compiler = new BytecodeCompiler( + ctx.compilerOptions.fileName, + 1 // tokenIndex for error reporting + ); + return compiler.compile(ast); + } else { + // Compiler path - returns generated class instance + ctx.logDebug("Compiling to JVM bytecode"); + Class generatedClass = EmitterMethodCreator.createClassWithMethod( + ctx, + ast, + false // no try-catch + ); + Constructor constructor = generatedClass.getConstructor(); + return constructor.newInstance(); + } + } + /** * Compiles Perl code to RuntimeCode without executing it. * This allows compilation once and execution multiple times for better performance. @@ -350,22 +382,13 @@ public static Object compilePerlCode(CompilerOptions compilerOptions) throws Exc parser.isTopLevelScript = false; // Not top-level for compiled script Node ast = parser.parse(); - // Compile to class + // Compile to class or bytecode based on flag ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); ctx.symbolTable = ctx.symbolTable.snapShot(); SpecialBlockParser.setCurrentScope(ctx.symbolTable); - Class generatedClass = EmitterMethodCreator.createClassWithMethod( - ctx, - ast, - false // no try-catch - ); - - // Create instance and return it (no cast to avoid ClassLoader issues) - Constructor constructor = generatedClass.getConstructor(); - Object instance = constructor.newInstance(); - // Return instance directly (will be used with MethodHandle) - return instance; + // Use unified compilation path (works for JSR 223 too!) + return compileToExecutable(ast, ctx); } } From 81879eca50e72bdc989c0753d775a4f3319e0b2e Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:04:41 +0100 Subject: [PATCH 03/10] Add print, say, and select support to bytecode interpreter Implement print/say operators with filehandle support (currently ignores filehandle and prints to STDOUT). Add select operator as placeholder for filehandle selection. Changes: - BytecodeCompiler: - Add early handling for print/say in BinaryOperatorNode (before standard operand evaluation) to avoid evaluating unused filehandle - Add select operator handling in OperatorNode (returns undef placeholder) - Fix LOAD_INT emission to use emitInt() for proper 4-byte encoding - BytecodeInterpreter: - Update PRINT and SAY opcodes to handle both RuntimeScalar and RuntimeList - Print all list elements for list arguments Testing: - ./jperl --interpreter -e 'print "Hello!\n"' - works - Multiple print statements - works - String concatenation with print - works - Compiler backend still works correctly Known limitations: - String interpolation not yet supported (requires join operator) - Filehandle selection ignored (always prints to STDOUT) Co-Authored-By: Claude Opus 4.6 --- .../interpreter/BytecodeCompiler.java | 40 +++++++++++++++++++ .../interpreter/BytecodeInterpreter.java | 27 +++++++++++-- 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index 17ae05470..b1277f28b 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -302,6 +302,30 @@ public void visit(IdentifierNode node) { @Override public void visit(BinaryOperatorNode node) { + // Handle print/say early (special handling - ignore filehandle) + if (node.operator.equals("print") || node.operator.equals("say")) { + // print/say FILEHANDLE LIST + // left = filehandle (select operator - ignored for now) + // right = list to print + + // Only compile the right operand (the content to print) + node.right.accept(this); + int contentReg = lastResultReg; + + // Emit PRINT or SAY + emit(node.operator.equals("say") ? Opcodes.SAY : Opcodes.PRINT); + emit(contentReg); + + // print/say return 1 on success + int rd = allocateRegister(); + emit(Opcodes.LOAD_INT); + emit(rd); + emitInt(1); + + lastResultReg = rd; + return; + } + // Handle assignment separately (doesn't follow standard left-right-op pattern) if (node.operator.equals("=")) { // Special case: my $x = value @@ -731,6 +755,22 @@ public void visit(OperatorNode node) { emit(undefReg); lastResultReg = undefReg; } + } else if (op.equals("select")) { + // select FILEHANDLE or select() + // For now, we ignore filehandle selection and just return a placeholder + // since we always print to STDOUT + // In full implementation, this would set/get the default output filehandle + + if (node.operand != null) { + // select FILEHANDLE - evaluate the filehandle but ignore it + node.operand.accept(this); + } + + // Return a placeholder (we could return the filehandle object here) + int rd = allocateRegister(); + emit(Opcodes.LOAD_UNDEF); + emit(rd); + lastResultReg = rd; } else { throw new UnsupportedOperationException("Unsupported operator: " + op); } diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java index 2fec61667..1c687b37d 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -561,16 +561,35 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.PRINT: { // Print to STDOUT int rs = bytecode[pc++] & 0xFF; - RuntimeScalar val = (RuntimeScalar) registers[rs]; - System.out.print(val.toString()); + Object val = registers[rs]; + if (val instanceof RuntimeList) { + // Print all elements of the list + RuntimeList list = (RuntimeList) val; + for (RuntimeBase elem : list.elements) { + System.out.print(elem.toString()); + } + } else if (val instanceof RuntimeScalar) { + // Print single scalar + System.out.print(((RuntimeScalar) val).toString()); + } break; } case Opcodes.SAY: { // Say to STDOUT (print with newline) int rs = bytecode[pc++] & 0xFF; - RuntimeScalar val = (RuntimeScalar) registers[rs]; - System.out.println(val.toString()); + Object val = registers[rs]; + if (val instanceof RuntimeList) { + // Print all elements of the list with newline at end + RuntimeList list = (RuntimeList) val; + for (RuntimeBase elem : list.elements) { + System.out.print(elem.toString()); + } + System.out.println(); + } else if (val instanceof RuntimeScalar) { + // Print single scalar with newline + System.out.println(((RuntimeScalar) val).toString()); + } break; } From befbeb2dd62b15b312fa66dae49f9b6ec983cd28 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:16:25 +0100 Subject: [PATCH 04/10] Implement proper filehandle support for select, print, and say in interpreter Update interpreter to use RuntimeIO.selectedHandle and IOOperator for filehandle operations, matching compiler behavior. Changes: - BytecodeCompiler: - Update select operator to call IOOperator.select() via SLOW_OP - Handle both select() and select(FILEHANDLE) cases - Emit SLOWOP_SELECT with proper operands - Opcodes.java: - Add SLOWOP_SELECT (ID 20) for filehandle selection - SlowOpcodeHandler.java: - Add executeSelect() method that calls IOOperator.select() - Add "select" to getSlowOpName() for disassembler - BytecodeInterpreter.java: - Update PRINT opcode to use RuntimeIO.selectedHandle via IOOperator.print() - Update SAY opcode to use RuntimeIO.selectedHandle via IOOperator.say() - Convert scalars to RuntimeList before calling IO operators Testing: - ./jperl --interpreter -e 'print "test\n"' - works (uses STDOUT) - ./jperl --interpreter -e 'my $fh = select()' - works (retrieves filehandle) - Compiler filehandle switching still works correctly - All unit tests pass Known limitations: - Filehandle switching requires '\' (reference) operator (not yet implemented) - Once '\' is implemented, full select(STDERR) will work Co-Authored-By: Claude Opus 4.6 --- .../interpreter/BytecodeCompiler.java | 36 ++++++++++---- .../interpreter/BytecodeInterpreter.java | 47 ++++++++++++------- .../org/perlonjava/interpreter/Opcodes.java | 3 ++ .../interpreter/SlowOpcodeHandler.java | 29 ++++++++++++ 4 files changed, 89 insertions(+), 26 deletions(-) diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index b1277f28b..3393b07e2 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -757,19 +757,37 @@ public void visit(OperatorNode node) { } } else if (op.equals("select")) { // select FILEHANDLE or select() - // For now, we ignore filehandle selection and just return a placeholder - // since we always print to STDOUT - // In full implementation, this would set/get the default output filehandle + // Call IOOperator.select() which handles the logic + // For now, emit a SLOW_OP that will call the operator at runtime - if (node.operand != null) { - // select FILEHANDLE - evaluate the filehandle but ignore it + int rd = allocateRegister(); + + if (node.operand != null && node.operand instanceof ListNode) { + // select FILEHANDLE or select() with arguments + // Compile the operand (ListNode containing filehandle ref) node.operand.accept(this); + int listReg = lastResultReg; + + // Emit SLOW_OP with SLOWOP_SELECT + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_SELECT); + emit(rd); + emit(listReg); + } else { + // select() with no arguments - returns current filehandle + // Create empty list + emit(Opcodes.CREATE_LIST); + int listReg = allocateRegister(); + emit(listReg); + emit(0); // count = 0 + + // Emit SLOW_OP with SLOWOP_SELECT + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_SELECT); + emit(rd); + emit(listReg); } - // Return a placeholder (we could return the filehandle object here) - int rd = allocateRegister(); - emit(Opcodes.LOAD_UNDEF); - emit(rd); lastResultReg = rd; } else { throw new UnsupportedOperationException("Unsupported operator: " + op); diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java index 1c687b37d..e917fef04 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -559,37 +559,50 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // ================================================================= case Opcodes.PRINT: { - // Print to STDOUT + // Print to selected filehandle (default STDOUT) int rs = bytecode[pc++] & 0xFF; Object val = registers[rs]; + + RuntimeList list; if (val instanceof RuntimeList) { - // Print all elements of the list - RuntimeList list = (RuntimeList) val; - for (RuntimeBase elem : list.elements) { - System.out.print(elem.toString()); - } + list = (RuntimeList) val; } else if (val instanceof RuntimeScalar) { - // Print single scalar - System.out.print(((RuntimeScalar) val).toString()); + // Convert scalar to single-element list + list = new RuntimeList(); + list.add((RuntimeScalar) val); + } else { + list = new RuntimeList(); } + + // Get selected filehandle + RuntimeScalar fh = new RuntimeScalar(RuntimeIO.selectedHandle); + + // Call IOOperator.print() + org.perlonjava.operators.IOOperator.print(list, fh); break; } case Opcodes.SAY: { - // Say to STDOUT (print with newline) + // Say to selected filehandle (default STDOUT) int rs = bytecode[pc++] & 0xFF; Object val = registers[rs]; + + RuntimeList list; if (val instanceof RuntimeList) { - // Print all elements of the list with newline at end - RuntimeList list = (RuntimeList) val; - for (RuntimeBase elem : list.elements) { - System.out.print(elem.toString()); - } - System.out.println(); + list = (RuntimeList) val; } else if (val instanceof RuntimeScalar) { - // Print single scalar with newline - System.out.println(((RuntimeScalar) val).toString()); + // Convert scalar to single-element list + list = new RuntimeList(); + list.add((RuntimeScalar) val); + } else { + list = new RuntimeList(); } + + // Get selected filehandle + RuntimeScalar fh = new RuntimeScalar(RuntimeIO.selectedHandle); + + // Call IOOperator.say() + org.perlonjava.operators.IOOperator.say(list, fh); break; } diff --git a/src/main/java/org/perlonjava/interpreter/Opcodes.java b/src/main/java/org/perlonjava/interpreter/Opcodes.java index 4a9980db0..21fa7f1ca 100644 --- a/src/main/java/org/perlonjava/interpreter/Opcodes.java +++ b/src/main/java/org/perlonjava/interpreter/Opcodes.java @@ -456,6 +456,9 @@ public class Opcodes { /** Slow op ID: rd = eval(rs_string) - dynamic code evaluation */ public static final int SLOWOP_EVAL_STRING = 19; + /** Slow op ID: rd = select(rs_list) - set/get default output filehandle */ + public static final int SLOWOP_SELECT = 20; + // ================================================================= // OPCODES 88-255: RESERVED FOR FUTURE FAST OPERATIONS // ================================================================= diff --git a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java index 370274571..fd4541242 100644 --- a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java @@ -139,6 +139,9 @@ public static int execute( case Opcodes.SLOWOP_EVAL_STRING: return executeEvalString(bytecode, pc, registers, code); + case Opcodes.SLOWOP_SELECT: + return executeSelect(bytecode, pc, registers); + default: throw new RuntimeException( "Unknown slow operation ID: " + slowOpId + @@ -173,6 +176,7 @@ public static String getSlowOpName(int slowOpId) { case Opcodes.SLOWOP_SHMWRITE -> "shmwrite"; case Opcodes.SLOWOP_SYSCALL -> "syscall"; case Opcodes.SLOWOP_EVAL_STRING -> "eval"; + case Opcodes.SLOWOP_SELECT -> "select"; default -> "slowop_" + slowOpId; }; } @@ -504,6 +508,31 @@ private static int executeEvalString( return pc; } + /** + * SLOW_SELECT: rd = select(listReg) + * Format: [SLOW_SELECT] [rd] [rs_list] + * Effect: Sets or gets the default output filehandle + */ + private static int executeSelect( + byte[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++] & 0xFF; + int listReg = bytecode[pc++] & 0xFF; + + RuntimeList list = (RuntimeList) registers[listReg]; + + // Call IOOperator.select() which handles the logic + RuntimeScalar result = org.perlonjava.operators.IOOperator.select( + list, + org.perlonjava.runtime.RuntimeContextType.SCALAR + ); + + registers[rd] = result; + return pc; + } + private SlowOpcodeHandler() { // Utility class - no instantiation } From 1b99eba36a9e4ece113ae308277d94bf3041a296 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:26:43 +0100 Subject: [PATCH 05/10] Implement reference operator (\) and glob loading (*) for filehandle support Enable print/say to work with explicit filehandles (STDERR, STDOUT) by implementing the reference operator and glob loading. Changes: - BytecodeCompiler: - Implement `*` operator to load globs via SLOWOP_LOAD_GLOB - Implement `\` operator using CREATE_REF opcode - Update print/say to compile both filehandle and content operands - Add package prefix (main::) to bareword globs - BytecodeInterpreter: - Implement CREATE_REF opcode (calls .createReference()) - Implement stub DEREF and GET_TYPE opcodes - Update PRINT/SAY opcodes to accept two registers (content, filehandle) - Opcodes.java: - Update PRINT/SAY opcode comments to reflect dual-register format - Add SLOWOP_LOAD_GLOB (ID 21) for glob loading - SlowOpcodeHandler.java: - Implement executeLoadGlob() calling GlobalVariable.getGlobalIO() - Add SLOWOP_LOAD_GLOB to getSlowOpName() for disassembler Testing: - ./jperl --interpreter -e 'print STDERR "Error\n"' works correctly - ./jperl --interpreter -e 'print STDOUT "Normal\n"' works correctly - ./jperl --interpreter -e 'print "test\n"' still works (uses select()) - Output matches compiler behavior Implementation notes: - glob loading uses SLOW_OP since it's relatively uncommon - Reference operator creates proper RuntimeScalar references - DEREF/GET_TYPE are stubs (full dereferencing is context-dependent) Co-Authored-By: Claude Opus 4.6 --- .../interpreter/BytecodeCompiler.java | 57 ++++++++++++++++-- .../interpreter/BytecodeInterpreter.java | 59 +++++++++++++++---- .../org/perlonjava/interpreter/Opcodes.java | 7 ++- .../interpreter/SlowOpcodeHandler.java | 27 +++++++++ 4 files changed, 132 insertions(+), 18 deletions(-) diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index 3393b07e2..b33b5bbd7 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -302,19 +302,24 @@ public void visit(IdentifierNode node) { @Override public void visit(BinaryOperatorNode node) { - // Handle print/say early (special handling - ignore filehandle) + // Handle print/say early (special handling for filehandle) if (node.operator.equals("print") || node.operator.equals("say")) { // print/say FILEHANDLE LIST - // left = filehandle (select operator - ignored for now) + // left = filehandle reference (\*STDERR) // right = list to print - // Only compile the right operand (the content to print) + // Compile the filehandle (left operand) + node.left.accept(this); + int filehandleReg = lastResultReg; + + // Compile the content (right operand) node.right.accept(this); int contentReg = lastResultReg; - // Emit PRINT or SAY + // Emit PRINT or SAY with both registers emit(node.operator.equals("say") ? Opcodes.SAY : Opcodes.PRINT); emit(contentReg); + emit(filehandleReg); // print/say return 1 on success int rd = allocateRegister(); @@ -617,6 +622,50 @@ public void visit(OperatorNode node) { } else if (op.equals("%")) { // Hash variable dereference: %x throw new RuntimeException("Hash variables not yet supported"); + } else if (op.equals("*")) { + // Glob variable dereference: *x + if (node.operand instanceof IdentifierNode) { + IdentifierNode idNode = (IdentifierNode) node.operand; + String varName = idNode.name; + + // Add package prefix if not present + if (!varName.contains("::")) { + varName = "main::" + varName; + } + + // Allocate register for glob + int rd = allocateRegister(); + int nameIdx = addToStringPool(varName); + + // Emit SLOW_OP with SLOWOP_LOAD_GLOB + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_LOAD_GLOB); + emit(rd); + emit(nameIdx); + + lastResultReg = rd; + } else { + throw new RuntimeException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); + } + } else if (op.equals("\\")) { + // Reference operator: \$x, \@x, \%x, \*x, etc. + if (node.operand != null) { + // Evaluate the operand + node.operand.accept(this); + int valueReg = lastResultReg; + + // Allocate register for reference + int rd = allocateRegister(); + + // Emit CREATE_REF + emit(Opcodes.CREATE_REF); + emit(rd); + emit(valueReg); + + lastResultReg = rd; + } else { + throw new RuntimeException("Reference operator requires operand"); + } } else if (op.equals("say") || op.equals("print")) { // say/print $x if (node.operand != null) { diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java index e917fef04..3fa7fa1b8 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -559,9 +559,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // ================================================================= case Opcodes.PRINT: { - // Print to selected filehandle (default STDOUT) - int rs = bytecode[pc++] & 0xFF; - Object val = registers[rs]; + // Print to filehandle + // Format: [PRINT] [rs_content] [rs_filehandle] + int contentReg = bytecode[pc++] & 0xFF; + int filehandleReg = bytecode[pc++] & 0xFF; + + Object val = registers[contentReg]; + RuntimeScalar fh = (RuntimeScalar) registers[filehandleReg]; RuntimeList list; if (val instanceof RuntimeList) { @@ -574,18 +578,19 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c list = new RuntimeList(); } - // Get selected filehandle - RuntimeScalar fh = new RuntimeScalar(RuntimeIO.selectedHandle); - // Call IOOperator.print() org.perlonjava.operators.IOOperator.print(list, fh); break; } case Opcodes.SAY: { - // Say to selected filehandle (default STDOUT) - int rs = bytecode[pc++] & 0xFF; - Object val = registers[rs]; + // Say to filehandle + // Format: [SAY] [rs_content] [rs_filehandle] + int contentReg = bytecode[pc++] & 0xFF; + int filehandleReg = bytecode[pc++] & 0xFF; + + Object val = registers[contentReg]; + RuntimeScalar fh = (RuntimeScalar) registers[filehandleReg]; RuntimeList list; if (val instanceof RuntimeList) { @@ -598,9 +603,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c list = new RuntimeList(); } - // Get selected filehandle - RuntimeScalar fh = new RuntimeScalar(RuntimeIO.selectedHandle); - // Call IOOperator.say() org.perlonjava.operators.IOOperator.say(list, fh); break; @@ -711,6 +713,39 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + // ================================================================= + // REFERENCE OPERATIONS + // ================================================================= + + case Opcodes.CREATE_REF: { + // Create reference: rd = rs.createReference() + int rd = bytecode[pc++] & 0xFF; + int rs = bytecode[pc++] & 0xFF; + RuntimeBase value = registers[rs]; + registers[rd] = value.createReference(); + break; + } + + case Opcodes.DEREF: { + // Dereference: rd = rs (dereferencing depends on context) + // For now, just copy the reference - proper dereferencing + // is context-dependent and handled by specific operators + int rd = bytecode[pc++] & 0xFF; + int rs = bytecode[pc++] & 0xFF; + registers[rd] = registers[rs]; + break; + } + + case Opcodes.GET_TYPE: { + // Get type: rd = new RuntimeScalar(rs.type) + int rd = bytecode[pc++] & 0xFF; + int rs = bytecode[pc++] & 0xFF; + RuntimeScalar value = (RuntimeScalar) registers[rs]; + // RuntimeScalar.type is an int constant from RuntimeScalarType + registers[rd] = new RuntimeScalar(value.type); + break; + } + // ================================================================= // EVAL BLOCK SUPPORT // ================================================================= diff --git a/src/main/java/org/perlonjava/interpreter/Opcodes.java b/src/main/java/org/perlonjava/interpreter/Opcodes.java index 21fa7f1ca..4f26e2fe6 100644 --- a/src/main/java/org/perlonjava/interpreter/Opcodes.java +++ b/src/main/java/org/perlonjava/interpreter/Opcodes.java @@ -287,10 +287,10 @@ public class Opcodes { // MISCELLANEOUS (71-74) // ================================================================= - /** Print to STDOUT: print(rs) */ + /** Print to filehandle: print(rs_content, rs_filehandle) */ public static final byte PRINT = 71; - /** Say to STDOUT: say(rs) */ + /** Say to filehandle: say(rs_content, rs_filehandle) */ public static final byte SAY = 72; /** Die with message: die(rs) */ @@ -459,6 +459,9 @@ public class Opcodes { /** Slow op ID: rd = select(rs_list) - set/get default output filehandle */ public static final int SLOWOP_SELECT = 20; + /** Slow op ID: rd = getGlobalIO(name) - load glob/filehandle from global variables */ + public static final int SLOWOP_LOAD_GLOB = 21; + // ================================================================= // OPCODES 88-255: RESERVED FOR FUTURE FAST OPERATIONS // ================================================================= diff --git a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java index fd4541242..1ded8f5f9 100644 --- a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java @@ -142,6 +142,9 @@ public static int execute( case Opcodes.SLOWOP_SELECT: return executeSelect(bytecode, pc, registers); + case Opcodes.SLOWOP_LOAD_GLOB: + return executeLoadGlob(bytecode, pc, registers, code); + default: throw new RuntimeException( "Unknown slow operation ID: " + slowOpId + @@ -177,6 +180,7 @@ public static String getSlowOpName(int slowOpId) { case Opcodes.SLOWOP_SYSCALL -> "syscall"; case Opcodes.SLOWOP_EVAL_STRING -> "eval"; case Opcodes.SLOWOP_SELECT -> "select"; + case Opcodes.SLOWOP_LOAD_GLOB -> "load_glob"; default -> "slowop_" + slowOpId; }; } @@ -533,6 +537,29 @@ private static int executeSelect( return pc; } + /** + * SLOW_LOAD_GLOB: rd = getGlobalIO(name) + * Format: [SLOW_LOAD_GLOB] [rd] [name_idx] + * Effect: Loads a glob/filehandle from global variables + */ + private static int executeLoadGlob( + byte[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + + int rd = bytecode[pc++] & 0xFF; + int nameIdx = bytecode[pc++] & 0xFF; + + String globName = code.stringPool[nameIdx]; + + // Call GlobalVariable.getGlobalIO() to get the RuntimeGlob + RuntimeGlob glob = org.perlonjava.runtime.GlobalVariable.getGlobalIO(globName); + + registers[rd] = glob; + return pc; + } + private SlowOpcodeHandler() { // Utility class - no instantiation } From 355f2dc2f29505d00421412d77510cd2b28ef21a Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:31:02 +0100 Subject: [PATCH 06/10] Add --interpreter --disassemble support for bytecode inspection Enable disassembly of interpreter bytecode to aid development, debugging, and performance analysis. Changes: - PerlLanguageProvider: - Check disassembleEnabled flag for interpreter path - Call InterpretedCode.disassemble() and print output - Matches existing --disassemble behavior for compiler - InterpretedCode disassembler improvements: - Add PRINT/SAY opcode handlers (dual-register format) - Add CREATE_REF, DEREF, GET_TYPE opcode handlers - Improve SLOW_OP decoding with operand display: * SLOWOP_EVAL_STRING: shows rd = eval(rs) * SLOWOP_SELECT: shows rd = select(rs) * SLOWOP_LOAD_GLOB: shows rd = *globname (with string pool lookup) Usage: ./jperl --interpreter --disassemble -e 'code' Output example: === Bytecode Disassembly === Source: -e:1 Registers: 7 Bytecode length: 21 bytes 0: LOAD_INT r4 = 5 6: LOAD_INT r5 = 3 12: ADD_SCALAR r6 = r4 + r5 16: MOVE r3 = r6 19: RETURN r3 Benefits: - Debug interpreter development (see generated bytecode) - Understand performance (identify superinstructions) - Compare compiler vs interpreter code generation - Educational tool for learning bytecode structure Co-Authored-By: Claude Opus 4.6 --- .../interpreter/InterpretedCode.java | 63 +++++++++++++++++-- .../scriptengine/PerlLanguageProvider.java | 11 +++- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java index 2e47da148..ca8e4313d 100644 --- a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java +++ b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java @@ -259,6 +259,33 @@ public String disassemble() { rd = bytecode[pc++] & 0xFF; sb.append("POST_AUTODECREMENT r").append(rd).append("--\n"); break; + case Opcodes.PRINT: { + int contentReg = bytecode[pc++] & 0xFF; + int filehandleReg = bytecode[pc++] & 0xFF; + sb.append("PRINT r").append(contentReg).append(", fh=r").append(filehandleReg).append("\n"); + break; + } + case Opcodes.SAY: { + int contentReg = bytecode[pc++] & 0xFF; + int filehandleReg = bytecode[pc++] & 0xFF; + sb.append("SAY r").append(contentReg).append(", fh=r").append(filehandleReg).append("\n"); + break; + } + case Opcodes.CREATE_REF: + rd = bytecode[pc++] & 0xFF; + rs = bytecode[pc++] & 0xFF; + sb.append("CREATE_REF r").append(rd).append(" = \\r").append(rs).append("\n"); + break; + case Opcodes.DEREF: + rd = bytecode[pc++] & 0xFF; + rs = bytecode[pc++] & 0xFF; + sb.append("DEREF r").append(rd).append(" = ${r").append(rs).append("}\n"); + break; + case Opcodes.GET_TYPE: + rd = bytecode[pc++] & 0xFF; + rs = bytecode[pc++] & 0xFF; + sb.append("GET_TYPE r").append(rd).append(" = type(r").append(rs).append(")\n"); + break; case Opcodes.DIE: rs = bytecode[pc++] & 0xFF; sb.append("DIE r").append(rs).append("\n"); @@ -303,13 +330,39 @@ public String disassemble() { sb.append("CALL_SUB r").append(rd).append(" = r").append(coderefReg) .append("->(r").append(argsReg).append(", ctx=").append(ctx).append(")\n"); break; - case Opcodes.SLOW_OP: + case Opcodes.SLOW_OP: { int slowOpId = bytecode[pc++] & 0xFF; - sb.append("SLOW_OP ").append(SlowOpcodeHandler.getSlowOpName(slowOpId)) - .append(" (id=").append(slowOpId).append(")\n"); - // Note: We can't easily decode operands without duplicating - // SlowOpcodeHandler logic, so just show opcode name and ID + String opName = SlowOpcodeHandler.getSlowOpName(slowOpId); + sb.append("SLOW_OP ").append(opName).append(" (id=").append(slowOpId).append(")"); + + // Decode operands for known SLOW_OPs + switch (slowOpId) { + case Opcodes.SLOWOP_EVAL_STRING: + // Format: [rd] [rs_string] + rd = bytecode[pc++] & 0xFF; + rs = bytecode[pc++] & 0xFF; + sb.append(" r").append(rd).append(" = eval(r").append(rs).append(")"); + break; + case Opcodes.SLOWOP_SELECT: + // Format: [rd] [rs_list] + rd = bytecode[pc++] & 0xFF; + rs = bytecode[pc++] & 0xFF; + sb.append(" r").append(rd).append(" = select(r").append(rs).append(")"); + break; + case Opcodes.SLOWOP_LOAD_GLOB: + // Format: [rd] [name_idx] + rd = bytecode[pc++] & 0xFF; + int globNameIdx = bytecode[pc++] & 0xFF; + String globName = stringPool[globNameIdx]; + sb.append(" r").append(rd).append(" = *").append(globName); + break; + default: + sb.append(" (operands not decoded)"); + break; + } + sb.append("\n"); break; + } default: sb.append("UNKNOWN(").append(opcode & 0xFF).append(")\n"); break; diff --git a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java index 91f3cf031..c80afbea2 100644 --- a/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/scriptengine/PerlLanguageProvider.java @@ -321,7 +321,16 @@ private static Object compileToExecutable(Node ast, EmitterContext ctx) throws E ctx.compilerOptions.fileName, 1 // tokenIndex for error reporting ); - return compiler.compile(ast); + InterpretedCode interpretedCode = compiler.compile(ast); + + // If --disassemble is enabled, print the bytecode + if (ctx.compilerOptions.disassembleEnabled) { + System.out.println("=== Interpreter Bytecode ==="); + System.out.println(interpretedCode.disassemble()); + System.out.println("=== End Bytecode ==="); + } + + return interpretedCode; } else { // Compiler path - returns generated class instance ctx.logDebug("Compiling to JVM bytecode"); From c28360162d9bba46910b5676aac71a331002bbf0 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:36:29 +0100 Subject: [PATCH 07/10] Add JOIN opcode for string interpolation support in interpreter Implement join operator (opcode 88) to enable string interpolation with variable substitution in print statements. Changes: - Opcodes.java: - Add JOIN opcode (88) for string joining operations - Used heavily in string interpolation ("Value: $x\n") - BytecodeCompiler: - Add join case to BinaryOperatorNode switch - Emit JOIN with three registers (result, separator, list) - BytecodeInterpreter: - Implement JOIN opcode handler - Call StringOperators.joinForInterpolation() for proper semantics - Handles empty separator (for interpolation) - InterpretedCode disassembler: - Add JOIN opcode disassembly with operand display Testing: - ./jperl --interpreter -e 'my $x = 42; print "Answer: $x\n"' Output: Answer: 42 - ./jperl --interpreter -e 'my $a = 1; my $b = 2; print "a=$a b=$b\n"' Output: a=1 b=2 - ./jperl --interpreter --disassemble -e 'print "x=$x\n"' Shows: JOIN r# = join(r#, r#) Performance note: - JOIN is a fast opcode (88, not SLOW_OP) because string interpolation is extremely common in Perl code - Preserves tableswitch optimization (dense opcode sequence) Co-Authored-By: Claude Opus 4.6 --- .../interpreter/BytecodeCompiler.java | 10 +++++++++ .../interpreter/BytecodeInterpreter.java | 22 +++++++++++++++++++ .../interpreter/InterpretedCode.java | 7 ++++++ .../org/perlonjava/interpreter/Opcodes.java | 7 ++++++ 4 files changed, 46 insertions(+) diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index b33b5bbd7..ce21175c6 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -551,6 +551,16 @@ public void visit(BinaryOperatorNode node) { // Note: CALL_SUB may return RuntimeControlFlowList // The interpreter will handle control flow propagation } + case "join" -> { + // String join: rd = join(separator, list) + // left (rs1) = separator (empty string for interpolation) + // right (rs2) = list of elements + + emit(Opcodes.JOIN); + emit(rd); + emit(rs1); + emit(rs2); + } default -> throw new RuntimeException("Unsupported operator: " + node.operator); } diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java index 3fa7fa1b8..3174777e4 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -831,6 +831,28 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + // ================================================================= + // STRING OPERATIONS + // ================================================================= + + case Opcodes.JOIN: { + // String join: rd = join(separator, list) + int rd = bytecode[pc++] & 0xFF; + int separatorReg = bytecode[pc++] & 0xFF; + int listReg = bytecode[pc++] & 0xFF; + + RuntimeScalar separator = (RuntimeScalar) registers[separatorReg]; + RuntimeBase list = registers[listReg]; + + // Call StringOperators.joinForInterpolation (doesn't warn on undef) + registers[rd] = org.perlonjava.operators.StringOperators.joinForInterpolation(separator, list); + break; + } + + // ================================================================= + // SLOW OPERATIONS + // ================================================================= + case Opcodes.SLOW_OP: { // Dispatch to slow operation handler // Format: [SLOW_OP] [slow_op_id] [operands...] diff --git a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java index ca8e4313d..f1bfa7e92 100644 --- a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java +++ b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java @@ -330,6 +330,13 @@ public String disassemble() { sb.append("CALL_SUB r").append(rd).append(" = r").append(coderefReg) .append("->(r").append(argsReg).append(", ctx=").append(ctx).append(")\n"); break; + case Opcodes.JOIN: + rd = bytecode[pc++] & 0xFF; + int separatorReg = bytecode[pc++] & 0xFF; + int listReg = bytecode[pc++] & 0xFF; + sb.append("JOIN r").append(rd).append(" = join(r").append(separatorReg) + .append(", r").append(listReg).append(")\n"); + break; case Opcodes.SLOW_OP: { int slowOpId = bytecode[pc++] & 0xFF; String opName = SlowOpcodeHandler.getSlowOpName(slowOpId); diff --git a/src/main/java/org/perlonjava/interpreter/Opcodes.java b/src/main/java/org/perlonjava/interpreter/Opcodes.java index 4f26e2fe6..41ee09558 100644 --- a/src/main/java/org/perlonjava/interpreter/Opcodes.java +++ b/src/main/java/org/perlonjava/interpreter/Opcodes.java @@ -369,6 +369,13 @@ public class Opcodes { */ public static final byte CREATE_LIST = 86; + // ================================================================= + // STRING OPERATIONS (88) + // ================================================================= + + /** Join list elements with separator: rd = join(rs_separator, rs_list) */ + public static final byte JOIN = 88; + // ================================================================= // SLOW OPERATIONS (87) - Single opcode for rarely-used operations // ================================================================= From e55db3aabea7d6511e5917214a58734250fda8e6 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:37:51 +0100 Subject: [PATCH 08/10] Update roadmap with --interpreter feature documentation --- docs/about/roadmap.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/about/roadmap.md b/docs/about/roadmap.md index ad75d0184..a27ee6800 100644 --- a/docs/about/roadmap.md +++ b/docs/about/roadmap.md @@ -69,6 +69,8 @@ The following areas are currently under active development to enhance the functi - Optimization: faster type resolution in Perl scalars. - Optimization: `make` now runs tests in parallel. - Optimization: A workaround is implemented to Java 64k bytes segment limit. + - New command line option: `--interpreter` to run PerlOnJava as an interpreter instead of JVM compiler. + - `./jperl --interpreter --disassemble -e 'print "Hello, World!\n"'` - Planned release date: 2026-02-10. - Work in Progress From 0f20246c7cb6682101859eedfd30508e0c269089 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:41:16 +0100 Subject: [PATCH 09/10] Add missing opcodes to disassembler (LOAD_CONST, LOAD_STRING, LOAD_UNDEF) --- .../interpreter/InterpretedCode.java | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java index f1bfa7e92..05c8b6d6b 100644 --- a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java +++ b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java @@ -189,12 +189,41 @@ public String disassemble() { int src = bytecode[pc++] & 0xFF; sb.append("MOVE r").append(dest).append(" = r").append(src).append("\n"); break; - case Opcodes.LOAD_INT: + case Opcodes.LOAD_CONST: int rd = bytecode[pc++] & 0xFF; + int constIdx = bytecode[pc++] & 0xFF; + sb.append("LOAD_CONST r").append(rd).append(" = constants[").append(constIdx).append("]"); + if (constants != null && constIdx < constants.length) { + sb.append(" (").append(constants[constIdx]).append(")"); + } + sb.append("\n"); + break; + case Opcodes.LOAD_INT: + rd = bytecode[pc++] & 0xFF; int value = readInt(bytecode, pc); pc += 4; sb.append("LOAD_INT r").append(rd).append(" = ").append(value).append("\n"); break; + case Opcodes.LOAD_STRING: + rd = bytecode[pc++] & 0xFF; + int strIdx = bytecode[pc++] & 0xFF; + sb.append("LOAD_STRING r").append(rd).append(" = \""); + if (stringPool != null && strIdx < stringPool.length) { + String str = stringPool[strIdx]; + // Escape special characters for readability + str = str.replace("\\", "\\\\") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + .replace("\"", "\\\""); + sb.append(str); + } + sb.append("\"\n"); + break; + case Opcodes.LOAD_UNDEF: + rd = bytecode[pc++] & 0xFF; + sb.append("LOAD_UNDEF r").append(rd).append("\n"); + break; case Opcodes.LOAD_GLOBAL_SCALAR: rd = bytecode[pc++] & 0xFF; int nameIdx = bytecode[pc++] & 0xFF; From e5223e6f48ce192e5bdd8d84af8f13f2010cc69d Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Thu, 12 Feb 2026 13:45:18 +0100 Subject: [PATCH 10/10] Promote SELECT to fast opcode (89) - used in every print statement SELECT was previously SLOWOP_SELECT (20) but is actually hot path code called once for every print/say statement to get the default filehandle. Performance impact: - Eliminates SLOW_OP dispatch overhead - Reduces bytecode size by 1 byte per print statement - Improves CPU i-cache utilization Dense opcodes now: 0-89 (no gaps, maintains tableswitch optimization) --- .../perlonjava/interpreter/BytecodeCompiler.java | 15 +++++++-------- .../interpreter/BytecodeInterpreter.java | 11 +++++++++++ .../perlonjava/interpreter/InterpretedCode.java | 5 +++++ .../java/org/perlonjava/interpreter/Opcodes.java | 7 +++++++ 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index ce21175c6..88b07e267 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -816,8 +816,9 @@ public void visit(OperatorNode node) { } } else if (op.equals("select")) { // select FILEHANDLE or select() - // Call IOOperator.select() which handles the logic - // For now, emit a SLOW_OP that will call the operator at runtime + // SELECT is a fast opcode (used in every print statement) + // Format: [SELECT] [rd] [rs_list] + // Effect: rd = IOOperator.select(registers[rs_list], SCALAR) int rd = allocateRegister(); @@ -827,9 +828,8 @@ public void visit(OperatorNode node) { node.operand.accept(this); int listReg = lastResultReg; - // Emit SLOW_OP with SLOWOP_SELECT - emitWithToken(Opcodes.SLOW_OP, node.getIndex()); - emit(Opcodes.SLOWOP_SELECT); + // Emit SELECT opcode + emitWithToken(Opcodes.SELECT, node.getIndex()); emit(rd); emit(listReg); } else { @@ -840,9 +840,8 @@ public void visit(OperatorNode node) { emit(listReg); emit(0); // count = 0 - // Emit SLOW_OP with SLOWOP_SELECT - emitWithToken(Opcodes.SLOW_OP, node.getIndex()); - emit(Opcodes.SLOWOP_SELECT); + // Emit SELECT opcode + emitWithToken(Opcodes.SELECT, node.getIndex()); emit(rd); emit(listReg); } diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java index 3174777e4..8e4a24f0c 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -849,6 +849,17 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.SELECT: { + // Select default output filehandle: rd = IOOperator.select(list, SCALAR) + int rd = bytecode[pc++] & 0xFF; + int listReg = bytecode[pc++] & 0xFF; + + RuntimeList list = (RuntimeList) registers[listReg]; + RuntimeScalar result = org.perlonjava.operators.IOOperator.select(list, RuntimeContextType.SCALAR); + registers[rd] = result; + break; + } + // ================================================================= // SLOW OPERATIONS // ================================================================= diff --git a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java index 05c8b6d6b..8d1f3b36f 100644 --- a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java +++ b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java @@ -366,6 +366,11 @@ public String disassemble() { sb.append("JOIN r").append(rd).append(" = join(r").append(separatorReg) .append(", r").append(listReg).append(")\n"); break; + case Opcodes.SELECT: + rd = bytecode[pc++] & 0xFF; + listReg = bytecode[pc++] & 0xFF; + sb.append("SELECT r").append(rd).append(" = select(r").append(listReg).append(")\n"); + break; case Opcodes.SLOW_OP: { int slowOpId = bytecode[pc++] & 0xFF; String opName = SlowOpcodeHandler.getSlowOpName(slowOpId); diff --git a/src/main/java/org/perlonjava/interpreter/Opcodes.java b/src/main/java/org/perlonjava/interpreter/Opcodes.java index 41ee09558..e5176f7a5 100644 --- a/src/main/java/org/perlonjava/interpreter/Opcodes.java +++ b/src/main/java/org/perlonjava/interpreter/Opcodes.java @@ -376,6 +376,13 @@ public class Opcodes { /** Join list elements with separator: rd = join(rs_separator, rs_list) */ public static final byte JOIN = 88; + // ================================================================= + // I/O OPERATIONS (89) + // ================================================================= + + /** Select default output filehandle: rd = IOOperator.select(rs_list, SCALAR) */ + public static final byte SELECT = 89; + // ================================================================= // SLOW OPERATIONS (87) - Single opcode for rarely-used operations // =================================================================