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 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/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index 17ae05470..88b07e267 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -302,6 +302,35 @@ public void visit(IdentifierNode node) { @Override public void visit(BinaryOperatorNode node) { + // Handle print/say early (special handling for filehandle) + if (node.operator.equals("print") || node.operator.equals("say")) { + // print/say FILEHANDLE LIST + // left = filehandle reference (\*STDERR) + // right = list 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 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(); + 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 @@ -522,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); } @@ -593,6 +632,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) { @@ -731,6 +814,39 @@ public void visit(OperatorNode node) { emit(undefReg); lastResultReg = undefReg; } + } else if (op.equals("select")) { + // select FILEHANDLE or select() + // 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(); + + 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 SELECT opcode + emitWithToken(Opcodes.SELECT, node.getIndex()); + 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 SELECT opcode + emitWithToken(Opcodes.SELECT, node.getIndex()); + emit(rd); + emit(listReg); + } + + 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..8e4a24f0c 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -559,18 +559,52 @@ 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()); + // 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) { + list = (RuntimeList) val; + } else if (val instanceof RuntimeScalar) { + // Convert scalar to single-element list + list = new RuntimeList(); + list.add((RuntimeScalar) val); + } else { + list = new RuntimeList(); + } + + // Call IOOperator.print() + org.perlonjava.operators.IOOperator.print(list, fh); 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()); + // 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) { + list = (RuntimeList) val; + } else if (val instanceof RuntimeScalar) { + // Convert scalar to single-element list + list = new RuntimeList(); + list.add((RuntimeScalar) val); + } else { + list = new RuntimeList(); + } + + // Call IOOperator.say() + org.perlonjava.operators.IOOperator.say(list, fh); break; } @@ -679,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 // ================================================================= @@ -764,6 +831,39 @@ 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; + } + + 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 + // ================================================================= + 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/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/interpreter/InterpretedCode.java b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java index 2e47da148..8d1f3b36f 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; @@ -259,6 +288,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 +359,51 @@ 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.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.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; - 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/interpreter/Opcodes.java b/src/main/java/org/perlonjava/interpreter/Opcodes.java index 4a9980db0..e5176f7a5 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) */ @@ -369,6 +369,20 @@ 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; + + // ================================================================= + // 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 // ================================================================= @@ -456,6 +470,12 @@ 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; + + /** 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 370274571..1ded8f5f9 100644 --- a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java @@ -139,6 +139,12 @@ public static int execute( case Opcodes.SLOWOP_EVAL_STRING: return executeEvalString(bytecode, pc, registers, code); + 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 + @@ -173,6 +179,8 @@ 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"; + case Opcodes.SLOWOP_LOAD_GLOB -> "load_glob"; default -> "slowop_" + slowOpId; }; } @@ -504,6 +512,54 @@ 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; + } + + /** + * 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 } 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..c80afbea2 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) { @@ -302,5 +302,102 @@ private static RuntimeList executeGeneratedClass(Class generatedClass, Emitte RuntimeIO.flushAllHandles(); 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 + ); + 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"); + 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. + * + * @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 or bytecode based on flag + ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); + ctx.symbolTable = ctx.symbolTable.snapShot(); + SpecialBlockParser.setCurrentScope(ctx.symbolTable); + + // Use unified compilation path (works for JSR 223 too!) + return compileToExecutable(ast, ctx); + } } 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