From 6de5ca453574ff4e0849b11deb21e26399c02c31 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Tue, 24 Feb 2026 09:37:50 +0100 Subject: [PATCH] Interpreter: fix strict refs/vars enforcement in eval STRING MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add LOAD_SYMBOLIC_SCALAR_NONSTRICT (338) and STORE_SYMBOLIC_SCALAR_NONSTRICT (339) opcodes so BytecodeCompiler emits the strict variant when strict refs is active and the non-strict variant otherwise, matching JVM compiler behaviour. - Fix BytecodeCompiler.enterScope/exitScope to call emitterContext.symbolTable .enterScope()/.exitScope() so pragma flags (strict/feature/warning) are properly scoped to blocks — prevents use 5.012 inside {…} from leaking strict into the surrounding code. - Add strict vars check in compileVariableReference for the undeclared-global path so Global-symbol errors are thrown at interpreter compile time. Results: comp/use.t interpreter: 40 → 44 passing op/die.t interpreter: 19 → 25 passing (only pre-existing TODO test 26 remains) --- .../backend/bytecode/BytecodeCompiler.java | 29 +++++++- .../backend/bytecode/BytecodeInterpreter.java | 69 +++++++++++-------- .../backend/bytecode/CompileAssignment.java | 14 ++-- .../backend/bytecode/InterpretedCode.java | 10 +++ .../perlonjava/backend/bytecode/Opcodes.java | 17 +++++ 5 files changed, 106 insertions(+), 33 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 5775c6a8c..c26d44f07 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -230,12 +230,23 @@ private void enterScope() { savedBaseRegister.push(baseRegisterForStatement); // Update base to protect all registers allocated before this scope baseRegisterForStatement = nextRegister; + // Mirror the JVM compiler: push a new pragma frame so that strict/feature/warning + // changes inside this block (e.g. `use strict`, `use 5.012`) are scoped to the block + // and do not leak into the surrounding code after the block exits. + if (emitterContext != null && emitterContext.symbolTable != null) { + savedSymbolTableScopes.push(emitterContext.symbolTable.enterScope()); + } else { + savedSymbolTableScopes.push(-1); // sentinel so stacks stay in sync + } } /** * Helper: Exit the current lexical scope. * Restores register allocation state to what it was before entering the scope. */ + // Saved symbol-table scope indices, parallel to savedNextRegister / savedBaseRegister. + private final Stack savedSymbolTableScopes = new Stack<>(); + private void exitScope() { if (variableScopes.size() > 1) { variableScopes.pop(); @@ -247,6 +258,11 @@ private void exitScope() { baseRegisterForStatement = savedBaseRegister.pop(); } } + // Restore the pragma frame (strict/feature/warning flags) for the scope we just exited. + if (emitterContext != null && emitterContext.symbolTable != null + && !savedSymbolTableScopes.isEmpty()) { + emitterContext.symbolTable.exitScope(savedSymbolTableScopes.pop()); + } } /** @@ -2530,6 +2546,10 @@ void compileVariableReference(OperatorNode node, String op) { lastResultReg = getVariableRegister(varName); } else { // Global variable - load it + // Check strict vars before loading an undeclared global + 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 @@ -2556,9 +2576,14 @@ void compileVariableReference(OperatorNode node, String op) { block.accept(this); int blockResultReg = lastResultReg; - // Load via symbolic reference + // Load via symbolic reference — use strict vs non-strict opcode. + // LOAD_SYMBOLIC_SCALAR throws "strict refs" for non-reference strings. + // LOAD_SYMBOLIC_SCALAR_NONSTRICT allows symbolic variable lookup. int rd = allocateRegister(); - emitWithToken(Opcodes.LOAD_SYMBOLIC_SCALAR, node.getIndex()); + short symOp = isStrictRefsEnabled() + ? Opcodes.LOAD_SYMBOLIC_SCALAR + : Opcodes.LOAD_SYMBOLIC_SCALAR_NONSTRICT; + emitWithToken(symOp, node.getIndex()); emitReg(rd); emitReg(blockResultReg); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index e960c83b8..d38b51a9d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1858,57 +1858,72 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; case Opcodes.STORE_SYMBOLIC_SCALAR: { - // Store via symbolic reference: GlobalVariable.getGlobalVariable(nameReg.toString()).set(valueReg) + // Strict symbolic scalar store: throws for string refs, allows REFERENCE. // Format: STORE_SYMBOLIC_SCALAR nameReg valueReg int nameReg = bytecode[pc++]; int valueReg = bytecode[pc++]; - // Get the variable name from the name register RuntimeScalar nameScalar = (RuntimeScalar) registers[nameReg]; - String varName = nameScalar.toString(); + // scalarDeref() throws "strict refs" for STRING and acts as deref for REFERENCE + RuntimeScalar targetVar = nameScalar.scalarDeref(); + targetVar.set(registers[valueReg]); + break; + } - // Normalize the variable name to include package prefix if needed - // This is important for ${label:var} cases where "colon" becomes "main::colon" - String normalizedName = NameNormalizer.normalizeVariableName( - varName, - "main" // Use main package as default for symbolic references - ); + case Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT: { + // Non-strict symbolic scalar store: allows string-keyed global variable store. + // Format: STORE_SYMBOLIC_SCALAR_NONSTRICT nameReg valueReg + int nameReg = bytecode[pc++]; + int valueReg = bytecode[pc++]; - // Get the global variable and set its value - RuntimeScalar globalVar = GlobalVariable.getGlobalVariable(normalizedName); - RuntimeBase value = registers[valueReg]; - globalVar.set(value); + RuntimeScalar nameScalar = (RuntimeScalar) registers[nameReg]; + + if (nameScalar.type == RuntimeScalarType.REFERENCE) { + // ${\ ref} = value — dereference then assign + nameScalar.scalarDeref().set(registers[valueReg]); + } else { + // ${"varname"} = value — symbolic reference store + String normalizedName = NameNormalizer.normalizeVariableName( + nameScalar.toString(), + code.compilePackage + ); + GlobalVariable.getGlobalVariable(normalizedName).set(registers[valueReg]); + } break; } case Opcodes.LOAD_SYMBOLIC_SCALAR: { - // Load via symbolic reference: rd = GlobalVariable.getGlobalVariable(nameReg.toString()).get() - // OR dereference if nameReg contains a scalar reference + // Strict symbolic scalar load: rd = ${\ref} only. + // Throws "strict refs" for strings, matching Perl strict semantics. // Format: LOAD_SYMBOLIC_SCALAR rd nameReg int rd = bytecode[pc++]; int nameReg = bytecode[pc++]; + // scalarDeref() handles both REFERENCE (dereference) and STRING + // (throws "Can't use string ... as a SCALAR ref while strict refs in use") + registers[rd] = ((RuntimeScalar) registers[nameReg]).scalarDeref(); + break; + } + + case Opcodes.LOAD_SYMBOLIC_SCALAR_NONSTRICT: { + // Non-strict symbolic scalar load: rd = ${"varname"} or ${\ref}. + // Allows string-keyed global variable lookup (no strict refs). + // Format: LOAD_SYMBOLIC_SCALAR_NONSTRICT rd nameReg + int rd = bytecode[pc++]; + int nameReg = bytecode[pc++]; - // Get the value from the name register RuntimeScalar nameScalar = (RuntimeScalar) registers[nameReg]; - // Check if it's a scalar reference - if so, dereference it if (nameScalar.type == RuntimeScalarType.REFERENCE) { - // This is ${\ref} - dereference the reference + // ${\ref} — dereference the scalar reference registers[rd] = nameScalar.scalarDeref(); } else { - // This is ${"varname"} - symbolic reference to variable + // ${"varname"} — symbolic reference: look up global by name String varName = nameScalar.toString(); - - // Normalize the variable name to include package prefix if needed - // This is important for ${label:var} cases where "colon" becomes "main::colon" String normalizedName = NameNormalizer.normalizeVariableName( varName, - "main" // Use main package as default for symbolic references + code.compilePackage // Use compile-time package for name resolution ); - - // Get the global variable and load its value - RuntimeScalar globalVar = GlobalVariable.getGlobalVariable(normalizedName); - registers[rd] = globalVar; + registers[rd] = GlobalVariable.getGlobalVariable(normalizedName); } break; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 748103d65..2657fc44e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -651,8 +651,11 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference - bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); + // Use strict vs non-strict store opcode depending on current strict refs + short storeOp = bytecodeCompiler.isStrictRefsEnabled() + ? Opcodes.STORE_SYMBOLIC_SCALAR + : Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT; + bytecodeCompiler.emit(storeOp); bytecodeCompiler.emitReg(nameReg); bytecodeCompiler.emitReg(valueReg); @@ -668,8 +671,11 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, node.right.accept(bytecodeCompiler); int valueReg = bytecodeCompiler.lastResultReg; - // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference - bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); + // Use strict vs non-strict store opcode depending on current strict refs + short storeOp2 = bytecodeCompiler.isStrictRefsEnabled() + ? Opcodes.STORE_SYMBOLIC_SCALAR + : Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT; + bytecodeCompiler.emit(storeOp2); bytecodeCompiler.emitReg(nameReg); bytecodeCompiler.emitReg(valueReg); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index a6d927b5d..7e7177037 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -894,6 +894,16 @@ public String disassemble() { .append("} pkg=").append(stringPool[derefNsPkgIdx]).append("\n"); break; } + case Opcodes.LOAD_SYMBOLIC_SCALAR_NONSTRICT: + rd = bytecode[pc++]; + rs = bytecode[pc++]; + sb.append("LOAD_SYMBOLIC_SCALAR_NONSTRICT r").append(rd).append(" = ${r").append(rs).append("}\n"); + break; + case Opcodes.STORE_SYMBOLIC_SCALAR_NONSTRICT: + rd = bytecode[pc++]; + rs = bytecode[pc++]; + sb.append("STORE_SYMBOLIC_SCALAR_NONSTRICT ${r").append(rd).append("} = r").append(rs).append("\n"); + break; case Opcodes.GET_TYPE: rd = bytecode[pc++]; rs = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 6fccae878..c5b359edd 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1091,5 +1091,22 @@ public class Opcodes { * Format: DEREF_NONSTRICT rd scalarReg packageIdx */ public static final short DEREF_NONSTRICT = 337; + /** Load via symbolic reference (strict refs): throws if nameReg is a string. + * Allows REFERENCE type (${\ ref}). Same format as LOAD_SYMBOLIC_SCALAR. + * Format: LOAD_SYMBOLIC_SCALAR rd nameReg (strict — already the default opcode 232) */ + // Note: LOAD_SYMBOLIC_SCALAR (232) is the strict variant. + + /** Load via symbolic reference (no strict refs): allows string-keyed global lookup. + * Format: LOAD_SYMBOLIC_SCALAR_NONSTRICT rd nameReg */ + public static final short LOAD_SYMBOLIC_SCALAR_NONSTRICT = 338; + + /** Store via symbolic reference (strict refs): throws if nameReg is a string. + * Format: STORE_SYMBOLIC_SCALAR rd nameReg */ + // Note: STORE_SYMBOLIC_SCALAR (231) is the strict variant. + + /** Store via symbolic reference (no strict refs): allows string-keyed global store. + * Format: STORE_SYMBOLIC_SCALAR_NONSTRICT nameReg valueReg */ + public static final short STORE_SYMBOLIC_SCALAR_NONSTRICT = 339; + private Opcodes() {} // Utility class - no instantiation }