From ec1a2736362ae0391ed4b8cadf473039a0da3ced Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 14:08:36 +0100 Subject: [PATCH 1/2] Interpreter: fix strict refs/vars in eval STRING and glob assign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Strict refs for ${block} scalar deref (BytecodeCompiler + CompileAssignment): - Add DEREF_SCALAR_STRICT (335) and DEREF_SCALAR_NONSTRICT (336) opcodes - BytecodeCompiler: emit DEREF_SCALAR_STRICT when strict refs enabled, DEREF_SCALAR_NONSTRICT (with pkg) when disabled — matches JVM path in EmitVariable.java case "$" - CompileAssignment: same for ${block}=value and $$var=value assignment - SlowOpcodeHandler: implement via scalarDeref()/scalarDerefNonStrict(pkg) 2. Strict vars for $identifier global access (BytecodeCompiler): - Add shouldBlockGlobalUnderStrictVars() check in compileVariableReference() for the global variable load path — was previously unchecked 3. Typeglob symbolic assignment *{"name"} = sub (CompileAssignment): - Add LOAD_GLOB_DYNAMIC (334) opcode for runtime glob-by-name loading - Handle BlockNode operand for * assignment, matching no-strict-refs symref - Fixes op/require_gh20577.t glob assign in eval STRING 4. DEREF_GLOB (333) + LOAD_GLOB_DYNAMIC disassembler support (InterpretedCode) Fixes comp/use.t: reduces interpreter-specific failures from -6 to -1 (test 23 is a pre-existing issue in both JVM and interpreter paths). Fixes op/require_gh20577.t: glob assign works; @INC hook dispatch remains. op/signatures.t baseline: 597/597. --- .../backend/bytecode/BytecodeCompiler.java | 33 ++++++--- .../backend/bytecode/BytecodeInterpreter.java | 14 +++- .../backend/bytecode/CompileAssignment.java | 68 +++++++++++++++---- .../perlonjava/backend/bytecode/Opcodes.java | 17 +++++ .../backend/bytecode/SlowOpcodeHandler.java | 58 ++++++++++++++++ 5 files changed, 167 insertions(+), 23 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index b8ce5c3fc..3533fd10f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -366,6 +366,12 @@ private boolean isNonAsciiLengthOneScalarAllowedUnderNoUtf8(String sigil, String * @param varName The variable name with sigil (e.g., "$A", "@array") * @return true if access should be blocked under strict vars */ + /** Returns true if strict refs is currently enabled in the symbol table. */ + boolean isStrictRefsEnabled() { + return emitterContext != null && emitterContext.symbolTable != null + && emitterContext.symbolTable.isStrictOptionEnabled(Strict.HINT_STRICT_REFS); + } + boolean shouldBlockGlobalUnderStrictVars(String varName) { // Only check if strict vars is enabled if (emitterContext == null || emitterContext.symbolTable == null) { @@ -2484,7 +2490,11 @@ void compileVariableReference(OperatorNode node, String op) { // Lexical variable - use existing register lastResultReg = getVariableRegister(varName); } else { - // Global variable - load it + // Global variable - check strict vars then load + if (shouldBlockGlobalUnderStrictVars(varName)) { + throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); + } + // Use NameNormalizer to properly handle special variables (like $&) // which must always be in the "main" package String globalVarName = varName.substring(1); // Remove $ sigil first @@ -2507,16 +2517,23 @@ void compileVariableReference(OperatorNode node, String op) { // Execute the block to get a variable name string, then load that variable BlockNode block = (BlockNode) node.operand; - // Compile the block + // Check strict refs at compile time — mirrors JVM path in EmitVariable.java block.accept(this); int blockResultReg = lastResultReg; - - // Load via symbolic reference int rd = allocateRegister(); - emitWithToken(Opcodes.LOAD_SYMBOLIC_SCALAR, node.getIndex()); - emitReg(rd); - emitReg(blockResultReg); - + if (isStrictRefsEnabled()) { + // strict refs: scalarDeref() — throws for non-refs + emitWithToken(Opcodes.DEREF_SCALAR_STRICT, node.getIndex()); + emitReg(rd); + emitReg(blockResultReg); + } else { + // no strict refs: scalarDerefNonStrict(pkg) — allows symrefs + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_SCALAR_NONSTRICT, node.getIndex()); + emitReg(rd); + emitReg(blockResultReg); + emit(pkgIdx); + } lastResultReg = rd; } else if (node.operand instanceof OperatorNode) { // Operator dereference: $$x, $${expr}, etc. diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 485392a19..21bc64746 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1769,12 +1769,15 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = executeSystemOps(opcode, bytecode, pc, registers); break; - // Group 9: Special I/O (151-154) and DEREF_GLOB + // Group 9: Special I/O (151-154), glob ops, strict deref case Opcodes.EVAL_STRING: case Opcodes.SELECT_OP: case Opcodes.LOAD_GLOB: case Opcodes.SLEEP_OP: case Opcodes.DEREF_GLOB: + case Opcodes.LOAD_GLOB_DYNAMIC: + case Opcodes.DEREF_SCALAR_STRICT: + case Opcodes.DEREF_SCALAR_NONSTRICT: pc = executeSpecialIO(opcode, bytecode, pc, registers, code); break; @@ -3087,7 +3090,8 @@ private static int executeSystemOps(int opcode, int[] bytecode, int pc, /** * Execute special I/O operations (opcodes 151-154). - * Handles: EVAL_STRING, SELECT_OP, LOAD_GLOB, SLEEP_OP, DEREF_GLOB + * Handles: EVAL_STRING, SELECT_OP, LOAD_GLOB, SLEEP_OP, DEREF_GLOB, LOAD_GLOB_DYNAMIC, + * DEREF_SCALAR_STRICT, DEREF_SCALAR_NONSTRICT */ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, RuntimeBase[] registers, InterpretedCode code) { @@ -3102,6 +3106,12 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, return SlowOpcodeHandler.executeSleep(bytecode, pc, registers); case Opcodes.DEREF_GLOB: return SlowOpcodeHandler.executeDerefGlob(bytecode, pc, registers, code); + case Opcodes.LOAD_GLOB_DYNAMIC: + return SlowOpcodeHandler.executeLoadGlobDynamic(bytecode, pc, registers, code); + case Opcodes.DEREF_SCALAR_STRICT: + return SlowOpcodeHandler.executeDerefScalarStrict(bytecode, pc, registers); + case Opcodes.DEREF_SCALAR_NONSTRICT: + return SlowOpcodeHandler.executeDerefScalarNonStrict(bytecode, pc, registers, code); default: throw new RuntimeException("Unknown special I/O opcode: " + opcode); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 3ee669681..372da7562 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -641,36 +641,59 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, // We need to evaluate the LHS FIRST to get the variable name, // then evaluate the RHS, to ensure the RHS doesn't clobber the LHS registers if (node.left instanceof OperatorNode leftOp && leftOp.operator.equals("$")) { + boolean strictRefsEnabled = bytecodeCompiler.isStrictRefsEnabled(); + if (leftOp.operand instanceof BlockNode) { - // ${block} = value + // ${block} = value — mirrors JVM EmitVariable.java case "$" BlockNode block = (BlockNode) leftOp.operand; block.accept(bytecodeCompiler); int nameReg = bytecodeCompiler.lastResultReg; - // Now compile the RHS + // Deref to get lvalue target (strict or non-strict) + int derefReg = bytecodeCompiler.allocateRegister(); + if (strictRefsEnabled) { + bytecodeCompiler.emitWithToken(Opcodes.DEREF_SCALAR_STRICT, node.getIndex()); + bytecodeCompiler.emitReg(derefReg); + bytecodeCompiler.emitReg(nameReg); + } else { + int pkgIdx = bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage()); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_SCALAR_NONSTRICT, node.getIndex()); + bytecodeCompiler.emitReg(derefReg); + bytecodeCompiler.emitReg(nameReg); + bytecodeCompiler.emit(pkgIdx); + } + + // Now compile the RHS and assign node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - - // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference - bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); - bytecodeCompiler.emitReg(nameReg); + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(derefReg); bytecodeCompiler.emitReg(valueReg); bytecodeCompiler.lastResultReg = valueReg; return; } else if (leftOp.operand instanceof OperatorNode) { - // $$var = value (scalar dereference assignment) - // Evaluate the inner expression to get the variable name + // $$var = value — mirrors JVM EmitVariable.java case "$" leftOp.operand.accept(bytecodeCompiler); int nameReg = bytecodeCompiler.lastResultReg; - // Now compile the RHS + int derefReg = bytecodeCompiler.allocateRegister(); + if (strictRefsEnabled) { + bytecodeCompiler.emitWithToken(Opcodes.DEREF_SCALAR_STRICT, node.getIndex()); + bytecodeCompiler.emitReg(derefReg); + bytecodeCompiler.emitReg(nameReg); + } else { + int pkgIdx = bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage()); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_SCALAR_NONSTRICT, node.getIndex()); + bytecodeCompiler.emitReg(derefReg); + bytecodeCompiler.emitReg(nameReg); + bytecodeCompiler.emit(pkgIdx); + } + node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - - // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference - bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); - bytecodeCompiler.emitReg(nameReg); + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(derefReg); bytecodeCompiler.emitReg(valueReg); bytecodeCompiler.lastResultReg = valueReg; @@ -896,6 +919,25 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(globReg); bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.lastResultReg = globReg; + } else if (leftOp.operator.equals("*") && leftOp.operand instanceof BlockNode) { + // Symbolic typeglob assignment: *{"name"} = value (no strict refs) + // Evaluate the block to get the glob name as a scalar, then load glob by name + leftOp.operand.accept(bytecodeCompiler); + int nameScalarReg = bytecodeCompiler.lastResultReg; + + int globReg = bytecodeCompiler.allocateRegister(); + int pkgIdx = bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage()); + bytecodeCompiler.emitWithToken(Opcodes.LOAD_GLOB_DYNAMIC, node.getIndex()); + bytecodeCompiler.emitReg(globReg); + bytecodeCompiler.emitReg(nameScalarReg); + bytecodeCompiler.emit(pkgIdx); + + // Store value to glob + bytecodeCompiler.emit(Opcodes.STORE_GLOB); + bytecodeCompiler.emitReg(globReg); + bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.lastResultReg = globReg; } else if (leftOp.operator.equals("pos")) { // pos($var) = value - lvalue assignment to regex position diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index d4741b087..2ba27a76e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1076,5 +1076,22 @@ public class Opcodes { * Format: DEREF_GLOB rd rs nameIdx(currentPackage) */ public static final short DEREF_GLOB = 333; + /** Load glob by runtime name (symbolic ref): rd = GlobalVariable.getGlobalIO(normalize(nameReg, pkg)) + * Used for *{"name"} = value typeglob assignment with dynamic name + * Format: LOAD_GLOB_DYNAMIC rd nameReg pkgIdx */ + public static final short LOAD_GLOB_DYNAMIC = 334; + + /** Scalar dereference (strict refs): rd = rs.scalarDeref() + * Throws "Can't use string as a SCALAR ref while strict refs in use" for non-refs. + * Matches JVM path: scalarDeref() — used when strict refs is enabled. + * Format: DEREF_SCALAR_STRICT rd rs */ + public static final short DEREF_SCALAR_STRICT = 335; + + /** Scalar dereference (no strict refs): rd = rs.scalarDerefNonStrict(pkg) + * Allows symbolic references (string name -> global variable lookup). + * Matches JVM path: scalarDerefNonStrict(pkg) — used when strict refs is disabled. + * Format: DEREF_SCALAR_NONSTRICT rd rs pkgIdx */ + public static final short DEREF_SCALAR_NONSTRICT = 336; + private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index bea587b70..09eb9aac0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -313,6 +313,64 @@ public static int executeLoadGlob( return pc; } + /** + * LOAD_GLOB_DYNAMIC: rd = GlobalVariable.getGlobalIO(normalize(nameReg, pkg)) + * Format: LOAD_GLOB_DYNAMIC rd nameReg pkgIdx + * Effect: Loads a glob by runtime name — used for *{"name"} = value symbolic assignment + */ + public static int executeLoadGlobDynamic( + int[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + + int rd = bytecode[pc++]; + int nameReg = bytecode[pc++]; + int pkgIdx = bytecode[pc++]; + + String pkg = code.stringPool[pkgIdx]; + String name = ((RuntimeScalar) registers[nameReg]).toString(); + String globalName = NameNormalizer.normalizeVariableName(name, pkg); + + registers[rd] = GlobalVariable.getGlobalIO(globalName); + return pc; + } + + /** + * DEREF_SCALAR_STRICT: rd = rs.scalarDeref() + * Format: DEREF_SCALAR_STRICT rd rs + * Matches JVM path: scalarDeref() — throws for non-refs under strict refs. + */ + public static int executeDerefScalarStrict( + int[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + registers[rd] = ((RuntimeScalar) registers[rs]).scalarDeref(); + return pc; + } + + /** + * DEREF_SCALAR_NONSTRICT: rd = rs.scalarDerefNonStrict(pkg) + * Format: DEREF_SCALAR_NONSTRICT rd rs pkgIdx + * Matches JVM path: scalarDerefNonStrict(pkg) — allows symbolic refs. + */ + public static int executeDerefScalarNonStrict( + int[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + int pkgIdx = bytecode[pc++]; + String pkg = code.stringPool[pkgIdx]; + registers[rd] = ((RuntimeScalar) registers[rs]).scalarDerefNonStrict(pkg); + return pc; + } + /** * DEREF_GLOB: rd = rs.globDerefNonStrict(currentPackage) * Format: DEREF_GLOB rd rs nameIdx(currentPackage) From 9f1caeb5f9350ca414f77a7bd0f92c3cbe22cdf6 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 14:20:19 +0100 Subject: [PATCH 2/2] Interpreter: fix open my $fh lexical filehandle propagation IOOperator.open() calls fileHandle.set(glob) on the first argument, but RuntimeArray.push() copies the scalar via new RuntimeScalar(this), so the mutation was lost on the original lexical register. Fix in CompileOperator.java: after emitting OPEN, retrieve element 0 from the args array (which IOOperator.open() mutated) and SET_SCALAR it back into the fh lexical register. Fixes op/require_gh20577.t: now 7/9 (matching JVM baseline). Tests 6/7 remain pre-existing failures in both JVM and interpreter. op/signatures.t baseline: 597/597. --- .../backend/bytecode/CompileOperator.java | 30 ++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 21a5ea90d..c419ee711 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1669,15 +1669,25 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.throwCompilerException("open requires arguments"); } - // Compile all arguments into a list + // Compile all arguments into a list. + // Track the first-arg (filehandle) register so we can write the GLOB back + // after OPEN — IOOperator.open() does fileHandle.set() on a copy in the array, + // so we must propagate the result back to the original lexical register. int argsReg = bytecodeCompiler.allocateRegister(); bytecodeCompiler.emit(Opcodes.NEW_ARRAY); bytecodeCompiler.emitReg(argsReg); + int fhReg = -1; + boolean first = true; for (Node arg : argsList.elements) { arg.accept(bytecodeCompiler); int elemReg = bytecodeCompiler.lastResultReg; + if (first) { + fhReg = elemReg; // remember the filehandle lvalue register + first = false; + } + bytecodeCompiler.emit(Opcodes.ARRAY_PUSH); bytecodeCompiler.emitReg(argsReg); bytecodeCompiler.emitReg(elemReg); @@ -1690,6 +1700,24 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); bytecodeCompiler.emitReg(argsReg); + // Write the (now-modified) first element of args back to the fh register. + // IOOperator.open() calls fileHandle.set(glob) on a copy inside the array, + // so we must retrieve element 0 and store it back into the lexical $fh. + if (fhReg >= 0) { + int idx0Reg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(idx0Reg); + bytecodeCompiler.emit(0); // index 0 + int gotReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_GET); + bytecodeCompiler.emitReg(gotReg); + bytecodeCompiler.emitReg(argsReg); + bytecodeCompiler.emitReg(idx0Reg); + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(fhReg); + bytecodeCompiler.emitReg(gotReg); + } + bytecodeCompiler.lastResultReg = rd; } else if (op.equals("matchRegex")) { // m/pattern/flags - create a regex and optionally match against a string