From a2791af0cf555b29a80af5ea6841e2e631bb33cc Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 5 Mar 2026 19:20:29 +0100 Subject: [PATCH 1/4] Fix interpreter strict vars check to allow use-vars-declared globals The BytecodeCompiler shouldBlockGlobalUnderStrictVars() was missing the allowIfAlreadyExists check that the JVM backend (EmitVariable.java) has. When a large file like ExifTool.pm (10K lines) falls back from JVM to interpreter compilation, CompilerFlagNode in the AST re-enables strict vars. But use vars declarations (which create globals at parse time) were not recognized by the interpreter strict check, causing false 'requires explicit package name' errors. The fix checks GlobalVariable.existsGlobalVariable/Array/Hash() before blocking access, mirroring the JVM backend logic. Impact: ExifTool tests go from 0/600 (all crashing) to 597/600 (99.5%). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index c99706e8d..c9c355a20 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -423,6 +423,20 @@ boolean shouldBlockGlobalUnderStrictVars(String varName) { return false; } + // Allow variables that already exist in the global registry + // (e.g., created by `use vars` at parse time) + // This mirrors the allowIfAlreadyExists logic in EmitVariable.java + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); + if (sigil.equals("$") && GlobalVariable.existsGlobalVariable(normalizedName)) { + return false; + } + if (sigil.equals("@") && GlobalVariable.existsGlobalArray(normalizedName)) { + return false; + } + if (sigil.equals("%") && !normalizedName.endsWith("::") && GlobalVariable.existsGlobalHash(normalizedName)) { + return false; + } + // BLOCK: Unqualified variable under strict vars return true; } From 9bcc019b57aaa7c1237f8e2bade4cc0b110a4bb2 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 5 Mar 2026 19:28:16 +0100 Subject: [PATCH 2/4] Add null guard to RuntimeScalar.set() for interpreter robustness RuntimeScalar.set(null) could occur in large interpreter-compiled subroutines (e.g., WriteExif.pl) when a register holds a null value from a nested call chain. Treat null as undef, matching Perl semantics. This fixes the remaining 3 ExifTool test crashes (DNG test 3, Nikon tests 8-9), bringing ExifTool from 597/600 to 600/600 (100%). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/runtime/runtimetypes/RuntimeScalar.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index e6a4471e2..a184f569d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -640,6 +640,11 @@ public RuntimeScalar addToScalar(RuntimeScalar scalar) { // Setters public RuntimeScalar set(RuntimeScalar value) { + if (value == null) { + this.type = RuntimeScalarType.UNDEF; + this.value = null; + return this; + } if (value.type == TIED_SCALAR) { return set(value.tiedFetch()); } From 8e178d351ee6c28d908137f12fb0fc4b7cab7556 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 5 Mar 2026 19:40:27 +0100 Subject: [PATCH 3/4] Guard against null elements in RuntimeList and list assignment Null elements can enter RuntimeArray via delete(), sparse initialization, or array extension. These nulls propagate through addToArray() and setFromList() into RuntimeList, where scalar() or set() on them causes NPEs. Add null guards in RuntimeList.scalar() and setFromList() to treat null elements as undef, matching Perl semantics. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/runtime/runtimetypes/RuntimeList.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index e3c9d0fb5..459364966 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -329,7 +329,8 @@ public RuntimeScalar scalar() { return scalarUndef; // Return undefined if empty } // XXX expand the last element - return elements.getLast().scalar(); + RuntimeBase last = elements.getLast(); + return (last == null) ? scalarUndef : last.scalar(); } /** @@ -464,8 +465,8 @@ public RuntimeArray setFromList(RuntimeList value) { rhsIndex++; } } else if (elem instanceof RuntimeScalar runtimeScalar) { - RuntimeScalar assigned = (rhsIndex < rhsSize) ? rhsElements.get(rhsIndex++) : new RuntimeScalar(); - runtimeScalar.set(assigned); + RuntimeScalar assigned = (rhsIndex < rhsSize) ? rhsElements.get(rhsIndex++) : null; + runtimeScalar.set(assigned != null ? assigned : new RuntimeScalar()); result.elements.add(runtimeScalar); // Add reference to the variable itself } else if (elem instanceof RuntimeArray runtimeArray) { List remaining = (rhsIndex < rhsSize) From de901b22c41b062918c8062d4870d81cbcf2b3b9 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 5 Mar 2026 21:01:44 +0100 Subject: [PATCH 4/4] Fix strict vars regressions: single-letter restriction, leading-zero captures, error message - Add single-letter variable restriction to interpreter shouldBlockGlobalUnderStrictVars, mirroring EmitVariable.java: single Unicode letter vars cannot bypass strict just because they exist in GlobalVariable registry. Fixes uni/variables.t (115 failures to 0). - Reject leading-zero capture variables ($01, $02) under strict in both BytecodeCompiler and EmitVariable. Fixes 12 re/pat.t failures (253 to 241). - Fix open() error message to match Perl: Unknown open() mode. Fixes io/open.t (33 to 31). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 27 ++++++++++++++++--- .../perlonjava/backend/jvm/EmitVariable.java | 2 +- .../runtime/runtimetypes/RuntimeIO.java | 2 +- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index c9c355a20..c0880d0a7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -396,8 +396,8 @@ boolean shouldBlockGlobalUnderStrictVars(String varName) { return false; } - // Allow regex capture variables ($1, $2, etc.) - if (ScalarUtils.isInteger(bareVarName)) { + // Allow regex capture variables ($1, $2, etc.) but not leading-zero variants ($01, $02) + if (ScalarUtils.isInteger(bareVarName) && !bareVarName.startsWith("0")) { return false; } @@ -427,13 +427,32 @@ boolean shouldBlockGlobalUnderStrictVars(String varName) { // (e.g., created by `use vars` at parse time) // This mirrors the allowIfAlreadyExists logic in EmitVariable.java String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); + boolean allowIfAlreadyExists = false; if (sigil.equals("$") && GlobalVariable.existsGlobalVariable(normalizedName)) { - return false; + allowIfAlreadyExists = true; } if (sigil.equals("@") && GlobalVariable.existsGlobalArray(normalizedName)) { - return false; + allowIfAlreadyExists = true; } if (sigil.equals("%") && !normalizedName.endsWith("::") && GlobalVariable.existsGlobalHash(normalizedName)) { + allowIfAlreadyExists = true; + } + + // Perl's strict 'vars' requires declaration for unqualified single-letter globals + // even if they were previously created under 'no strict'. + // This mirrors EmitVariable.java lines 349-359. + boolean isSpecialSortVar = sigil.equals("$") + && (bareVarName.equals("a") || bareVarName.equals("b")); + if (sigil.equals("$") + && bareVarName != null + && bareVarName.length() == 1 + && Character.isLetter(bareVarName.charAt(0)) + && !bareVarName.contains("::") + && !isSpecialSortVar) { + allowIfAlreadyExists = false; + } + + if (allowIfAlreadyExists) { return false; } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index ac3bd26e5..6e9fd6cac 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -361,7 +361,7 @@ static void handleVariableOperator(EmitterVisitor emitterVisitor, OperatorNode n // Compute createIfNotExists flag - determines if variable can be auto-vivified boolean createIfNotExists = name.contains("::") // Fully qualified: $Package::var - || ScalarUtils.isInteger(name) // Regex capture: $1, $2, etc. + || (ScalarUtils.isInteger(name) && !name.startsWith("0")) // Regex capture: $1, $2, etc. || isSpecialSortVar // Sort variables: $a, $b || isBuiltinSpecialLengthOneVar(sigil, name) // $%, $-, $[, $}, etc. || isBuiltinSpecialScalarVar(sigil, name) // ${^GLOBAL_PHASE}, $ARGV, $ENV, etc. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index faea5221a..3792ceb2a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -912,7 +912,7 @@ private Set convertMode(String mode) { Set options = MODE_OPTIONS.get(mode); if (options == null) { - throw new PerlCompilerException("Unsupported file mode: " + mode); + throw new PerlCompilerException("Unknown open() mode '" + mode + "'"); } return new HashSet<>(options); }