From df9f926a5b95233208c34b3fe9ded40bb57e4dad Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Feb 2026 14:51:55 +0100 Subject: [PATCH 1/2] Fix interpreter eval: compound assign globals, package block runtime restore - BytecodeCompiler.handleCompoundAssignment: strip sigil before normalizeVariableName for LOAD_GLOBAL_SCALAR, so e.g. '$x += 1' loads 'main::x' not 'main::$x' Also remove redundant STORE_GLOBAL_SCALAR (LOAD loads live object, mutated in-place) Fixes: $x += 1, $x .= 'a' etc in eval STRING (comp/package_block.t 1-3,5) - Add GET_LOCAL_LEVEL opcode (341): saves DynamicVariableManager.getLocalLevel() to register - StatementParser: set isScoped annotation on packageNode for 'package Foo { }' blocks - BlockNode.visit(): detect scoped package first-child, bracket with GET_LOCAL_LEVEL + POP_LOCAL_LEVEL so PUSH_PACKAGE is restored after block exits Fixes: runtime package restore after 'package Foo { }' in eval STRING Fixes: eval("__PACKAGE__") returns correct value inside/outside package blocks - EvalStringHandler: use InterpreterState.currentPackage (runtime) not compilePackage for the inner eval compile context, so eval("__PACKAGE__") reflects call-site package Files changed: BytecodeCompiler.java, BytecodeInterpreter.java, InterpretedCode.java, Opcodes.java, EvalStringHandler.java, StatementParser.java --- .../backend/bytecode/BytecodeCompiler.java | 31 ++++++++++++++----- .../backend/bytecode/BytecodeInterpreter.java | 7 +++++ .../backend/bytecode/EvalStringHandler.java | 11 +++---- .../backend/bytecode/InterpretedCode.java | 3 ++ .../perlonjava/backend/bytecode/Opcodes.java | 5 +++ .../frontend/parser/StatementParser.java | 5 +++ 6 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index d8b15e446..9f61e381d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -707,6 +707,18 @@ public void visit(BlockNode node) { && node.elements.get(0) instanceof OperatorNode localOp && localOp.operator.equals("local"); + // If the first statement is a scoped package (package Foo { }), + // save the DynamicVariableManager level before the block body so PUSH_PACKAGE is restored. + int scopedPackageLevelReg = -1; + if (!node.elements.isEmpty() + && node.elements.get(0) instanceof OperatorNode firstOp + && (firstOp.operator.equals("package") || firstOp.operator.equals("class")) + && Boolean.TRUE.equals(firstOp.getAnnotation("isScoped"))) { + scopedPackageLevelReg = allocateRegister(); + emit(Opcodes.GET_LOCAL_LEVEL); + emitReg(scopedPackageLevelReg); + } + enterScope(); // Visit each statement in the block @@ -752,6 +764,13 @@ public void visit(BlockNode node) { // Exit scope restores register state exitScope(); + // Restore DynamicVariableManager level after scoped package block + // (undoes PUSH_PACKAGE emitted by the package operator inside the block) + if (scopedPackageLevelReg >= 0) { + emit(Opcodes.POP_LOCAL_LEVEL); + emitReg(scopedPackageLevelReg); + } + // Set lastResultReg to the outer register (or -1 if VOID context) lastResultReg = outerResultReg; } @@ -1244,7 +1263,9 @@ void handleCompoundAssignment(BinaryOperatorNode node) { // Global variable - need to load it first isGlobal = true; targetReg = allocateRegister(); - String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); + // Strip sigil before normalizing (varName is "$x", need "x" for normalize) + String normalizedName = NameNormalizer.normalizeVariableName( + varName.substring(1), getCurrentPackage()); int nameIdx = addToStringPool(normalizedName); emit(Opcodes.LOAD_GLOBAL_SCALAR); emitReg(targetReg); @@ -1313,12 +1334,8 @@ void handleCompoundAssignment(BinaryOperatorNode node) { if (shouldBlockGlobalUnderStrictVars(varName)) { throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); } - - String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); - int nameIdx = addToStringPool(normalizedName); - emit(Opcodes.STORE_GLOBAL_SCALAR); - emit(nameIdx); - emitReg(targetReg); + // LOAD_GLOBAL_SCALAR loaded the live object; the compound-assign opcode + // already mutated it in-place via .set(), so no STORE_GLOBAL_SCALAR needed. } // The result is stored in targetReg diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index f0b90bb9a..22f6e963f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2083,6 +2083,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.GET_LOCAL_LEVEL: { + // Save DynamicVariableManager local level into register rd + int rd = bytecode[pc++]; + registers[rd] = new RuntimeScalar(DynamicVariableManager.getLocalLevel()); + break; + } + case Opcodes.POP_PACKAGE: // Scoped package block exit — restore handled by POP_LOCAL_LEVEL. break; diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 065258fa4..5980ef615 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -78,12 +78,11 @@ public static RuntimeScalar evalString(String perlCode, symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); } - // 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"; + // Use the runtime package at the eval call site. + // InterpreterState.currentPackage tracks runtime package changes from SET_PACKAGE/ + // PUSH_PACKAGE opcodes, so it correctly reflects the package active when eval is called. + // This matches Perl's behaviour: eval("__PACKAGE__") returns the package at call site. + String compilePackage = InterpreterState.currentPackage.get().toString(); symbolTable.setCurrentPackage(compilePackage, false); ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 8f2b41f9a..782e84771 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1440,6 +1440,9 @@ public String disassemble() { break; // GENERATED_DISASM_END + case Opcodes.GET_LOCAL_LEVEL: + sb.append("GET_LOCAL_LEVEL r").append(bytecode[pc++]).append("\n"); + break; case Opcodes.SET_PACKAGE: sb.append("SET_PACKAGE '").append(stringPool[bytecode[pc++]]).append("'\n"); break; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 46d83a45d..c40c2451f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -998,6 +998,11 @@ public class Opcodes { * Format: POP_LOCAL_LEVEL rs */ public static final short POP_LOCAL_LEVEL = 303; + /** Save current DynamicVariableManager local level into register rd. + * Used to bracket scoped package blocks so local pushes (PUSH_PACKAGE etc) are restored. + * Format: GET_LOCAL_LEVEL rd */ + public static final short GET_LOCAL_LEVEL = 341; + /** Superinstruction: foreach loop step for a global loop variable (e.g. $_). * Combines: hasNext check, next() into varReg, aliasGlobalVariable(name, varReg), conditional exit. * If iterator has next: varReg = next(), aliasGlobalVariable(name, varReg), fall through. diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index d9032a951..7d0339d84 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -879,6 +879,11 @@ public static BlockNode parseOptionalPackageBlock(Parser parser, IdentifierNode parser.isInClassBlock = wasInClassBlock; } + // Mark as scoped so BytecodeCompiler emits PUSH_PACKAGE (not SET_PACKAGE) + // and BlockNode.visit() brackets the block with GET_LOCAL_LEVEL/POP_LOCAL_LEVEL + // to restore the runtime package after the block exits. + packageNode.setAnnotation("isScoped", Boolean.TRUE); + // Insert packageNode as first statement in block block.elements.addFirst(packageNode); From bbfbc1f25f76dd265bed26ea6249d9ef9bab1ab5 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Wed, 25 Feb 2026 15:05:33 +0100 Subject: [PATCH 2/2] Fix interpreter eval: state assignment, local *GLOB, flip-flop operator - CompileAssignment: add 'state' alongside 'my' in all assignment LHS checks Fixes: op/state.t test 1 (CORE::state outside feature.pm scope) - Add LOCAL_GLOB opcode (342): localizes a typeglob via DynamicVariableManager BytecodeCompiler: handle 'local *GLOB' in the local operator switch BytecodeInterpreter + InterpretedCode: add handler + disassembler case Fixes: local *STDOUT (and similar) in eval STRING (op/yadayada.t 31-34) - Add FLIP_FLOP opcode (343): ScalarFlipFlopOperator.evaluate(id, left, right) CompileBinaryOperatorHelper: add '...' case, allocate per-call-site flipFlopId BytecodeInterpreter + InterpretedCode: add handler + disassembler case Fixes: '...' flip-flop operator in eval STRING (op/yadayada.t) --- .../backend/bytecode/BytecodeCompiler.java | 13 ++++++++++ .../backend/bytecode/BytecodeInterpreter.java | 24 +++++++++++++++++++ .../backend/bytecode/CompileAssignment.java | 6 ++--- .../bytecode/CompileBinaryOperatorHelper.java | 14 +++++++++++ .../backend/bytecode/InterpretedCode.java | 11 +++++++++ .../perlonjava/backend/bytecode/Opcodes.java | 10 ++++++++ 6 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 9f61e381d..50516e7c0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -2481,6 +2481,19 @@ void compileVariableDeclaration(OperatorNode node, String op) { lastResultReg = resultReg; return; } + // local *GLOB - localize a typeglob + if (node.operand instanceof OperatorNode sigilOp2 + && sigilOp2.operator.equals("*") + && sigilOp2.operand instanceof IdentifierNode idNode2) { + String globalName = NameNormalizer.normalizeVariableName(idNode2.name, getCurrentPackage()); + int nameIdx = addToStringPool(globalName); + int rd = allocateRegister(); + emit(Opcodes.LOCAL_GLOB); + emitReg(rd); + emit(nameIdx); + lastResultReg = rd; + return; + } throwCompilerException("Unsupported local operand: " + node.operand.getClass().getSimpleName()); } throwCompilerException("Unsupported variable declaration operator: " + op); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 22f6e963f..0ab7c2261 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -2083,6 +2083,30 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.FLIP_FLOP: { + // Flip-flop operator: rd = ScalarFlipFlopOperator.evaluate(id, left, right) + int rd = bytecode[pc++]; + int flipFlopId = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + registers[rd] = ScalarFlipFlopOperator.evaluate( + flipFlopId, + ((RuntimeBase) registers[rs1]).scalar(), + ((RuntimeBase) registers[rs2]).scalar()); + break; + } + + case Opcodes.LOCAL_GLOB: { + // Localize a typeglob: save state, return glob + int rd = bytecode[pc++]; + int nameIdx = bytecode[pc++]; + String name = code.stringPool[nameIdx]; + RuntimeGlob glob = GlobalVariable.getGlobalIO(name); + DynamicVariableManager.pushLocalVariable(glob); + registers[rd] = glob; + break; + } + case Opcodes.GET_LOCAL_LEVEL: { // Save DynamicVariableManager local level into register rd int rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index ea24bdb7f..14aa3f819 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -21,7 +21,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, // Check if LHS is a scalar assignment (my $x = ... or our $x = ...) if (node.left instanceof OperatorNode) { OperatorNode leftOp = (OperatorNode) node.left; - if ((leftOp.operator.equals("my") || leftOp.operator.equals("our")) && leftOp.operand instanceof OperatorNode) { + if ((leftOp.operator.equals("my") || leftOp.operator.equals("state") || leftOp.operator.equals("our")) && leftOp.operand instanceof OperatorNode) { OperatorNode sigilOp = (OperatorNode) leftOp.operand; if (sigilOp.operator.equals("$")) { // Scalar assignment: use SCALAR context for RHS @@ -41,8 +41,8 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, // Special case: my $x = value if (node.left instanceof OperatorNode) { OperatorNode leftOp = (OperatorNode) node.left; - if (leftOp.operator.equals("my")) { - // Extract variable name from "my" operand + if (leftOp.operator.equals("my") || leftOp.operator.equals("state")) { + // Extract variable name from "my"/"state" operand Node myOperand = leftOp.operand; // Handle my $x (where $x is OperatorNode("$", IdentifierNode("x"))) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java index 2bc247a80..f06559c5f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java @@ -419,6 +419,20 @@ public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, 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; + // this case handles scalar-context flip-flop. + int flipFlopId = org.perlonjava.runtime.operators.ScalarFlipFlopOperator.currentId++; + org.perlonjava.runtime.operators.ScalarFlipFlopOperator op = + new org.perlonjava.runtime.operators.ScalarFlipFlopOperator(operator.equals("...")); + org.perlonjava.runtime.operators.ScalarFlipFlopOperator.flipFlops.putIfAbsent(flipFlopId, op); + bytecodeCompiler.emit(Opcodes.FLIP_FLOP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emit(flipFlopId); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } default -> bytecodeCompiler.throwCompilerException("Unsupported operator: " + operator, tokenIndex); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 782e84771..bab2e0d5d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1440,6 +1440,17 @@ public String disassemble() { break; // GENERATED_DISASM_END + case Opcodes.FLIP_FLOP: { + int ffRd = bytecode[pc++]; + int ffId = bytecode[pc++]; + int ffRs1 = bytecode[pc++]; + int ffRs2 = bytecode[pc++]; + sb.append("FLIP_FLOP r").append(ffRd).append(" = flipFlop(").append(ffId).append(", r").append(ffRs1).append(", r").append(ffRs2).append(")\n"); + break; + } + case Opcodes.LOCAL_GLOB: + sb.append("LOCAL_GLOB r").append(bytecode[pc++]).append(" = pushLocalVariable(glob '").append(stringPool[bytecode[pc++]]).append("')\n"); + break; case Opcodes.GET_LOCAL_LEVEL: sb.append("GET_LOCAL_LEVEL r").append(bytecode[pc++]).append("\n"); break; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index c40c2451f..2937aace0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -639,6 +639,16 @@ public class Opcodes { * Format: STORE_GLOB globReg valueReg */ public static final short STORE_GLOB = 164; + /** Localize a typeglob: rd = DynamicVariableManager.pushLocalVariable(LOAD_GLOB(nameIdx)) + * Saves current glob state and returns the glob for potential assignment. + * Format: LOCAL_GLOB rd nameIdx */ + public static final short LOCAL_GLOB = 342; + + /** Flip-flop operator: rd = ScalarFlipFlopOperator.evaluate(flipFlopId, rs1, rs2) + * flipFlopId is a unique per-call-site int constant. + * Format: FLIP_FLOP rd flipFlopId rs1 rs2 isExclusive */ + public static final short FLIP_FLOP = 343; + /** Open file: rd = IOOperator.open(ctx, args...) * Format: OPEN rd ctx argsReg */ public static final short OPEN = 165;