From 871cef830fa6bda1ba8e679d857e96b60095c265 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 21:05:19 +0100 Subject: [PATCH 1/5] Fix interpreter eval STRING: strict refs, glob ops, call-site pragma inheritance Multiple fixes for interpreter mode (JPERL_EVAL_USE_INTERPRETER=1): 1. Call-site strict/feature flags for eval STRING (CompileOperator, SlowOpcodeHandler, EvalStringHandler): Embed strict/feature flags at each eval call site in the bytecode. Previously EvalStringHandler used the end-of-compilation snapshot from InterpretedCode, which missed lexically-scoped pragma changes (e.g. 'no strict refs' inside a block). Now the exact flags at the eval call site are passed to EvalStringHandler. 2. Strict-refs-aware deref opcodes (BytecodeCompiler, Opcodes, SlowOpcodeHandler, BytecodeInterpreter): Added DEREF_HASH_NONSTRICT and DEREF_ARRAY_NONSTRICT opcodes. BytecodeCompiler now emits these when 'no strict refs' is in effect at compile time, matching the JVM path which calls hashDerefNonStrict/arrayDerefNonStrict. Handles: %$ref, %{block}, %{'name'}, @$ref, @{block}, @{'name'}. 3. Dynamic glob load/assign (BytecodeCompiler, CompileAssignment, Opcodes, SlowOpcodeHandler, BytecodeInterpreter): Added LOAD_SYMBOLIC_GLOB opcode for *{expr} and *{'name'} glob access. Added DEREF_GLOB opcode for ** postfix glob dereference. CompileAssignment now handles *(BlockNode), *(StringNode), *(OperatorNode) for dynamic glob assignment: *{'name'} = value, *{expr} = value. 4. @{'name'} and %{'name'} symref support (BytecodeCompiler): Added StringNode cases for @ and % dereference operators. op/postfixderef.t: 85/128 -> 81/128 interpreter (was 78/128, improved by 3) op/require_gh20577.t: 0/0 -> 4/9 interpreter (was 0/0, improved by 4) --- .../backend/bytecode/BytecodeCompiler.java | 154 +++++++++++++++--- .../backend/bytecode/BytecodeInterpreter.java | 12 ++ .../backend/bytecode/CompileAssignment.java | 21 +++ .../backend/bytecode/CompileOperator.java | 8 +- .../backend/bytecode/EvalStringHandler.java | 18 +- .../perlonjava/backend/bytecode/Opcodes.java | 16 ++ .../backend/bytecode/SlowOpcodeHandler.java | 89 +++++++++- 7 files changed, 286 insertions(+), 32 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index c14edf705..768006121 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -369,6 +369,30 @@ 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 at compile time. */ + boolean isStrictRefsEnabled() { + if (emitterContext == null || emitterContext.symbolTable == null) { + return false; + } + return emitterContext.symbolTable.isStrictOptionEnabled(Strict.HINT_STRICT_REFS); + } + + /** Returns the current strict options bitmask at this point in compilation. */ + int getCurrentStrictOptions() { + if (emitterContext == null || emitterContext.symbolTable == null) { + return 0; + } + return emitterContext.symbolTable.strictOptionsStack.peek(); + } + + /** Returns the current feature flags bitmask at this point in compilation. */ + int getCurrentFeatureFlags() { + if (emitterContext == null || emitterContext.symbolTable == null) { + return 0; + } + return emitterContext.symbolTable.featureFlagsStack.peek(); + } + boolean shouldBlockGlobalUnderStrictVars(String varName) { // Only check if strict vars is enabled if (emitterContext == null || emitterContext.symbolTable == null) { @@ -2601,31 +2625,60 @@ void compileVariableReference(OperatorNode node, String op) { operandOp.accept(this); int refReg = lastResultReg; - // Dereference to get the array - // The reference should contain a RuntimeArray - // For @$scalar, we need to dereference it int rd = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(rd); - emitReg(refReg); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(rd); + emitReg(refReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); + emitReg(rd); + emitReg(refReg); + emit(pkgIdx); + } lastResultReg = rd; - // Note: We don't check scalar context here because dereferencing - // should return the array itself. The slice or other operation - // will handle scalar context conversion if needed. } else if (node.operand instanceof BlockNode) { // @{ block } - evaluate block and dereference the result - // The block should return an arrayref BlockNode blockNode = (BlockNode) node.operand; blockNode.accept(this); int refReg = lastResultReg; - // Dereference to get the array int rd = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(rd); - emitReg(refReg); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(rd); + emitReg(refReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); + emitReg(rd); + emitReg(refReg); + emit(pkgIdx); + } + + lastResultReg = rd; + } else if (node.operand instanceof StringNode strNode) { + // @{'name'} — symbolic array reference + int nameReg = allocateRegister(); + int strIdx = addToStringPool(strNode.value); + emit(Opcodes.LOAD_STRING); + emitReg(nameReg); + emit(strIdx); + int rd = allocateRegister(); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(rd); + emitReg(nameReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_ARRAY_NONSTRICT, node.getIndex()); + emitReg(rd); + emitReg(nameReg); + emit(pkgIdx); + } lastResultReg = rd; } else { throwCompilerException("Unsupported @ operand: " + node.operand.getClass().getSimpleName()); @@ -2666,9 +2719,17 @@ void compileVariableReference(OperatorNode node, String op) { refOp.accept(this); int scalarReg = lastResultReg; int hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + emitReg(hashReg); + emitReg(scalarReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); + emitReg(hashReg); + emitReg(scalarReg); + emit(pkgIdx); + } if (currentCallContext == RuntimeContextType.SCALAR) { int rd = allocateRegister(); emit(Opcodes.ARRAY_SIZE); @@ -2683,15 +2744,44 @@ void compileVariableReference(OperatorNode node, String op) { blockNode.accept(this); int scalarReg = lastResultReg; int hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + emitReg(hashReg); + emitReg(scalarReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); + emitReg(hashReg); + emitReg(scalarReg); + emit(pkgIdx); + } + lastResultReg = hashReg; + } else if (node.operand instanceof StringNode strNode) { + // %{'name'} — symbolic hash reference + int nameReg = allocateRegister(); + int strIdx = addToStringPool(strNode.value); + emit(Opcodes.LOAD_STRING); + emitReg(nameReg); + emit(strIdx); + + int hashReg = allocateRegister(); + if (isStrictRefsEnabled()) { + emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + emitReg(hashReg); + emitReg(nameReg); + } else { + int pkgIdx = addToStringPool(getCurrentPackage()); + emitWithToken(Opcodes.DEREF_HASH_NONSTRICT, node.getIndex()); + emitReg(hashReg); + emitReg(nameReg); + emit(pkgIdx); + } lastResultReg = hashReg; } else { throwCompilerException("Unsupported % operand: " + node.operand.getClass().getSimpleName()); } } else if (op.equals("*")) { - // Glob variable dereference: *x + // Glob variable dereference: *x or *{expr} if (node.operand instanceof IdentifierNode) { IdentifierNode idNode = (IdentifierNode) node.operand; String varName = idNode.name; @@ -2710,6 +2800,28 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emit(nameIdx); + lastResultReg = rd; + } else if (node.operand instanceof BlockNode || node.operand instanceof StringNode) { + // *{expr} or *{'name'} — dynamic glob via symbolic reference + node.operand.accept(this); + int nameReg = lastResultReg; + + int rd = allocateRegister(); + emitWithToken(Opcodes.LOAD_SYMBOLIC_GLOB, node.getIndex()); + emitReg(rd); + emitReg(nameReg); + + lastResultReg = rd; + } else if (node.operand instanceof OperatorNode) { + // *$ref or **postfix — dereference scalar as glob + node.operand.accept(this); + int scalarReg = lastResultReg; + + int rd = allocateRegister(); + emitWithToken(Opcodes.DEREF_GLOB, node.getIndex()); + emitReg(rd); + emitReg(scalarReg); + lastResultReg = rd; } else { throwCompilerException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index b14f13fe8..8e7e135cf 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1774,6 +1774,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.SELECT_OP: case Opcodes.LOAD_GLOB: case Opcodes.SLEEP_OP: + case Opcodes.LOAD_SYMBOLIC_GLOB: + case Opcodes.DEREF_GLOB: + case Opcodes.DEREF_HASH_NONSTRICT: + case Opcodes.DEREF_ARRAY_NONSTRICT: pc = executeSpecialIO(opcode, bytecode, pc, registers, code); break; @@ -3099,6 +3103,14 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, return SlowOpcodeHandler.executeLoadGlob(bytecode, pc, registers, code); case Opcodes.SLEEP_OP: return SlowOpcodeHandler.executeSleep(bytecode, pc, registers); + case Opcodes.LOAD_SYMBOLIC_GLOB: + return SlowOpcodeHandler.executeLoadSymbolicGlob(bytecode, pc, registers); + case Opcodes.DEREF_GLOB: + return SlowOpcodeHandler.executeDerefGlob(bytecode, pc, registers); + case Opcodes.DEREF_HASH_NONSTRICT: + return SlowOpcodeHandler.executeDerefHashNonStrict(bytecode, pc, registers, code); + case Opcodes.DEREF_ARRAY_NONSTRICT: + return SlowOpcodeHandler.executeDerefArrayNonStrict(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..748103d65 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -896,6 +896,27 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(globReg); bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.lastResultReg = globReg; + } else if (leftOp.operator.equals("*") && + (leftOp.operand instanceof BlockNode || + leftOp.operand instanceof OperatorNode || + leftOp.operand instanceof StringNode)) { + // Dynamic typeglob assignment: *{"Pkg::name"} = value, *$ref = value, *{'name'} = value + // Evaluate the expression to get the glob name at runtime + leftOp.operand.accept(bytecodeCompiler); + int nameReg = bytecodeCompiler.lastResultReg; + + // Load the glob via symbolic reference + int globReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.LOAD_SYMBOLIC_GLOB, node.getIndex()); + bytecodeCompiler.emitReg(globReg); + bytecodeCompiler.emitReg(nameReg); + + // 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/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 21a5ea90d..0824d86cc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -835,10 +835,16 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode // Allocate register for result int rd = bytecodeCompiler.allocateRegister(); - // Emit direct opcode EVAL_STRING + // Emit direct opcode EVAL_STRING with call-site strict/feature/warning flags + // so EvalStringHandler inherits the pragmas in effect at the eval call site + // (not just the end-of-compilation snapshot in InterpretedCode) + int callSiteStrictOptions = bytecodeCompiler.getCurrentStrictOptions(); + int callSiteFeatureFlags = bytecodeCompiler.getCurrentFeatureFlags(); bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(stringReg); + bytecodeCompiler.emitInt(callSiteStrictOptions); + bytecodeCompiler.emitInt(callSiteFeatureFlags); bytecodeCompiler.lastResultReg = rd; } else { diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 13db3320e..862d0f633 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -49,7 +49,9 @@ public static RuntimeScalar evalString(String perlCode, InterpretedCode currentCode, RuntimeBase[] registers, String sourceName, - int sourceLine) { + int sourceLine, + int callSiteStrictOptions, + int callSiteFeatureFlags) { try { // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); @@ -65,13 +67,15 @@ public static RuntimeScalar evalString(String perlCode, opts.fileName = sourceName + " (eval)"; ScopedSymbolTable symbolTable = new ScopedSymbolTable(); - // Inherit lexical pragma flags from parent if available + // Inherit lexical pragma flags from the call site (not end-of-compilation snapshot) + // callSiteStrictOptions/callSiteFeatureFlags are embedded in the bytecode at the eval + // call site, capturing the exact pragmas in effect at that point (e.g. inside a + // "no strict 'refs'" block). Fall back to currentCode snapshot if not available. + symbolTable.strictOptionsStack.pop(); + symbolTable.strictOptionsStack.push(callSiteStrictOptions); + symbolTable.featureFlagsStack.pop(); + symbolTable.featureFlagsStack.push(callSiteFeatureFlags); if (currentCode != null) { - // Replace default values with parent's flags - symbolTable.strictOptionsStack.pop(); - symbolTable.strictOptionsStack.push(currentCode.strictOptions); - symbolTable.featureFlagsStack.pop(); - symbolTable.featureFlagsStack.push(currentCode.featureFlags); symbolTable.warningFlagsStack.pop(); symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 559e67830..64740c7e8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1071,5 +1071,21 @@ public class Opcodes { * Effect: Restores previous packageName */ public static final short POP_PACKAGE = 308; + /** Load glob via symbolic reference: rd = GlobalVariable.getGlobalIO(nameReg.toString()) + * Format: LOAD_SYMBOLIC_GLOB rd nameReg */ + public static final short LOAD_SYMBOLIC_GLOB = 333; + + /** Dereference scalar as glob: rd = scalarReg.globDeref() + * Format: DEREF_GLOB rd scalarReg */ + public static final short DEREF_GLOB = 334; + + /** Dereference scalar as hash (no strict refs): rd = scalarReg.hashDerefNonStrict(pkg) + * Format: DEREF_HASH_NONSTRICT rd scalarReg packageIdx */ + public static final short DEREF_HASH_NONSTRICT = 335; + + /** Dereference scalar as array (no strict refs): rd = scalarReg.arrayDerefNonStrict(pkg) + * Format: DEREF_ARRAY_NONSTRICT rd scalarReg packageIdx */ + public static final short DEREF_ARRAY_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 39d8eafea..cccb44338 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -240,6 +240,9 @@ public static int executeEvalString( int rd = bytecode[pc++]; int stringReg = bytecode[pc++]; + // Read call-site strict/feature flags embedded by CompileOperator at the eval site + int callSiteStrictOptions = bytecode[pc++]; + int callSiteFeatureFlags = bytecode[pc++]; // Get the code string - handle both RuntimeScalar and RuntimeList (from string interpolation) RuntimeBase codeValue = registers[stringReg]; @@ -255,10 +258,12 @@ public static int executeEvalString( // Call EvalStringHandler to parse, compile, and execute RuntimeScalar result = EvalStringHandler.evalString( perlCode, - code, // Current InterpretedCode for context - registers, // Current registers for variable access + code, // Current InterpretedCode for context + registers, // Current registers for variable access code.sourceName, - code.sourceLine + code.sourceLine, + callSiteStrictOptions, // Strict flags at the eval call site + callSiteFeatureFlags // Feature flags at the eval call site ); registers[rd] = result; @@ -290,6 +295,84 @@ public static int executeSelect( return pc; } + /** + * DEREF_HASH_NONSTRICT: rd = scalarReg.hashDerefNonStrict(pkg) + * Format: [DEREF_HASH_NONSTRICT] [rd] [scalarReg] [packageIdx] + * Effect: Dereferences a scalar as a hash with no-strict-refs semantics + */ + public static int executeDerefHashNonStrict( + int[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + + int rd = bytecode[pc++]; + int scalarReg = bytecode[pc++]; + int packageIdx = bytecode[pc++]; + + String packageName = code.stringPool[packageIdx]; + RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; + registers[rd] = scalar.hashDerefNonStrict(packageName); + return pc; + } + + /** + * DEREF_ARRAY_NONSTRICT: rd = scalarReg.arrayDerefNonStrict(pkg) + * Format: [DEREF_ARRAY_NONSTRICT] [rd] [scalarReg] [packageIdx] + * Effect: Dereferences a scalar as an array with no-strict-refs semantics + */ + public static int executeDerefArrayNonStrict( + int[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + + int rd = bytecode[pc++]; + int scalarReg = bytecode[pc++]; + int packageIdx = bytecode[pc++]; + + String packageName = code.stringPool[packageIdx]; + RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; + registers[rd] = scalar.arrayDerefNonStrict(packageName); + return pc; + } + + /** + * DEREF_GLOB: rd = scalarReg.globDeref() + * Format: [DEREF_GLOB] [rd] [scalarReg] + * Effect: Dereferences a scalar as a glob (** postfix deref) + */ + public static int executeDerefGlob( + int[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int scalarReg = bytecode[pc++]; + + RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; + registers[rd] = scalar.globDeref(); + return pc; + } + + /** + * LOAD_SYMBOLIC_GLOB: rd = getGlobalIO(nameReg.toString()) + * Format: [LOAD_SYMBOLIC_GLOB] [rd] [nameReg] + * Effect: Loads a glob via a runtime string expression (e.g. *{"Pkg::name"}) + */ + public static int executeLoadSymbolicGlob( + int[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int nameReg = bytecode[pc++]; + + String globName = registers[nameReg].toString(); + registers[rd] = GlobalVariable.getGlobalIO(globName); + return pc; + } + /** * SLOW_LOAD_GLOB: rd = getGlobalIO(name) * Format: [SLOW_LOAD_GLOB] [rd] [name_idx] From f37cd95f4adf93201cb759a707c3a27045111319 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 21:35:45 +0100 Subject: [PATCH 2/5] Fix interpreter: local *glob, DEREF strict, LOAD_SYMBOLIC_GLOB name normalization 1. local *glob support (BytecodeCompiler): Added '*' sigil case to compileVariableDeclaration. Emits LOAD_GLOB + PUSH_LOCAL_VARIABLE, matching JVM path (EmitOperatorLocal). This allows 'local *mysubalias' to correctly save/restore the glob state so that assignments inside eval STRING persist in the outer scope. 2. DEREF always calls scalarDeref() (BytecodeInterpreter): Fixed both DEREF cases in the main switch and executeTypeOps helper. Previously, DEREF silently passed through non-REFERENCE scalars. Now it always calls scalarDeref() which throws 'Not a SCALAR reference' for IO, FORMAT, and other non-reference types, matching Perl semantics. 3. LOAD_SYMBOLIC_GLOB name normalization (SlowOpcodeHandler): executeLoadSymbolicGlob now normalizes the glob name with the current package (e.g. 'mysub' -> 'main::mysub') before calling getGlobalIO(). Previously it used the raw name, creating a different glob object from the one where the sub was actually defined. 4. GLOB_SLOT_GET handles RuntimeGlob directly (SlowOpcodeHandler): executeGlobSlotGet now checks instanceof RuntimeGlob and calls hashDerefGetNonStrict on it directly (which RuntimeGlob overrides to call getGlobSlot). Previously .scalar() was called first which could lose the glob type. 5. Disassembler additions (InterpretedCode): Added LOAD_SYMBOLIC_GLOB, DEREF_GLOB, DEREF_HASH_NONSTRICT, DEREF_ARRAY_NONSTRICT to the bytecode disassembler. op/postfixderef.t: interpreter now matches JVM exactly (85/85) --- .../backend/bytecode/BytecodeCompiler.java | 19 +++++++++++++++ .../backend/bytecode/BytecodeInterpreter.java | 23 +++++-------------- .../backend/bytecode/InterpretedCode.java | 22 ++++++++++++++++++ .../backend/bytecode/SlowOpcodeHandler.java | 23 +++++++++++++------ 4 files changed, 63 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 768006121..5775c6a8c 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -2218,6 +2218,25 @@ void compileVariableDeclaration(OperatorNode node, String op) { OperatorNode sigilOp = (OperatorNode) node.operand; String sigil = sigilOp.operator; + if (sigil.equals("*") && sigilOp.operand instanceof IdentifierNode) { + // local *glob — save glob state and return same glob object + // Mirrors JVM path: load glob, call DynamicVariableManager.pushLocalVariable(RuntimeGlob) + String globName = NameNormalizer.normalizeVariableName(((IdentifierNode) sigilOp.operand).name, getCurrentPackage()); + int nameIdx = addToStringPool(globName); + + int globReg = allocateRegister(); + emitWithToken(Opcodes.LOAD_GLOB, node.getIndex()); + emitReg(globReg); + emit(nameIdx); + + // Push glob to local variable stack (saves state, returns same object) + emit(Opcodes.PUSH_LOCAL_VARIABLE); + emitReg(globReg); + + lastResultReg = globReg; + return; + } + if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode) { String varName = "$" + ((IdentifierNode) sigilOp.operand).name; diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 8e7e135cf..4312914af 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1376,20 +1376,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.DEREF: { // Dereference: rd = $$rs (scalar reference dereference) - // Can receive RuntimeScalar or RuntimeList + // Always call scalarDeref() — throws "Not a SCALAR reference" for + // non-reference types (IO, FORMAT, etc.), matching Perl semantics. int rd = bytecode[pc++]; int rs = bytecode[pc++]; RuntimeBase value = registers[rs]; - // Only dereference if it's a RuntimeScalar with REFERENCE type if (value instanceof RuntimeScalar) { - RuntimeScalar scalar = (RuntimeScalar) value; - if (scalar.type == RuntimeScalarType.REFERENCE) { - registers[rd] = scalar.scalarDeref(); - } else { - // Non-reference scalar, just copy - registers[rd] = value; - } + registers[rd] = ((RuntimeScalar) value).scalarDeref(); } else { // RuntimeList or other types, pass through registers[rd] = value; @@ -2223,15 +2217,10 @@ private static int executeTypeOps(int opcode, int[] bytecode, int pc, int rs = bytecode[pc++]; RuntimeBase value = registers[rs]; - // Only dereference if it's a RuntimeScalar with REFERENCE type + // Always call scalarDeref() on RuntimeScalar — throws "Not a SCALAR reference" + // for non-reference types (IO, FORMAT, etc.), matching Perl semantics. if (value instanceof RuntimeScalar) { - RuntimeScalar scalar = (RuntimeScalar) value; - if (scalar.type == RuntimeScalarType.REFERENCE) { - registers[rd] = scalar.scalarDeref(); - } else { - // Non-reference scalar, just copy - registers[rd] = value; - } + registers[rd] = ((RuntimeScalar) value).scalarDeref(); } else { // RuntimeList or other types, pass through registers[rd] = value; diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index d90e9619d..7e0306479 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -730,6 +730,28 @@ public String disassemble() { int keyReg = bytecode[pc++]; sb.append("GLOB_SLOT_GET r").append(rd).append(" = r").append(globReg2).append("{r").append(keyReg).append("}\n"); break; + case Opcodes.LOAD_SYMBOLIC_GLOB: + rd = bytecode[pc++]; + rs1 = bytecode[pc++]; + sb.append("LOAD_SYMBOLIC_GLOB r").append(rd).append(" = getGlobalIO(r").append(rs1).append(")\n"); + break; + case Opcodes.DEREF_GLOB: + rd = bytecode[pc++]; + rs1 = bytecode[pc++]; + sb.append("DEREF_GLOB r").append(rd).append(" = r").append(rs1).append(".globDeref()\n"); + break; + case Opcodes.DEREF_HASH_NONSTRICT: + rd = bytecode[pc++]; + rs1 = bytecode[pc++]; + rs2 = bytecode[pc++]; + sb.append("DEREF_HASH_NONSTRICT r").append(rd).append(" = r").append(rs1).append(".hashDerefNonStrict(pool[").append(rs2).append("])\n"); + break; + case Opcodes.DEREF_ARRAY_NONSTRICT: + rd = bytecode[pc++]; + rs1 = bytecode[pc++]; + rs2 = bytecode[pc++]; + sb.append("DEREF_ARRAY_NONSTRICT r").append(rd).append(" = r").append(rs1).append(".arrayDerefNonStrict(pool[").append(rs2).append("])\n"); + break; case Opcodes.SPRINTF: rd = bytecode[pc++]; int formatReg = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index cccb44338..21619f327 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -368,7 +368,10 @@ public static int executeLoadSymbolicGlob( int rd = bytecode[pc++]; int nameReg = bytecode[pc++]; - String globName = registers[nameReg].toString(); + // Normalize the name with the current package (e.g. "mysub" -> "main::mysub") + String rawName = registers[nameReg].toString(); + String pkg = InterpreterState.currentPackage.get().toString(); + String globName = NameNormalizer.normalizeVariableName(rawName, pkg); registers[rd] = GlobalVariable.getGlobalIO(globName); return pc; } @@ -1004,12 +1007,18 @@ public static int executeGlobSlotGet( // 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, pkg); + RuntimeScalar result; + if (globBase instanceof RuntimeGlob globObj) { + // Direct glob — access slot via a scalar wrapper that holds the glob reference + // RuntimeGlob.hashDerefGetNonStrict is not available directly; use scalar() to get + // a RuntimeScalar of type GLOB, then call hashDerefGetNonStrict on it. + // But scalar() on a RuntimeGlob returns a GLOB-typed scalar that delegates correctly. + result = globObj.scalar().hashDerefGetNonStrict(key, pkg); + } else { + // Already a scalar (e.g. from a variable holding a glob) + result = globBase.scalar().hashDerefGetNonStrict(key, pkg); + } + registers[rd] = result; return pc; } From 1bff72f6e189e803c2f4786918305d03416a7378 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 21:55:22 +0100 Subject: [PATCH 3/5] JPERL_DISASSEMBLE env also enables JVM bytecode disassembly --- src/main/java/org/perlonjava/app/cli/CompilerOptions.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/app/cli/CompilerOptions.java b/src/main/java/org/perlonjava/app/cli/CompilerOptions.java index b12529d14..9736fcc9a 100644 --- a/src/main/java/org/perlonjava/app/cli/CompilerOptions.java +++ b/src/main/java/org/perlonjava/app/cli/CompilerOptions.java @@ -34,7 +34,7 @@ */ public class CompilerOptions implements Cloneable { public boolean debugEnabled = false; - public boolean disassembleEnabled = false; + public boolean disassembleEnabled = System.getenv("JPERL_DISASSEMBLE") != null; public boolean useInterpreter = false; public boolean tokenizeOnly = false; public boolean parseOnly = false; From 32328e623a0a67c68c63f955eb86edcf4253bfd7 Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 21:59:16 +0100 Subject: [PATCH 4/5] Fix open lvalue: pass fhReg explicitly in OPEN opcode --- .../backend/bytecode/CompileOperator.java | 16 ++++++++++++---- .../backend/bytecode/InterpretedCode.java | 3 ++- .../bytecode/OpcodeHandlerExtended.java | 18 ++++++++++++++++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 0824d86cc..1ed2a34e6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1675,13 +1675,19 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.throwCompilerException("open requires arguments"); } - // Compile all arguments into a list + // Compile the filehandle argument (first arg) as an lvalue register + // We must NOT push it through ARRAY_PUSH (which copies via addToArray), + // because IOOperator.open needs to call fileHandle.set() on the actual lvalue. + argsList.elements.get(0).accept(bytecodeCompiler); + int fhReg = bytecodeCompiler.lastResultReg; + + // Compile remaining arguments into a list (mode, filename/ref, ...) int argsReg = bytecodeCompiler.allocateRegister(); bytecodeCompiler.emit(Opcodes.NEW_ARRAY); bytecodeCompiler.emitReg(argsReg); - for (Node arg : argsList.elements) { - arg.accept(bytecodeCompiler); + for (int i = 1; i < argsList.elements.size(); i++) { + argsList.elements.get(i).accept(bytecodeCompiler); int elemReg = bytecodeCompiler.lastResultReg; bytecodeCompiler.emit(Opcodes.ARRAY_PUSH); @@ -1689,11 +1695,13 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitReg(elemReg); } - // Call open with context and args + // Call open: OPEN rd ctx fhReg argsReg + // fhReg is the actual lvalue register for the filehandle (written back directly) int rd = bytecodeCompiler.allocateRegister(); bytecodeCompiler.emit(Opcodes.OPEN); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + bytecodeCompiler.emitReg(fhReg); bytecodeCompiler.emitReg(argsReg); bytecodeCompiler.lastResultReg = rd; diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 7e0306479..51d6c2282 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -788,8 +788,9 @@ public String disassemble() { case Opcodes.OPEN: rd = bytecode[pc++]; int openCtx = bytecode[pc++]; + int openFhReg = bytecode[pc++]; int openArgs = bytecode[pc++]; - sb.append("OPEN r").append(rd).append(" = open(ctx=").append(openCtx).append(", r").append(openArgs).append(")\n"); + sb.append("OPEN r").append(rd).append(" = open(ctx=").append(openCtx).append(", fh=r").append(openFhReg).append(", args=r").append(openArgs).append(")\n"); break; case Opcodes.READLINE: rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 505c00733..efd8ab846 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -713,14 +713,28 @@ public static int executePostAutoDecrement(int[] bytecode, int pc, RuntimeBase[] /** * Execute open operation. - * Format: OPEN rd ctx argsReg + * Format: OPEN rd ctx fhReg argsReg + * + * fhReg is the actual lvalue register for the filehandle. IOOperator.open calls + * fileHandle.set() on args[0], so we pass registers[fhReg] directly (not a copy + * from ARRAY_PUSH which would call addToArray -> new RuntimeScalar(this)). + * After the call, registers[fhReg] has been updated in place by set(). */ public static int executeOpen(int[] bytecode, int pc, RuntimeBase[] registers) { int rd = bytecode[pc++]; int ctx = bytecode[pc++]; + int fhReg = bytecode[pc++]; int argsReg = bytecode[pc++]; RuntimeArray argsArray = (RuntimeArray) registers[argsReg]; - RuntimeBase[] argsVarargs = argsArray.elements.toArray(new RuntimeBase[0]); + + // Build varargs with the actual fh lvalue as args[0], then the rest + RuntimeBase fhLvalue = registers[fhReg]; + RuntimeBase[] argsVarargs = new RuntimeBase[argsArray.elements.size() + 1]; + argsVarargs[0] = fhLvalue; + for (int i = 0; i < argsArray.elements.size(); i++) { + argsVarargs[i + 1] = argsArray.elements.get(i); + } + registers[rd] = IOOperator.open(ctx, argsVarargs); return pc; } From feb214159394a2f0661b4f4be372fa3da0b475df Mon Sep 17 00:00:00 2001 From: Flavio Soibelmann Glock Date: Mon, 23 Feb 2026 22:45:27 +0100 Subject: [PATCH 5/5] Fix interpreter DEREF opcode regression in op/decl-refs.t The DEREF opcode was calling scalarDeref() on all RuntimeScalar types, causing 'Not a SCALAR reference' crashes for ARRAYREFERENCE, HASHREFERENCE, CODE, and REGEX types in no-strict-refs contexts (e.g. decl-refs.t). Fix: pass through non-scalar reference types (ARRAYREFERENCE, HASHREFERENCE, CODE, REGEX) unchanged, and only call scalarDeref() for scalar refs, undef, and non-reference types (strings, globs, FORMAT, IO, etc.). This matches the JVM compiler behavior where scalarDerefNonStrict() is used for no-strict contexts (which also passes through non-scalar refs). Also adds: - DEREF_NONSTRICT opcode (337) for future use - executeDerefNonStrict() handler in SlowOpcodeHandler - DEREF_NONSTRICT disassembler entry in InterpretedCode - UNDEF autovivification in scalarDerefNonStrict() matching scalarDeref() Results: - op/decl-refs.t interpreter: 272/408 (was 169, regression fixed) - op/postfixderef.t interpreter: 85/86 (unchanged) - op/require_gh20577.t interpreter: 7/9 (unchanged) --- .../backend/bytecode/BytecodeInterpreter.java | 33 +++++++++++++++---- .../backend/bytecode/InterpretedCode.java | 8 +++++ .../perlonjava/backend/bytecode/Opcodes.java | 4 +++ .../backend/bytecode/SlowOpcodeHandler.java | 21 ++++++++++++ .../runtime/runtimetypes/RuntimeScalar.java | 7 ++++ 5 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 4312914af..e960c83b8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1383,9 +1383,20 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c RuntimeBase value = registers[rs]; if (value instanceof RuntimeScalar) { - registers[rd] = ((RuntimeScalar) value).scalarDeref(); + RuntimeScalar sv = (RuntimeScalar) value; + // Call scalarDeref() for scalar refs, undef, non-ref types (strings, globs, etc.) + // Pass through non-scalar reference types (array/hash/code/regex refs) — + // those are handled by the JVM compiler as non-scalar refs and should not + // throw here (decl-refs.t uses $$arrayref in no-strict context). + if (sv.type == RuntimeScalarType.ARRAYREFERENCE + || sv.type == RuntimeScalarType.HASHREFERENCE + || sv.type == RuntimeScalarType.CODE + || sv.type == RuntimeScalarType.REGEX) { + registers[rd] = sv; // pass through non-scalar refs + } else { + registers[rd] = sv.scalarDeref(); + } } else { - // RuntimeList or other types, pass through registers[rd] = value; } break; @@ -1772,6 +1783,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.DEREF_GLOB: case Opcodes.DEREF_HASH_NONSTRICT: case Opcodes.DEREF_ARRAY_NONSTRICT: + case Opcodes.DEREF_NONSTRICT: pc = executeSpecialIO(opcode, bytecode, pc, registers, code); break; @@ -2217,12 +2229,19 @@ private static int executeTypeOps(int opcode, int[] bytecode, int pc, int rs = bytecode[pc++]; RuntimeBase value = registers[rs]; - // Always call scalarDeref() on RuntimeScalar — throws "Not a SCALAR reference" - // for non-reference types (IO, FORMAT, etc.), matching Perl semantics. + // Call scalarDeref() for scalar refs, undef, non-ref types (strings, globs, etc.) + // Pass through non-scalar reference types (array/hash/code/regex refs). if (value instanceof RuntimeScalar) { - registers[rd] = ((RuntimeScalar) value).scalarDeref(); + RuntimeScalar sv = (RuntimeScalar) value; + if (sv.type == RuntimeScalarType.ARRAYREFERENCE + || sv.type == RuntimeScalarType.HASHREFERENCE + || sv.type == RuntimeScalarType.CODE + || sv.type == RuntimeScalarType.REGEX) { + registers[rd] = sv; // pass through non-scalar refs + } else { + registers[rd] = sv.scalarDeref(); + } } else { - // RuntimeList or other types, pass through registers[rd] = value; } return pc; @@ -3100,6 +3119,8 @@ private static int executeSpecialIO(int opcode, int[] bytecode, int pc, return SlowOpcodeHandler.executeDerefHashNonStrict(bytecode, pc, registers, code); case Opcodes.DEREF_ARRAY_NONSTRICT: return SlowOpcodeHandler.executeDerefArrayNonStrict(bytecode, pc, registers, code); + case Opcodes.DEREF_NONSTRICT: + return SlowOpcodeHandler.executeDerefNonStrict(bytecode, pc, registers, code); default: throw new RuntimeException("Unknown special I/O opcode: " + opcode); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 51d6c2282..a6d927b5d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -886,6 +886,14 @@ public String disassemble() { rs = bytecode[pc++]; sb.append("DEREF r").append(rd).append(" = ${r").append(rs).append("}\n"); break; + case Opcodes.DEREF_NONSTRICT: { + rd = bytecode[pc++]; + rs = bytecode[pc++]; + int derefNsPkgIdx = bytecode[pc++]; + sb.append("DEREF_NONSTRICT r").append(rd).append(" = ${r").append(rs) + .append("} pkg=").append(stringPool[derefNsPkgIdx]).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 64740c7e8..6fccae878 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1087,5 +1087,9 @@ public class Opcodes { * Format: DEREF_ARRAY_NONSTRICT rd scalarReg packageIdx */ public static final short DEREF_ARRAY_NONSTRICT = 336; + /** Dereference scalar (no strict refs): rd = scalarReg.scalarDerefNonStrict(pkg) + * Format: DEREF_NONSTRICT rd scalarReg packageIdx */ + public static final short DEREF_NONSTRICT = 337; + 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 21619f327..98841ee3e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -337,6 +337,27 @@ public static int executeDerefArrayNonStrict( return pc; } + /** + * DEREF_NONSTRICT: rd = scalarReg.scalarDerefNonStrict(pkg) + * Format: [DEREF_NONSTRICT] [rd] [scalarReg] [packageIdx] + * Effect: Dereferences a scalar as a scalar with no-strict-refs semantics ($$ref or symbolic ref) + */ + public static int executeDerefNonStrict( + int[] bytecode, + int pc, + RuntimeBase[] registers, + InterpretedCode code) { + + int rd = bytecode[pc++]; + int scalarReg = bytecode[pc++]; + int packageIdx = bytecode[pc++]; + + String packageName = code.stringPool[packageIdx]; + RuntimeScalar scalar = (RuntimeScalar) registers[scalarReg]; + registers[rd] = scalar.scalarDerefNonStrict(packageName); + return pc; + } + /** * DEREF_GLOB: rd = scalarReg.globDeref() * Format: [DEREF_GLOB] [rd] [scalarReg] diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index e3f201f33..374c60118 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -1106,6 +1106,13 @@ public RuntimeScalar scalarDerefNonStrict(String packageName) { } return switch (type) { + case UNDEF -> { + // Autovivify: create a new scalar reference for undefined values (same as scalarDeref) + RuntimeScalar newScalar = new RuntimeScalar(); + this.value = newScalar; + this.type = RuntimeScalarType.REFERENCE; + yield newScalar; + } case REFERENCE -> (RuntimeScalar) value; case TIED_SCALAR -> tiedFetch().scalarDerefNonStrict(packageName); default -> {