From ae5a01f0911dc45aa9e4fe7f005da29ccbd76ea6 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 20:02:59 +0100 Subject: [PATCH 1/3] Add missing bytecode operators for ExifTool support - Binary operator handlers: binmode, seek, eof, printf, close, fileno, getc, tell, join (as BinaryOperatorNode form) - $#array = value assignment (SET_ARRAY_LAST_INDEX opcode) - Logical xor/^^ operator (XOR_LOGICAL opcode) - //= defined-or assignment (DEFINED_OR_ASSIGN opcode) - CONCAT: handle RuntimeList operands via scalar coercion ExifTool now runs successfully in interpreter mode. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 1 + .../backend/bytecode/BytecodeInterpreter.java | 28 +++++- .../backend/bytecode/CompileAssignment.java | 47 +++++++++- .../bytecode/CompileBinaryOperator.java | 90 ++++++++++++++++++- .../bytecode/CompileBinaryOperatorHelper.java | 6 ++ .../bytecode/OpcodeHandlerExtended.java | 12 +++ .../perlonjava/backend/bytecode/Opcodes.java | 9 ++ 7 files changed, 189 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index b71fde291..557bbbaf7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1477,6 +1477,7 @@ void handleCompoundAssignment(BinaryOperatorNode node) { case ">>=" -> emit(Opcodes.RIGHT_SHIFT_ASSIGN); // Right shift case "&&=" -> emit(Opcodes.LOGICAL_AND_ASSIGN); // Logical AND case "||=" -> emit(Opcodes.LOGICAL_OR_ASSIGN); // Logical OR + case "//=" -> emit(Opcodes.DEFINED_OR_ASSIGN); // Defined-or default -> { throwCompilerException("Unknown compound assignment operator: " + op); currentCallContext = savedContext; diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index cd123fc68..4930bd3c7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -534,9 +534,11 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int rd = bytecode[pc++]; int rs1 = bytecode[pc++]; int rs2 = bytecode[pc++]; + RuntimeBase concatLeft = registers[rs1]; + RuntimeBase concatRight = registers[rs2]; registers[rd] = StringOperators.stringConcat( - (RuntimeScalar) registers[rs1], - (RuntimeScalar) registers[rs2] + concatLeft instanceof RuntimeScalar ? (RuntimeScalar) concatLeft : concatLeft.scalar(), + concatRight instanceof RuntimeScalar ? (RuntimeScalar) concatRight : concatRight.scalar() ); break; } @@ -706,6 +708,12 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = OpcodeHandlerExtended.executeLogicalOrAssign(bytecode, pc, registers); break; + case Opcodes.DEFINED_OR_ASSIGN: + // Compound assignment: rd //= rs (short-circuit) + // Format: DEFINED_OR_ASSIGN rd rs + pc = OpcodeHandlerExtended.executeDefinedOrAssign(bytecode, pc, registers); + break; + // ================================================================= // SHIFT OPERATIONS // ================================================================= @@ -829,6 +837,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.SET_ARRAY_LAST_INDEX: { + int arrayReg = bytecode[pc++]; + int valueReg = bytecode[pc++]; + RuntimeArray.indexLastElem((RuntimeArray) registers[arrayReg]) + .set(((RuntimeScalar) registers[valueReg])); + break; + } + case Opcodes.CREATE_ARRAY: { // Create array reference from list: rd = new RuntimeArray(rs_list).createReference() // Array literals always return references in Perl @@ -1199,6 +1215,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = OpcodeHandlerExtended.executeStringBitwiseXor(bytecode, pc, registers); break; + case Opcodes.XOR_LOGICAL: { + int rd = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + registers[rd] = Operator.xor((RuntimeScalar) registers[rs1], (RuntimeScalar) registers[rs2]); + break; + } + case Opcodes.BITWISE_NOT_BINARY: // Numeric bitwise NOT: rd = binary~ rs // Format: BITWISE_NOT_BINARY rd rs diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 056e6ee0a..19ba6fbed 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -1032,8 +1032,13 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, } else { bytecodeCompiler.throwCompilerException("Assignment to unsupported array dereference"); } + } else if (leftOp.operator.equals("$#")) { + int arrayReg = CompileAssignment.resolveArrayForDollarHash(bytecodeCompiler, leftOp); + bytecodeCompiler.emit(Opcodes.SET_ARRAY_LAST_INDEX); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.lastResultReg = valueReg; } else { - // chop/chomp cannot be used as lvalues (matches JVM compiler message) if (leftOp.operator.equals("chop") || leftOp.operator.equals("chomp")) { bytecodeCompiler.throwCompilerException("Can't modify " + leftOp.operator + " in scalar assignment"); } @@ -1663,4 +1668,44 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.currentCallContext = savedContext; } } + + static int resolveArrayForDollarHash(BytecodeCompiler bytecodeCompiler, OperatorNode dollarHashOp) { + if (dollarHashOp.operand instanceof OperatorNode operandOp + && operandOp.operator.equals("@") && operandOp.operand instanceof IdentifierNode idNode) { + String varName = "@" + idNode.name; + if (bytecodeCompiler.hasVariable(varName)) { + return bytecodeCompiler.getVariableRegister(varName); + } + int arrayReg = bytecodeCompiler.allocateRegister(); + String globalName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + return arrayReg; + } else if (dollarHashOp.operand instanceof IdentifierNode idNode) { + String varName = "@" + idNode.name; + if (bytecodeCompiler.hasVariable(varName)) { + return bytecodeCompiler.getVariableRegister(varName); + } + int arrayReg = bytecodeCompiler.allocateRegister(); + String globalName = NameNormalizer.normalizeVariableName(idNode.name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + return arrayReg; + } else if (dollarHashOp.operand instanceof OperatorNode operandOp && operandOp.operator.equals("$")) { + operandOp.accept(bytecodeCompiler); + int refReg = bytecodeCompiler.lastResultReg; + int arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, dollarHashOp.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(refReg); + return arrayReg; + } + bytecodeCompiler.throwCompilerException("$# assignment requires array variable"); + return -1; + } } + diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index d9e55c0c5..98815c879 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -85,6 +85,21 @@ static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperato return; } + // Handle I/O and misc binary operators that use MiscOpcodeHandler (filehandle + args → list) + switch (node.operator) { + case "binmode", "seek", "eof", "close", "fileno", "getc", "printf": + compileBinaryAsListOp(bytecodeCompiler, node); + return; + case "tell": + compileTellBinaryOp(bytecodeCompiler, node); + return; + case "join": + compileJoinBinaryOp(bytecodeCompiler, node); + return; + default: + break; + } + // Handle compound assignment operators (+=, -=, *=, /=, %=, .=, &=, |=, ^=, &.=, |.=, ^.=, binary&=, binary|=, binary^=, x=, **=, <<=, >>=, &&=, ||=) if (node.operator.equals("+=") || node.operator.equals("-=") || node.operator.equals("*=") || node.operator.equals("/=") || @@ -93,7 +108,7 @@ static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperato node.operator.equals("&.=") || node.operator.equals("|.=") || node.operator.equals("^.=") || node.operator.equals("x=") || node.operator.equals("**=") || node.operator.equals("<<=") || node.operator.equals(">>=") || - node.operator.equals("&&=") || node.operator.equals("||=") || + node.operator.equals("&&=") || node.operator.equals("||=") || node.operator.equals("//=") || node.operator.startsWith("binary")) { // Handle binary&=, binary|=, binary^= bytecodeCompiler.handleCompoundAssignment(node); return; @@ -633,6 +648,79 @@ else if (node.right instanceof BinaryOperatorNode) { int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(bytecodeCompiler, node.operator, rs1, rs2, node.getIndex()); + bytecodeCompiler.lastResultReg = rd; + } + + private static void compileBinaryAsListOp(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { + node.left.accept(bytecodeCompiler); + int fhReg = bytecodeCompiler.lastResultReg; + + java.util.List argRegs = new java.util.ArrayList<>(); + argRegs.add(fhReg); + + if (node.right instanceof ListNode argsList) { + for (Node arg : argsList.elements) { + arg.accept(bytecodeCompiler); + argRegs.add(bytecodeCompiler.lastResultReg); + } + } else { + node.right.accept(bytecodeCompiler); + argRegs.add(bytecodeCompiler.lastResultReg); + } + + int argsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(argsListReg); + bytecodeCompiler.emit(argRegs.size()); + for (int argReg : argRegs) { + bytecodeCompiler.emitReg(argReg); + } + + int opcode = switch (node.operator) { + case "binmode" -> Opcodes.BINMODE; + case "seek" -> Opcodes.SEEK; + case "eof" -> Opcodes.EOF_OP; + case "close" -> Opcodes.CLOSE; + case "fileno" -> Opcodes.FILENO; + case "getc" -> Opcodes.GETC; + case "printf" -> Opcodes.PRINTF; + default -> throw new RuntimeException("Unknown operator: " + node.operator); + }; + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(opcode); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argsListReg); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + + bytecodeCompiler.lastResultReg = rd; + } + + private static void compileTellBinaryOp(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { + node.left.accept(bytecodeCompiler); + int fhReg = bytecodeCompiler.lastResultReg; + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.TELL); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(fhReg); + + bytecodeCompiler.lastResultReg = rd; + } + + private static void compileJoinBinaryOp(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { + node.left.accept(bytecodeCompiler); + int separatorReg = bytecodeCompiler.lastResultReg; + + node.right.accept(bytecodeCompiler); + int listReg = bytecodeCompiler.lastResultReg; + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.JOIN); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(separatorReg); + bytecodeCompiler.emitReg(listReg); + bytecodeCompiler.lastResultReg = rd; } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java index f06559c5f..5606363e1 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java @@ -419,6 +419,12 @@ public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } + case "xor", "^^" -> { + bytecodeCompiler.emit(Opcodes.XOR_LOGICAL); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } case "..." -> { // Flip-flop operator (.. and ...) - per-call-site state via unique ID // Note: numeric range (..) is handled earlier in visitBinaryOperator for list context; diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 3ce4057f6..4849678ec 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -223,6 +223,18 @@ public static int executeLogicalOrAssign(int[] bytecode, int pc, RuntimeBase[] r return pc; } + public static int executeDefinedOrAssign(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + RuntimeScalar s1 = ((RuntimeBase) registers[rd]).scalar(); + if (s1.getDefinedBoolean()) { + return pc; + } + RuntimeScalar s2 = ((RuntimeBase) registers[rs]).scalar(); + ((RuntimeScalar) registers[rd]).set(s2); + return pc; + } + /** * Execute string concatenation assign operation. * Format: STRING_CONCAT_ASSIGN rd rs diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index e809cc50e..56fa0170b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1140,5 +1140,14 @@ public class Opcodes { * Format: HASH_KEYVALUE_SLICE rd hashReg keysListReg */ public static final short HASH_KEYVALUE_SLICE = 344; + /** Set $#array = value: Format: SET_ARRAY_LAST_INDEX arrayReg valueReg */ + public static final short SET_ARRAY_LAST_INDEX = 347; + + /** Logical xor: rd = left xor right. Format: XOR_LOGICAL rd rs1 rs2 */ + public static final short XOR_LOGICAL = 348; + + /** Defined-or assignment: rd //= rs. Format: DEFINED_OR_ASSIGN rd rs */ + public static final short DEFINED_OR_ASSIGN = 349; + private Opcodes() {} // Utility class - no instantiation } From bb14a9aa51ed7c24e1cab4630c7040173cf9b6cb Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 20:45:17 +0100 Subject: [PATCH 2/3] Fix bytecode join operator to build list from ListNode elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The compileJoinBinaryOp was only using lastResultReg from node.right.accept(), which for a ListNode with multiple elements (e.g. string interpolation "$x-$y") would only capture the last element. Now properly iterates ListNode elements and emits CREATE_LIST before JOIN. This fixes a regression in re/regexp.t (1565→1784) caused by string interpolation in eval strings producing empty/partial results. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../bytecode/CompileBinaryOperator.java | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 98815c879..57a40c1ce 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -108,7 +108,8 @@ static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperato node.operator.equals("&.=") || node.operator.equals("|.=") || node.operator.equals("^.=") || node.operator.equals("x=") || node.operator.equals("**=") || node.operator.equals("<<=") || node.operator.equals(">>=") || - node.operator.equals("&&=") || node.operator.equals("||=") || node.operator.equals("//=") || + node.operator.equals("&&=") || node.operator.equals("||=") || + node.operator.equals("//=") || node.operator.startsWith("binary")) { // Handle binary&=, binary|=, binary^= bytecodeCompiler.handleCompoundAssignment(node); return; @@ -712,8 +713,24 @@ private static void compileJoinBinaryOp(BytecodeCompiler bytecodeCompiler, Binar node.left.accept(bytecodeCompiler); int separatorReg = bytecodeCompiler.lastResultReg; - node.right.accept(bytecodeCompiler); - int listReg = bytecodeCompiler.lastResultReg; + java.util.List argRegs = new java.util.ArrayList<>(); + if (node.right instanceof ListNode listNode) { + for (Node arg : listNode.elements) { + arg.accept(bytecodeCompiler); + argRegs.add(bytecodeCompiler.lastResultReg); + } + } else { + node.right.accept(bytecodeCompiler); + argRegs.add(bytecodeCompiler.lastResultReg); + } + + int listReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(listReg); + bytecodeCompiler.emit(argRegs.size()); + for (int argReg : argRegs) { + bytecodeCompiler.emitReg(argReg); + } int rd = bytecodeCompiler.allocateRegister(); bytecodeCompiler.emit(Opcodes.JOIN); From fd0dc570e9d57088aee09585c6745b52cf67dbcd Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 26 Feb 2026 21:03:13 +0100 Subject: [PATCH 3/3] Fix bytecode join to use LIST context for non-ListNode operands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Array interpolation in eval strings (e.g. ">@array<") was getting scalar context (array count) instead of list context (array elements) because compileJoinBinaryOp didn't set LIST context for the right operand when it wasn't a ListNode. This fixes base/lex.t (-3 → +1) and re/regexp.t (1784 → 1786). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../bytecode/CompileBinaryOperator.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 57a40c1ce..98010eb1e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -713,23 +713,26 @@ private static void compileJoinBinaryOp(BytecodeCompiler bytecodeCompiler, Binar node.left.accept(bytecodeCompiler); int separatorReg = bytecodeCompiler.lastResultReg; - java.util.List argRegs = new java.util.ArrayList<>(); + int listReg; if (node.right instanceof ListNode listNode) { + java.util.List argRegs = new java.util.ArrayList<>(); for (Node arg : listNode.elements) { arg.accept(bytecodeCompiler); argRegs.add(bytecodeCompiler.lastResultReg); } + listReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(listReg); + bytecodeCompiler.emit(argRegs.size()); + for (int argReg : argRegs) { + bytecodeCompiler.emitReg(argReg); + } } else { + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; node.right.accept(bytecodeCompiler); - argRegs.add(bytecodeCompiler.lastResultReg); - } - - int listReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.CREATE_LIST); - bytecodeCompiler.emitReg(listReg); - bytecodeCompiler.emit(argRegs.size()); - for (int argReg : argRegs) { - bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.currentCallContext = savedContext; + listReg = bytecodeCompiler.lastResultReg; } int rd = bytecodeCompiler.allocateRegister();