diff --git a/dev/tools/check_opcodes.pl b/dev/tools/check_opcodes.pl new file mode 100644 index 000000000..b524314a4 --- /dev/null +++ b/dev/tools/check_opcodes.pl @@ -0,0 +1,81 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +# Check Opcodes.java for duplicate or out-of-order opcode numbers. +# Optionally renumber the absolute block (284+) to be contiguous after +# the last hand-assigned opcode. +# +# Usage (run from repo root): +# perl dev/tools/check_opcodes.pl src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +# perl dev/tools/check_opcodes.pl src/main/java/org/perlonjava/backend/bytecode/Opcodes.java --renumber + +my ($file, $flag) = @ARGV; +die "Usage: $0 Opcodes.java [--renumber]\n" unless $file && -f $file; +my $renumber = ($flag // '') eq '--renumber'; + +my $content = do { open my $fh, '<', $file or die "Cannot open $file: $!"; local $/; <$fh> }; + +# Collect all public static final short NAME = NUMBER; (skip LASTOP+N expressions) +my (%name2num, %num2names); +while ($content =~ /\bpublic\s+static\s+final\s+short\s+(\w+)\s*=\s*(\d+)\s*;/g) { + my ($name, $num) = ($1, $2); + $name2num{$name} = $num; + push @{ $num2names{$num} }, $name; +} + +# --- Duplicates --- +my @dups = sort { $a <=> $b } grep { @{ $num2names{$_} } > 1 } keys %num2names; +if (@dups) { + print "DUPLICATES:\n"; + for my $n (@dups) { + print " $n => ", join(", ", @{ $num2names{$n} }), "\n"; + } +} else { + print "No duplicates.\n"; +} + +# --- Range and gaps --- +my @sorted = sort { $a <=> $b } keys %num2names; +printf "Range: %d..%d (%d distinct values)\n", $sorted[0], $sorted[-1], scalar @sorted; + +my @gaps; +for my $i (1 .. $#sorted) { + my ($prev, $cur) = @sorted[ $i - 1, $i ]; + push @gaps, sprintf(" %d..%d (gap of %d)", $prev, $cur, $cur - $prev - 1) if $cur - $prev > 1; +} +if (@gaps) { + print "GAPS:\n"; + print "$_\n" for @gaps; +} else { + print "No gaps.\n"; +} + +# --- Renumber --- +if ($renumber) { + # Find the last opcode below 284 (the hand-assigned range) + my @low = sort { $a <=> $b } grep { $_ < 284 } keys %num2names; + my $next = ($low[-1] // 283) + 1; + print "\nRenumbering 284+ starting at $next:\n"; + + my @high = sort { $a <=> $b } grep { $_ >= 284 } keys %num2names; + my %remap; + for my $old (@high) { + $remap{$old} = $next++; + } + + for my $old (sort { $a <=> $b } keys %remap) { + my $new = $remap{$old}; + next if $old == $new; + my @names = @{ $num2names{$old} }; + printf " %d -> %d (%s)\n", $old, $new, join(", ", @names); + for my $name (@names) { + # Replace NAME = OLD; with NAME = NEW; + $content =~ s/\b(\Q$name\E\s*=\s*)\d+(\s*;)/$1$new$2/; + } + } + + open my $fh, '>', $file or die "Cannot write $file: $!"; + print $fh $content; + print "Written.\n"; +} diff --git a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java index 98208d8d2..d7e85a9b1 100644 --- a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java +++ b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java @@ -918,6 +918,7 @@ private static int processLongSwitches(String[] args, CompilerOptions parsedArgs // Enable disassemble mode validateExclusiveOptions(parsedArgs, "disassemble"); parsedArgs.disassembleEnabled = true; + RuntimeCode.setDisassemble(true); break; case "--interpreter": // Use bytecode interpreter instead of JVM compiler diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 27e1de016..5b4a35fb6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -254,6 +254,15 @@ String getCurrentPackage() { return symbolTable.getCurrentPackage(); } + /** + * Set the compile-time package for name normalization. + * Called by eval STRING handlers to sync the package from the call site, + * so bare names like *named compile to FOO3::named instead of main::named. + */ + public void setCompilePackage(String packageName) { + symbolTable.setCurrentPackage(packageName, false); + } + /** * Helper: Get all variable names in all scopes (for closure detection). */ @@ -536,7 +545,8 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { errorUtil, // Pass error util for line number lookup strictOptions, // Strict flags for eval STRING inheritance featureFlags, // Feature flags for eval STRING inheritance - warningFlags // Warning flags for eval STRING inheritance + warningFlags, // Warning flags for eval STRING inheritance + symbolTable.getCurrentPackage() // Compile-time package for eval STRING name resolution ); } @@ -2686,7 +2696,7 @@ void compileVariableReference(OperatorNode node, String op) { // Add package prefix if not present if (!varName.contains("::")) { - varName = "main::" + varName; + varName = getCurrentPackage() + "::" + varName; } // Allocate register for glob @@ -2729,6 +2739,16 @@ void compileVariableReference(OperatorNode node, String op) { } else if (op.equals("\\")) { // Reference operator: \$x, \@x, \%x, \*x, etc. if (node.operand != null) { + // Special case: \&name — CODE is already a reference type. + // Emit LOAD_GLOBAL_CODE directly without CREATE_REF, matching JVM compiler. + if (node.operand instanceof OperatorNode operandOp + && operandOp.operator.equals("&") + && operandOp.operand instanceof IdentifierNode) { + node.operand.accept(this); + // lastResultReg already holds the CODE scalar — no wrapping needed + return; + } + // Compile operand in LIST context to get the actual value // Example: \@array should get a reference to the array itself, // not its size (which would happen in SCALAR context) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 954e406f7..b14f13fe8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1558,7 +1558,11 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rd = bytecode[pc++]; int listReg = bytecode[pc++]; - RuntimeList list = (RuntimeList) registers[listReg]; + // registers[listReg] may be a RuntimeList (from CREATE_LIST) or a + // RuntimeScalar (from LOAD_UNDEF when an empty ListNode is compiled). + RuntimeBase listBase = registers[listReg]; + RuntimeList list = (listBase instanceof RuntimeList rl) + ? rl : listBase.getList(); RuntimeScalar result = IOOperator.select(list, RuntimeContextType.SCALAR); registers[rd] = result; break; @@ -1907,7 +1911,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } case Opcodes.GLOB_SLOT_GET: { - // Glob slot access: rd = glob.hashDerefGetNonStrict(key, "main") + // Glob slot access: rd = glob.hashDerefGetNonStrict(key, pkg) // Format: GLOB_SLOT_GET rd globReg keyReg pc = SlowOpcodeHandler.executeGlobSlotGet(bytecode, pc, registers); break; @@ -2020,6 +2024,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.SYSSEEK: case Opcodes.TRUNCATE: case Opcodes.READ: + case Opcodes.OPENDIR: + case Opcodes.READDIR: + case Opcodes.SEEKDIR: pc = MiscOpcodeHandler.execute(opcode, bytecode, pc, registers); break; @@ -3035,25 +3042,25 @@ private static int executeSystemOps(int opcode, int[] bytecode, int pc, RuntimeBase[] registers) { switch (opcode) { case Opcodes.CHOWN: - return SlowOpcodeHandler.executeChown(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.CHOWN, bytecode, pc, registers); case Opcodes.WAITPID: - return SlowOpcodeHandler.executeWaitpid(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.WAITPID, bytecode, pc, registers); case Opcodes.FORK: return SlowOpcodeHandler.executeFork(bytecode, pc, registers); case Opcodes.GETPPID: return SlowOpcodeHandler.executeGetppid(bytecode, pc, registers); case Opcodes.GETPGRP: - return SlowOpcodeHandler.executeGetpgrp(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.GETPGRP, bytecode, pc, registers); case Opcodes.SETPGRP: - return SlowOpcodeHandler.executeSetpgrp(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.SETPGRP, bytecode, pc, registers); case Opcodes.GETPRIORITY: - return SlowOpcodeHandler.executeGetpriority(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.GETPRIORITY, bytecode, pc, registers); case Opcodes.SETPRIORITY: - return SlowOpcodeHandler.executeSetpriority(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.SETPRIORITY, bytecode, pc, registers); case Opcodes.GETSOCKOPT: - return SlowOpcodeHandler.executeGetsockopt(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.GETSOCKOPT, bytecode, pc, registers); case Opcodes.SETSOCKOPT: - return SlowOpcodeHandler.executeSetsockopt(bytecode, pc, registers); + return MiscOpcodeHandler.execute(Opcodes.SETSOCKOPT, bytecode, pc, registers); case Opcodes.SYSCALL: return SlowOpcodeHandler.executeSyscall(bytecode, pc, registers); case Opcodes.SEMGET: diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 8db1e2a77..21a5ea90d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -856,25 +856,23 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode int rd = bytecodeCompiler.allocateRegister(); - if (node.operand != null && node.operand instanceof ListNode) { - // select FILEHANDLE or select() with arguments - // Compile the operand (ListNode containing filehandle ref) + boolean hasArgs = node.operand instanceof ListNode ln && !ln.elements.isEmpty(); + if (hasArgs) { + // select FILEHANDLE or select(RBITS,WBITS,EBITS,TIMEOUT) with arguments node.operand.accept(bytecodeCompiler); int listReg = bytecodeCompiler.lastResultReg; - // Emit SELECT opcode bytecodeCompiler.emitWithToken(Opcodes.SELECT, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(listReg); } else { - // select() with no arguments - returns current filehandle - // Create empty list - bytecodeCompiler.emit(Opcodes.CREATE_LIST); + // select() with no arguments (or empty ListNode from print-without-filehandle) + // Must emit CREATE_LIST so SELECT receives a RuntimeList, not a RuntimeScalar int listReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); bytecodeCompiler.emitReg(listReg); bytecodeCompiler.emit(0); // count = 0 - // Emit SELECT opcode bytecodeCompiler.emitWithToken(Opcodes.SELECT, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(listReg); @@ -2591,62 +2589,6 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitWithToken(Opcodes.GETPPID, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.lastResultReg = rd; - } else if (op.equals("getpgrp")) { - // getpgrp($pid) - returns process group - // Format: GETPGRP rd pidReg - if (node.operand != null) { - node.operand.accept(bytecodeCompiler); - int pidReg = bytecodeCompiler.lastResultReg; - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emitWithToken(Opcodes.GETPGRP, node.getIndex()); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(pidReg); - bytecodeCompiler.lastResultReg = rd; - } else { - bytecodeCompiler.throwCompilerException("getpgrp requires an argument"); - } - } else if (op.equals("setpgrp")) { - // setpgrp($pid, $pgrp) - sets process group - // Format: SETPGRP pidReg pgrpReg - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (list.elements.size() >= 2) { - list.elements.get(0).accept(bytecodeCompiler); - int pidReg = bytecodeCompiler.lastResultReg; - list.elements.get(1).accept(bytecodeCompiler); - int pgrpReg = bytecodeCompiler.lastResultReg; - bytecodeCompiler.emitWithToken(Opcodes.SETPGRP, node.getIndex()); - bytecodeCompiler.emitReg(pidReg); - bytecodeCompiler.emitReg(pgrpReg); - bytecodeCompiler.lastResultReg = -1; // No return value - } else { - bytecodeCompiler.throwCompilerException("setpgrp requires two arguments"); - } - } else { - bytecodeCompiler.throwCompilerException("setpgrp requires two arguments"); - } - } else if (op.equals("getpriority")) { - // getpriority($which, $who) - returns process priority - // Format: GETPRIORITY rd whichReg whoReg - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (list.elements.size() >= 2) { - list.elements.get(0).accept(bytecodeCompiler); - int whichReg = bytecodeCompiler.lastResultReg; - list.elements.get(1).accept(bytecodeCompiler); - int whoReg = bytecodeCompiler.lastResultReg; - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emitWithToken(Opcodes.GETPRIORITY, node.getIndex()); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(whichReg); - bytecodeCompiler.emitReg(whoReg); - bytecodeCompiler.lastResultReg = rd; - } else { - bytecodeCompiler.throwCompilerException("getpriority requires two arguments"); - } - } else { - bytecodeCompiler.throwCompilerException("getpriority requires two arguments"); - } } else if (op.equals("atan2")) { // atan2($y, $x) - returns arctangent of y/x // Format: ATAN2 rd rs1 rs2 @@ -2729,7 +2671,12 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode op.equals("sysopen") || op.equals("socket") || op.equals("bind") || op.equals("connect") || op.equals("listen") || op.equals("write") || op.equals("formline") || op.equals("printf") || op.equals("accept") || - op.equals("sysseek") || op.equals("truncate") || op.equals("read")) { + op.equals("sysseek") || op.equals("truncate") || op.equals("read") || + op.equals("chown") || op.equals("waitpid") || + op.equals("setsockopt") || op.equals("getsockopt") || + op.equals("getpgrp") || op.equals("setpgrp") || + op.equals("getpriority") || op.equals("setpriority") || + op.equals("opendir") || op.equals("readdir") || op.equals("seekdir")) { // Generic handler for operators that take arguments and call runtime methods // Format: OPCODE rd argsReg ctx // argsReg must be a RuntimeList @@ -2795,6 +2742,17 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode case "sysseek" -> Opcodes.SYSSEEK; case "truncate" -> Opcodes.TRUNCATE; case "read" -> Opcodes.READ; + case "chown" -> Opcodes.CHOWN; + case "waitpid" -> Opcodes.WAITPID; + case "setsockopt" -> Opcodes.SETSOCKOPT; + case "getsockopt" -> Opcodes.GETSOCKOPT; + case "getpgrp" -> Opcodes.GETPGRP; + case "setpgrp" -> Opcodes.SETPGRP; + case "getpriority" -> Opcodes.GETPRIORITY; + case "setpriority" -> Opcodes.SETPRIORITY; + case "opendir" -> Opcodes.OPENDIR; + case "readdir" -> Opcodes.READDIR; + case "seekdir" -> Opcodes.SEEKDIR; default -> throw new IllegalStateException("Unexpected operator: " + op); }; diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index b3700b7d7..13db3320e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -76,11 +76,13 @@ public static RuntimeScalar evalString(String perlCode, symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); } - // Inherit the runtime current package so eval STRING compiles in the right package. - // InterpreterState.currentPackage is updated by SET_PACKAGE/PUSH_PACKAGE opcodes - // as "package Foo;" declarations execute at runtime. - String runtimePackage = InterpreterState.currentPackage.get().toString(); - symbolTable.setCurrentPackage(runtimePackage, false); + // Inherit the compile-time package from the calling code, matching what + // evalStringHelper (JVM path) does via capturedSymbolTable.snapShot(). + // Using the compile-time package (not InterpreterState.currentPackage which is + // the runtime package) ensures bare names like *named resolve to FOO3::named + // when the eval call site is inside "package FOO3". + String compilePackage = (currentCode != null) ? currentCode.compilePackage : "main"; + symbolTable.setCurrentPackage(compilePackage, false); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); EmitterContext ctx = new EmitterContext( @@ -161,7 +163,10 @@ public static RuntimeScalar evalString(String perlCode, capturedVars = capturedList.toArray(new RuntimeBase[0]); } - // Step 4: Compile AST to interpreter bytecode with adjusted variable registry + // Step 4: Compile AST to interpreter bytecode with adjusted variable registry. + // The compile-time package is already propagated via ctx.symbolTable (set above + // from currentCode.compilePackage), so BytecodeCompiler will use it for name + // resolution (e.g. *named -> FOO3::named) without needing setCompilePackage(). BytecodeCompiler compiler = new BytecodeCompiler( sourceName + " (eval)", sourceLine, @@ -169,6 +174,9 @@ public static RuntimeScalar evalString(String perlCode, adjustedRegistry // Pass adjusted registry for variable capture ); InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation + if (RuntimeCode.DISASSEMBLE) { + System.out.println(evalCode.disassemble()); + } // Step 4.5: Store source lines in debugger symbol table if $^P flags are set // This implements Perl's eval source retention feature for debugging @@ -242,12 +250,17 @@ public static RuntimeScalar evalString(String perlCode, Parser parser = new Parser(ctx, tokens); Node ast = parser.parse(); - // Compile to bytecode + // Compile to bytecode. + // IMPORTANT: Do NOT call compiler.setCompilePackage() here — same reason as the + // first evalString overload above: it corrupts die/warn location baking. BytecodeCompiler compiler = new BytecodeCompiler( sourceName + " (eval)", sourceLine ); InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation + if (RuntimeCode.DISASSEMBLE) { + System.out.println(evalCode.disassemble()); + } // Store source lines in debugger symbol table if $^P flags are set int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 769d812f8..d90e9619d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -33,6 +33,7 @@ public class InterpretedCode extends RuntimeCode { public final int strictOptions; // Strict flags at compile time public final int featureFlags; // Feature flags at compile time public final BitSet warningFlags; // Warning flags at compile time + public final String compilePackage; // Package at compile time (for eval STRING name resolution) // Debug information (optional) public final String sourceName; // Source file name (for stack traces) @@ -64,6 +65,19 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, Map variableRegistry, ErrorMessageUtil errorUtil, int strictOptions, int featureFlags, BitSet warningFlags) { + this(bytecode, constants, stringPool, maxRegisters, capturedVars, + sourceName, sourceLine, pcToTokenIndex, variableRegistry, errorUtil, + strictOptions, featureFlags, warningFlags, "main"); + } + + public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, + int maxRegisters, RuntimeBase[] capturedVars, + String sourceName, int sourceLine, + TreeMap pcToTokenIndex, + Map variableRegistry, + ErrorMessageUtil errorUtil, + int strictOptions, int featureFlags, BitSet warningFlags, + String compilePackage) { super(null, new java.util.ArrayList<>()); // Call RuntimeCode constructor with null prototype, empty attributes this.bytecode = bytecode; this.constants = constants; @@ -78,6 +92,7 @@ public InterpretedCode(int[] bytecode, Object[] constants, String[] stringPool, this.strictOptions = strictOptions; this.featureFlags = featureFlags; this.warningFlags = warningFlags; + this.compilePackage = compilePackage; } // Legacy constructor for backward compatibility diff --git a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java index b002b5e28..144304f7d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java @@ -2,6 +2,9 @@ import org.perlonjava.runtime.nativ.NativeUtils; import org.perlonjava.runtime.operators.*; +import org.perlonjava.runtime.operators.ChownOperator; +import org.perlonjava.runtime.operators.Directory; +import org.perlonjava.runtime.operators.WaitpidOperator; import org.perlonjava.runtime.operators.Unpack; import org.perlonjava.runtime.runtimetypes.RuntimeBase; import org.perlonjava.runtime.runtimetypes.RuntimeCode; @@ -78,6 +81,17 @@ public static int execute(int opcode, int[] bytecode, int pc, RuntimeBase[] regi case Opcodes.SYSSEEK -> IOOperator.sysseek(ctx, argsArray); case Opcodes.TRUNCATE -> IOOperator.truncate(ctx, argsArray); case Opcodes.READ -> IOOperator.read(ctx, argsArray); + case Opcodes.CHOWN -> ChownOperator.chown(ctx, argsArray); + case Opcodes.WAITPID -> WaitpidOperator.waitpid(ctx, argsArray); + case Opcodes.SETSOCKOPT -> IOOperator.setsockopt(ctx, argsArray); + case Opcodes.GETSOCKOPT -> IOOperator.getsockopt(ctx, argsArray); + case Opcodes.GETPGRP -> Operator.getpgrp(ctx, argsArray); + case Opcodes.SETPGRP -> Operator.setpgrp(ctx, argsArray); + case Opcodes.GETPRIORITY -> Operator.getpriority(ctx, argsArray); + case Opcodes.SETPRIORITY -> new RuntimeScalar(0); // stub - no native impl yet + case Opcodes.OPENDIR -> Directory.opendir(args); + case Opcodes.READDIR -> Directory.readdir(args.elements.isEmpty() ? null : (RuntimeScalar) args.elements.get(0), ctx); + case Opcodes.SEEKDIR -> Directory.seekdir(args); default -> throw new IllegalStateException("Unknown opcode in MiscOpcodeHandler: " + opcode); }; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index dd7ac7b97..559e67830 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1,5 +1,9 @@ package org.perlonjava.backend.bytecode; +// To check for duplicate or out-of-order opcode numbers, run: +// perl dev/tools/check_opcodes.pl src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +// To renumber the absolute block (284+) to fill gaps, add --renumber. + /** * Bytecode opcodes for the PerlOnJava interpreter. * @@ -907,62 +911,58 @@ public class Opcodes { public static final short TIED = 240; // ================================================================= - // BUILT-IN FUNCTION OPCODES - after LASTOP - // Last manually-assigned opcode (for tool reference) - private static final short LASTOP = 240; - - // ================================================================= + // BUILT-IN FUNCTION OPCODES (241-283) // Generated by dev/tools/generate_opcode_handlers.pl // DO NOT EDIT MANUALLY - regenerate using the tool // GENERATED_OPCODES_START // scalar binary operations (atan2, eq, ne, lt, le, gt, ge, cmp, etc.) - public static final short ATAN2 = LASTOP + 1; - public static final short BINARY_AND = LASTOP + 2; - public static final short BINARY_OR = LASTOP + 3; - public static final short BINARY_XOR = LASTOP + 4; - public static final short EQ = LASTOP + 5; - public static final short NE = LASTOP + 6; - public static final short LT = LASTOP + 7; - public static final short LE = LASTOP + 8; - public static final short GT = LASTOP + 9; - public static final short GE = LASTOP + 10; - public static final short CMP = LASTOP + 11; - public static final short X = LASTOP + 12; + public static final short ATAN2 = 241; + public static final short BINARY_AND = 242; + public static final short BINARY_OR = 243; + public static final short BINARY_XOR = 244; + public static final short EQ = 245; + public static final short NE = 246; + public static final short LT = 247; + public static final short LE = 248; + public static final short GT = 249; + public static final short GE = 250; + public static final short CMP = 251; + public static final short X = 252; // scalar unary operations (chr, ord, abs, sin, cos, lc, uc, etc.) - public static final short INT = LASTOP + 13; - public static final short LOG = LASTOP + 14; - public static final short SQRT = LASTOP + 15; - public static final short COS = LASTOP + 16; - public static final short SIN = LASTOP + 17; - public static final short EXP = LASTOP + 18; - public static final short ABS = LASTOP + 19; - public static final short BINARY_NOT = LASTOP + 20; - public static final short INTEGER_BITWISE_NOT = LASTOP + 21; - public static final short ORD = LASTOP + 22; - public static final short ORD_BYTES = LASTOP + 23; - public static final short OCT = LASTOP + 24; - public static final short HEX = LASTOP + 25; - public static final short SRAND = LASTOP + 26; - public static final short CHR = LASTOP + 27; - public static final short CHR_BYTES = LASTOP + 28; - public static final short LENGTH_BYTES = LASTOP + 29; - public static final short QUOTEMETA = LASTOP + 30; - public static final short FC = LASTOP + 31; - public static final short LC = LASTOP + 32; - public static final short LCFIRST = LASTOP + 33; - public static final short UC = LASTOP + 34; - public static final short UCFIRST = LASTOP + 35; - public static final short SLEEP = LASTOP + 36; - public static final short TELL = LASTOP + 37; - public static final short RMDIR = LASTOP + 38; - public static final short CLOSEDIR = LASTOP + 39; - public static final short REWINDDIR = LASTOP + 40; - public static final short TELLDIR = LASTOP + 41; - public static final short CHDIR = LASTOP + 42; - public static final short EXIT = LASTOP + 43; + public static final short INT = 253; + public static final short LOG = 254; + public static final short SQRT = 255; + public static final short COS = 256; + public static final short SIN = 257; + public static final short EXP = 258; + public static final short ABS = 259; + public static final short BINARY_NOT = 260; + public static final short INTEGER_BITWISE_NOT = 261; + public static final short ORD = 262; + public static final short ORD_BYTES = 263; + public static final short OCT = 264; + public static final short HEX = 265; + public static final short SRAND = 266; + public static final short CHR = 267; + public static final short CHR_BYTES = 268; + public static final short LENGTH_BYTES = 269; + public static final short QUOTEMETA = 270; + public static final short FC = 271; + public static final short LC = 272; + public static final short LCFIRST = 273; + public static final short UC = 274; + public static final short UCFIRST = 275; + public static final short SLEEP = 276; + public static final short TELL = 277; + public static final short RMDIR = 278; + public static final short CLOSEDIR = 279; + public static final short REWINDDIR = 280; + public static final short TELLDIR = 281; + public static final short CHDIR = 282; + public static final short EXIT = 283; // GENERATED_OPCODES_END // Miscellaneous operators with context-sensitive signatures (284-301) @@ -1054,6 +1054,13 @@ public class Opcodes { /** read FILEHANDLE,SCALAR,LENGTH: Format: READ rd argsReg ctx */ public static final short READ = 329; + /** opendir DIRHANDLE,EXPR: Format: OPENDIR rd argsReg ctx */ + public static final short OPENDIR = 330; + /** readdir DIRHANDLE: Format: READDIR rd argsReg ctx */ + public static final short READDIR = 331; + /** seekdir DIRHANDLE,POS: Format: SEEKDIR rd argsReg ctx */ + public static final short SEEKDIR = 332; + /** Enter scoped package block (package Foo { ...). * Format: PUSH_PACKAGE nameIdx * Effect: Saves current packageName, sets new one */ diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 5e577f5f0..f99c19fcc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -61,133 +61,6 @@ public class SlowOpcodeHandler { // ================================================================= // ================================================================= - /** - * SLOW_CHOWN: chown(list, uid, gid) - * Format: [SLOW_CHOWN] [rs_list] [rs_uid] [rs_gid] - * Effect: Changes ownership of files in list - */ - public static int executeChown(int[] bytecode, int pc, RuntimeBase[] registers) { - int listReg = bytecode[pc++]; - int uidReg = bytecode[pc++]; - int gidReg = bytecode[pc++]; - - // TODO: Implement chown via JNI or ProcessBuilder - // For now, throw unsupported operation exception - throw new UnsupportedOperationException( - "chown() not yet implemented in interpreter" - ); - } - - /** - * SLOW_WAITPID: rd = waitpid(pid, flags) - * Format: [SLOW_WAITPID] [rd] [rs_pid] [rs_flags] - * Effect: Waits for child process and returns status - */ - public static int executeWaitpid(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int pidReg = bytecode[pc++]; - int flagsReg = bytecode[pc++]; - - RuntimeScalar pid = (RuntimeScalar) registers[pidReg]; - RuntimeScalar flags = (RuntimeScalar) registers[flagsReg]; - - // TODO: Implement waitpid via JNI or ProcessBuilder - // For now, return -1 (error) - registers[rd] = new RuntimeScalar(-1); - return pc; - } - - /** - * SLOW_SETSOCKOPT: setsockopt(socket, level, optname, optval) - * Format: [SLOW_SETSOCKOPT] [rs_socket] [rs_level] [rs_optname] [rs_optval] - * Effect: Sets socket option - */ - public static int executeSetsockopt(int[] bytecode, int pc, RuntimeBase[] registers) { - int socketReg = bytecode[pc++]; - int levelReg = bytecode[pc++]; - int optnameReg = bytecode[pc++]; - int optvalReg = bytecode[pc++]; - - // TODO: Implement via java.nio.channels or JNI - throw new UnsupportedOperationException( - "setsockopt() not yet implemented in interpreter" - ); - } - - /** - * SLOW_GETSOCKOPT: rd = getsockopt(socket, level, optname) - * Format: [SLOW_GETSOCKOPT] [rd] [rs_socket] [rs_level] [rs_optname] - * Effect: Gets socket option value - */ - public static int executeGetsockopt(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int socketReg = bytecode[pc++]; - int levelReg = bytecode[pc++]; - int optnameReg = bytecode[pc++]; - - // TODO: Implement via java.nio.channels or JNI - throw new UnsupportedOperationException( - "getsockopt() not yet implemented in interpreter" - ); - } - - /** - * SLOW_GETPRIORITY: rd = getpriority(which, who) - * Format: [SLOW_GETPRIORITY] [rd] [rs_which] [rs_who] - * Effect: Gets process priority - */ - public static int executeGetpriority(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int whichReg = bytecode[pc++]; - int whoReg = bytecode[pc++]; - - // TODO: Implement via JNI - registers[rd] = new RuntimeScalar(0); // Default priority - return pc; - } - - /** - * SLOW_SETPRIORITY: setpriority(which, who, priority) - * Format: [SLOW_SETPRIORITY] [rs_which] [rs_who] [rs_priority] - * Effect: Sets process priority - */ - public static int executeSetpriority(int[] bytecode, int pc, RuntimeBase[] registers) { - int whichReg = bytecode[pc++]; - int whoReg = bytecode[pc++]; - int priorityReg = bytecode[pc++]; - - // TODO: Implement via JNI - // For now, silently succeed - return pc; - } - - /** - * SLOW_GETPGRP: rd = getpgrp(pid) - * Format: [SLOW_GETPGRP] [rd] [rs_pid] - * Effect: Gets process group ID - */ - public static int executeGetpgrp(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int pidReg = bytecode[pc++]; - - // TODO: Implement via JNI - registers[rd] = new RuntimeScalar(0); - return pc; - } - - /** - * SLOW_SETPGRP: setpgrp(pid, pgrp) - * Format: [SLOW_SETPGRP] [rs_pid] [rs_pgrp] - * Effect: Sets process group ID - */ - public static int executeSetpgrp(int[] bytecode, int pc, RuntimeBase[] registers) { - int pidReg = bytecode[pc++]; - int pgrpReg = bytecode[pc++]; - - // TODO: Implement via JNI - return pc; - } - /** * SLOW_GETPPID: rd = getppid() * Format: [SLOW_GETPPID] [rd] @@ -1030,10 +903,10 @@ public static int executeFiletestLastHandle( } /** - * GLOB_SLOT_GET: rd = glob.hashDerefGetNonStrict(key, "main") + * GLOB_SLOT_GET: rd = glob.hashDerefGetNonStrict(key, pkg) * Format: [GLOB_SLOT_GET] [rd] [globReg] [keyReg] - * Effect: Access glob slot (like *X{HASH}) using RuntimeGlob's override - * This ensures proper glob slot access without incorrectly dereferencing the glob as a hash + * Effect: Access glob slot (like *X{HASH}) using RuntimeGlob's override. + * Uses the runtime current package, which is correct for both regular code and eval STRING. */ public static int executeGlobSlotGet( int[] bytecode, @@ -1047,12 +920,15 @@ public static int executeGlobSlotGet( RuntimeBase globBase = registers[globReg]; RuntimeScalar key = (RuntimeScalar) registers[keyReg]; + // Use runtime current package — correct for both regular code and eval STRING + String pkg = InterpreterState.currentPackage.get().toString(); + // Convert to scalar if needed RuntimeScalar glob = globBase.scalar(); // Call hashDerefGetNonStrict which for RuntimeGlob accesses the slot directly // without dereferencing the glob as a hash - registers[rd] = glob.hashDerefGetNonStrict(key, "main"); + registers[rd] = glob.hashDerefGetNonStrict(key, pkg); return pc; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 55163c53e..51d6e7d88 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -63,6 +63,21 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { public static final boolean EVAL_VERBOSE = System.getenv("JPERL_EVAL_VERBOSE") != null; + /** + * Flag to enable disassembly of eval STRING bytecode. + * When set, prints the interpreter bytecode for each eval STRING compilation. + * + * Set environment variable JPERL_DISASSEMBLE=1 to enable, or use --disassemble CLI flag. + * The --disassemble flag sets this via setDisassemble(). + */ + public static boolean DISASSEMBLE = + System.getenv("JPERL_DISASSEMBLE") != null; + + /** Called by CLI argument parser when --disassemble is set. */ + public static void setDisassemble(boolean value) { + DISASSEMBLE = value; + } + /** * ThreadLocal storage for runtime values of captured variables during eval STRING compilation. * @@ -811,13 +826,27 @@ public static RuntimeList evalStringWithInterpreter( } } - // Compile to InterpretedCode with variable registry + // Compile to InterpretedCode with variable registry. + // + // setCompilePackage() is safe here (unlike EvalStringHandler) because: + // - evalCtx.errorUtil uses evalCompilerOptions.fileName (the outer script name), + // not the eval string's tokens, so die/warn location baking is already + // relative to the outer script and is unaffected by the package change. + // - capturedSymbolTable.getCurrentPackage() gives the compile-time package + // of the eval call site (e.g. "FOO3"), so bare names like *named are + // correctly qualified to FOO3::named in the bytecode string pool. + // - Without this call, the BytecodeCompiler defaults to "main", causing + // eval q[*named{CODE}] to look up main::named instead of FOO3::named. BytecodeCompiler compiler = new BytecodeCompiler( evalCompilerOptions.fileName, 1, evalCtx.errorUtil, adjustedRegistry); + compiler.setCompilePackage(capturedSymbolTable.getCurrentPackage()); interpretedCode = compiler.compile(ast, evalCtx); + if (DISASSEMBLE) { + System.out.println(interpretedCode.disassemble()); + } // Set captured variables if (runtimeValues.length > 0) { diff --git a/src/main/perl/lib/B.pm b/src/main/perl/lib/B.pm index cb5bc40fc..62af71c8f 100644 --- a/src/main/perl/lib/B.pm +++ b/src/main/perl/lib/B.pm @@ -112,11 +112,23 @@ package B; # Main introspection function sub svref_2object { my $ref = shift; - - if (ref($ref) eq 'CODE') { + my $type = ref($ref); + + # A plain CODE scalar (e.g. from \&f in interpreter mode) has ref() eq 'CODE'. + # A CODE-typed scalar passed directly (not wrapped in REFERENCE) also needs + # to be treated as a CV — detect it via Scalar::Util::reftype as well. + if ($type eq 'CODE') { return B::CV->new($ref); } - + + # Scalar::Util::reftype sees through blessing; use it as a fallback + # for cases where ref() returns a package name (blessed code ref). + require Scalar::Util; + my $rtype = Scalar::Util::reftype($ref) // ''; + if ($rtype eq 'CODE') { + return B::CV->new($ref); + } + return B::SV->new($ref); }