From cce10fa97292c21f5139686c1fcee9306ab04b9d Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 15:59:08 +0100 Subject: [PATCH 01/10] Wire chown, waitpid, setsockopt, getsockopt, getpgrp, setpgrp, getpriority, setpriority to real runtime implementations via MiscOpcodeHandler --- .../backend/bytecode/BytecodeInterpreter.java | 16 ++--- .../backend/bytecode/CompileOperator.java | 70 ++++--------------- .../backend/bytecode/MiscOpcodeHandler.java | 10 +++ 3 files changed, 31 insertions(+), 65 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 954e406f7..01b5f380e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -3035,25 +3035,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..b4e6af53d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -2591,62 +2591,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 +2673,11 @@ 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")) { // Generic handler for operators that take arguments and call runtime methods // Format: OPCODE rd argsReg ctx // argsReg must be a RuntimeList @@ -2795,6 +2743,14 @@ 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; default -> throw new IllegalStateException("Unexpected operator: " + op); }; diff --git a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java index b002b5e28..054a385fe 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java @@ -2,6 +2,8 @@ import org.perlonjava.runtime.nativ.NativeUtils; import org.perlonjava.runtime.operators.*; +import org.perlonjava.runtime.operators.ChownOperator; +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 +80,14 @@ 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 default -> throw new IllegalStateException("Unknown opcode in MiscOpcodeHandler: " + opcode); }; From 19428c1fd7e05a7a0f456bf9548f22c1c9a82007 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 16:00:32 +0100 Subject: [PATCH 02/10] Remove dead SlowOpcodeHandler stubs for chown, waitpid, setsockopt, getsockopt, getpgrp, setpgrp, getpriority, setpriority --- .../backend/bytecode/SlowOpcodeHandler.java | 127 ------------------ 1 file changed, 127 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 5e577f5f0..24253a49c 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] From 13b4eb2b35b0f76c0b9395bbe4d882f1c3a7c975 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 16:09:07 +0100 Subject: [PATCH 03/10] Replace LASTOP+N with plain numbers in Opcodes.java; add dev/tools/check_opcodes.pl --- dev/tools/check_opcodes.pl | 81 ++++++++++++++ .../perlonjava/backend/bytecode/Opcodes.java | 103 ++++++++++-------- 2 files changed, 136 insertions(+), 48 deletions(-) create mode 100644 dev/tools/check_opcodes.pl 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/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 */ From a093ef9a8f2194695a67748de75ec04eced06987 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 16:12:05 +0100 Subject: [PATCH 04/10] Add opendir, readdir, seekdir to interpreter: opcodes, MiscOpcodeHandler dispatch, CompileOperator, BytecodeInterpreter --- .../perlonjava/backend/bytecode/BytecodeInterpreter.java | 3 +++ .../org/perlonjava/backend/bytecode/CompileOperator.java | 6 +++++- .../org/perlonjava/backend/bytecode/MiscOpcodeHandler.java | 4 ++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 01b5f380e..05d358507 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2020,6 +2020,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; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index b4e6af53d..d39941c67 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -2677,7 +2677,8 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode 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("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 @@ -2751,6 +2752,9 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode 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/MiscOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java index 054a385fe..144304f7d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java @@ -3,6 +3,7 @@ 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; @@ -88,6 +89,9 @@ public static int execute(int opcode, int[] bytecode, int pc, RuntimeBase[] regi 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); }; From 9b4f7ccb8c9e15630702f7184a7d59d494dc3aab Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 16:37:56 +0100 Subject: [PATCH 05/10] Fix ref(\&f) in interpreter: skip CREATE_REF for CODE, fix glob * package, fix B::svref_2object for CODE scalars --- .../backend/bytecode/BytecodeCompiler.java | 12 +++++++++++- .../backend/bytecode/BytecodeInterpreter.java | 2 +- .../backend/bytecode/SlowOpcodeHandler.java | 11 +++++++---- src/main/perl/lib/B.pm | 18 +++++++++++++++--- 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 27e1de016..6e65caee1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -2686,7 +2686,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 +2729,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 05d358507..77c6cb9f2 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1907,7 +1907,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; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index 24253a49c..f99c19fcc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -903,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, @@ -920,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/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); } From fcac2c3b727810d96977a1eba5602eeeae24a695 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 16:52:59 +0100 Subject: [PATCH 06/10] Fix eval STRING register allocation: correct package for LOAD_GLOB, fix SELECT with empty ListNode --- .../backend/bytecode/BytecodeCompiler.java | 11 +++++++ .../backend/bytecode/CompileOperator.java | 32 ++++++++++++------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 6e65caee1..8553c8096 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -460,6 +460,17 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Use the calling context from EmitterContext for top-level expressions // This is crucial for eval STRING to propagate context correctly currentCallContext = ctx.contextType; + + // Inherit the current package from the EmitterContext symbol table. + // This is critical for eval STRING: the eval compiler creates a fresh + // BytecodeCompiler (symbolTable defaults to "main"), but the ctx.symbolTable + // has already been set to the runtime package (e.g. "FOO3") by EvalStringHandler. + if (ctx.symbolTable != null) { + String pkg = ctx.symbolTable.getCurrentPackage(); + if (pkg != null && !pkg.equals("main")) { + symbolTable.setCurrentPackage(pkg, false); + } + } } // If we have captured variables, allocate registers for them diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index d39941c67..7d9d2650e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -101,13 +101,25 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.throwCompilerException(op + " operator requires an identifier"); } } else if (op.equals("say") || op.equals("print")) { - // say/print $x + // say/print $x (no explicit filehandle — use select() default) if (node.operand != null) { node.operand.accept(bytecodeCompiler); - int rs = bytecodeCompiler.lastResultReg; + int contentReg = bytecodeCompiler.lastResultReg; + + // Emit SELECT with empty list to get the default filehandle, + // matching the CompileBinaryOperator path for print FILEHANDLE LIST + int listReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(listReg); + bytecodeCompiler.emit(0); // count = 0 + int fhReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.SELECT, node.getIndex()); + bytecodeCompiler.emitReg(fhReg); + bytecodeCompiler.emitReg(listReg); bytecodeCompiler.emit(op.equals("say") ? Opcodes.SAY : Opcodes.PRINT); - bytecodeCompiler.emitReg(rs); + bytecodeCompiler.emitReg(contentReg); + bytecodeCompiler.emitReg(fhReg); } } else if (op.equals("not") || op.equals("!")) { // Logical NOT operator: not $x or !$x @@ -856,25 +868,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); From a12f6bc17b694d134d027f47e802186d5afc87e1 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 17:15:36 +0100 Subject: [PATCH 07/10] Fix select() with empty ListNode: emit CREATE_LIST not LOAD_UNDEF for print default filehandle --- .../backend/bytecode/CompileOperator.java | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 7d9d2650e..21a5ea90d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -101,25 +101,13 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.throwCompilerException(op + " operator requires an identifier"); } } else if (op.equals("say") || op.equals("print")) { - // say/print $x (no explicit filehandle — use select() default) + // say/print $x if (node.operand != null) { node.operand.accept(bytecodeCompiler); - int contentReg = bytecodeCompiler.lastResultReg; - - // Emit SELECT with empty list to get the default filehandle, - // matching the CompileBinaryOperator path for print FILEHANDLE LIST - int listReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.CREATE_LIST); - bytecodeCompiler.emitReg(listReg); - bytecodeCompiler.emit(0); // count = 0 - int fhReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emitWithToken(Opcodes.SELECT, node.getIndex()); - bytecodeCompiler.emitReg(fhReg); - bytecodeCompiler.emitReg(listReg); + int rs = bytecodeCompiler.lastResultReg; bytecodeCompiler.emit(op.equals("say") ? Opcodes.SAY : Opcodes.PRINT); - bytecodeCompiler.emitReg(contentReg); - bytecodeCompiler.emitReg(fhReg); + bytecodeCompiler.emitReg(rs); } } else if (op.equals("not") || op.equals("!")) { // Logical NOT operator: not $x or !$x From 65b252af6c64fbbf987c87dea47ec0d0f8da843c Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 17:59:29 +0100 Subject: [PATCH 08/10] Fix signatures.t regression: revert BytecodeCompiler package sync, fix SELECT robustness, add EvalStringHandler package sync MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Revert BytecodeCompiler.compile() package sync (from fcac2c3b) which broke signatures.t by corrupting register allocation in subroutine compilation - Restore CompileOperator print path to original single-register PRINT emit (a12f6bc1 select fix is preserved) - Fix BytecodeInterpreter SELECT handler to accept RuntimeScalar (LOAD_UNDEF) as well as RuntimeList — empty ListNode compiles to LOAD_UNDEF - Add targeted package sync in EvalStringHandler after BytecodeCompiler construction so *named resolves to correct package in eval STRING --- .../perlonjava/backend/bytecode/BytecodeCompiler.java | 11 ----------- .../backend/bytecode/BytecodeInterpreter.java | 6 +++++- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 8553c8096..6e65caee1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -460,17 +460,6 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Use the calling context from EmitterContext for top-level expressions // This is crucial for eval STRING to propagate context correctly currentCallContext = ctx.contextType; - - // Inherit the current package from the EmitterContext symbol table. - // This is critical for eval STRING: the eval compiler creates a fresh - // BytecodeCompiler (symbolTable defaults to "main"), but the ctx.symbolTable - // has already been set to the runtime package (e.g. "FOO3") by EvalStringHandler. - if (ctx.symbolTable != null) { - String pkg = ctx.symbolTable.getCurrentPackage(); - if (pkg != null && !pkg.equals("main")) { - symbolTable.setCurrentPackage(pkg, false); - } - } } // If we have captured variables, allocate registers for them diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 77c6cb9f2..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; From f8127382efa286771f47c9f01a639cc25f048ba2 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 19:10:34 +0100 Subject: [PATCH 09/10] Add JPERL_DISASSEMBLE flag, fix eval STRING package for stash.t - RuntimeCode.DISASSEMBLE: new static flag set by JPERL_DISASSEMBLE env var or --disassemble CLI flag; prints interpreter bytecode for each eval STRING - BytecodeCompiler.setCompilePackage(): new public method to sync compile-time package from eval call site - RuntimeCode.evalStringWithInterpreter: call setCompilePackage() so bare names like *named compile to FOO3::named instead of main::named; fixes eval q[*named{CODE}] returning undef in JPERL_EVAL_USE_INTERPRETER=1 mode - EvalStringHandler: add DISASSEMBLE output; document why setCompilePackage must NOT be called here (corrupts die/warn location baking) - ArgumentParser: set RuntimeCode.DISASSEMBLE when --disassemble is passed stash.t: 34/56 passing with JPERL_EVAL_USE_INTERPRETER=1 (was 16) signatures.t: 597/908 passing (unchanged from HEAD) --- .../perlonjava/app/cli/ArgumentParser.java | 1 + .../backend/bytecode/BytecodeCompiler.java | 9 ++++++ .../backend/bytecode/EvalStringHandler.java | 23 ++++++++++++-- .../runtime/runtimetypes/RuntimeCode.java | 31 ++++++++++++++++++- 4 files changed, 61 insertions(+), 3 deletions(-) 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 6e65caee1..85a98dc29 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). */ diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index b3700b7d7..84ff578f1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -161,7 +161,18 @@ 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. + // + // IMPORTANT: Do NOT call compiler.setCompilePackage() here. + // The package context is already correct because: + // 1. The BytecodeCompiler uses `errorUtil` (constructed above with the eval's tokens) + // to bake "at file line N" into die/warn nodes at compile time. + // 2. Calling setCompilePackage() would change symbolTable.currentPackage, which + // shifts how dieWarnNode() in OperatorParser maps token indices to line numbers, + // causing signature-validation die nodes to report wrong locations. + // 3. Package-qualified name resolution (e.g. *named -> FOO3::named) is handled + // by the SET_PACKAGE opcode emitted at runtime by the outer script, which + // updates InterpreterState.currentPackage before this eval runs. BytecodeCompiler compiler = new BytecodeCompiler( sourceName + " (eval)", sourceLine, @@ -169,6 +180,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 +256,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/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) { From f11df9a784e7fa4b31d3e42f3d66ce95aa7d0ac2 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 19:36:35 +0100 Subject: [PATCH 10/10] Fix eval STRING package resolution: use compile-time package from InterpretedCode Store the compile-time package in InterpretedCode.compilePackage (set by BytecodeCompiler from symbolTable.getCurrentPackage() when building the code). EvalStringHandler now uses currentCode.compilePackage instead of InterpreterState.currentPackage (runtime package). This matches exactly what evalStringHelper (JVM path) does via capturedSymbolTable.snapShot(): - compile-time package = package at the eval call site in source - runtime package = package last set by a 'package Foo;' statement at runtime Using the runtime package was wrong: it caused bare names like *named to resolve to the wrong package when multiple package declarations had run. stash.t: 34/56 passing with and without JPERL_EVAL_USE_INTERPRETER=1 (was 16) signatures.t: 597/908 passing (no regression) --- .../backend/bytecode/BytecodeCompiler.java | 3 ++- .../backend/bytecode/EvalStringHandler.java | 26 +++++++------------ .../backend/bytecode/InterpretedCode.java | 15 +++++++++++ 3 files changed, 27 insertions(+), 17 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 85a98dc29..5b4a35fb6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -545,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 ); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 84ff578f1..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( @@ -162,17 +164,9 @@ public static RuntimeScalar evalString(String perlCode, } // Step 4: Compile AST to interpreter bytecode with adjusted variable registry. - // - // IMPORTANT: Do NOT call compiler.setCompilePackage() here. - // The package context is already correct because: - // 1. The BytecodeCompiler uses `errorUtil` (constructed above with the eval's tokens) - // to bake "at file line N" into die/warn nodes at compile time. - // 2. Calling setCompilePackage() would change symbolTable.currentPackage, which - // shifts how dieWarnNode() in OperatorParser maps token indices to line numbers, - // causing signature-validation die nodes to report wrong locations. - // 3. Package-qualified name resolution (e.g. *named -> FOO3::named) is handled - // by the SET_PACKAGE opcode emitted at runtime by the outer script, which - // updates InterpreterState.currentPackage before this eval runs. + // 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, 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