diff --git a/dev/design/log4perl-compatibility.md b/dev/design/log4perl-compatibility.md index 2be816ef6..70355cf68 100644 --- a/dev/design/log4perl-compatibility.md +++ b/dev/design/log4perl-compatibility.md @@ -10,19 +10,17 @@ This document tracks the work needed to make `./jcpan Log::Log4perl` fully pass ``` Files=73, Tests=700 -Failed 6/73 test programs -Failed 11/700 subtests +Failed 4/73 test programs +Failed 8/700 subtests ``` -**Improvement from previous:** Was 18/700 subtests failing. Fixed 7 caller() line number issues. +**Improvement from previous:** Was 10/700 subtests failing. Fixed `local $OurVariable` bug affecting %T stack trace format. ### Failing Tests Summary | Test File | Failed/Total | Issue Category | |-----------|--------------|----------------| | t/016Export.t | 1/16 | DESTROY message during global destruction | -| t/022Wrap.t | 2/5 | %T (stack trace) format - too many frames | -| t/024WarnDieCarp.t | 1/73 | One remaining caller() issue (test 62) | | t/026FileApp.t | 3/27 | File permissions / chmod | | t/041SafeEval.t | 3/23 | Safe.pm compartment restrictions | | t/049Unhide.t | 1/1 | Source filter / ###l4p | @@ -33,7 +31,8 @@ Failed 11/700 subtests |-----------|----------|---------|---------------| | t/020Easy.t | 3/21 failed | All pass | local $pkg::var bug fixed, bareword IO handles | | t/051Extra.t | 2/11 failed | All pass | Line number reporting improvements | -| t/024WarnDieCarp.t | 11/73 failed | 8/73 failed | Partial improvement in caller() | +| t/024WarnDieCarp.t | 11/73 failed | All pass | caller() line number fix + eval block name | +| t/022Wrap.t | 2/5 failed | All pass | local $OurVariable fix for %T stack trace | ### Resolved: t/020Easy.t Carp.pm Error @@ -172,38 +171,40 @@ BEGIN failed--compilation aborted at -e line 1, near "" ## Remaining Issues (Updated 2026-03-19) -### Issue 1: caller() Line Number Reporting - MOSTLY FIXED +### Issue 1: caller() Line Number Reporting - FIXED -**Status:** Fixed 7 of 8 failures. One remaining issue (test 62). +**Status:** All tests passing. Both line numbers and eval block names now work correctly. -**Fix Applied:** Changed `ByteCodeSourceMapper.saveSourceLocation()` to use `getLineNumberAccurate()` -instead of `getLineNumber()`. The forward-only cache in `getLineNumber()` was returning stale -values during deferred subroutine compilation. - -**Remaining failure (test 62):** Needs further investigation - may be a different root cause. +**Fixes Applied:** +1. Changed `ByteCodeSourceMapper.saveSourceLocation()` to use `getLineNumberAccurate()` + instead of `getLineNumber()` (fixed 7 tests) +2. Set subroutine context to "(eval)" during `eval { BLOCK }` parsing (fixed test 62) +3. Don't add package prefix to special names like "(eval)" in ExceptionFormatter **Files Changed:** - `src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java` +- `src/main/java/org/perlonjava/frontend/parser/OperatorParser.java` +- `src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java` +- `src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java` **Design Document:** `dev/design/caller_line_number_fix.md` -### Issue 2: Stack Trace Format (%T) - ACTIVE +### Issue 2: Stack Trace Format (%T) - FIXED -**Status:** Working but includes too many frames. +**Status:** FIXED - `local $Carp::CarpLevel` now works correctly inside subroutines. -**Symptom:** t/022Wrap.t tests fail because %T (Carp::longmess) includes internal Log4perl frames. +**Root Cause:** When `local $VarName` was used inside a subroutine where `$VarName` was declared with `our` in an outer scope, the localization didn't work correctly: +1. JVM Backend: `EmitOperatorLocal` checked if variable was in symbol table and used wrong path +2. Interpreter Backend: `BytecodeCompiler` used cached register for `our` variables instead of loading from global table -**Example:** -``` -got: 'trace: Log::Log4perl::Layout::PatternLayout::render() called at ... line 306, - Log::Log4perl::Appender::log() called at ... line 1115, ...' -expected: 'trace: at 022Wrap.t line 69' -``` +**Fix Applied:** +- `EmitOperatorLocal.java`: Check for `our` variables when handling `local` and use `GlobalRuntimeScalar.makeLocal()` for them +- `BytecodeCompiler.java`: For scalars/arrays/hashes declared with `our`, use `LOAD_GLOBAL_*` instead of cached register -**Root Cause:** PerlOnJava's Carp::longmess includes all stack frames. Perl's version filters out internal frames based on `@CARP_NOT` and caller level adjustments that Log4perl uses. +**Commit:** 4737089da -**Affected Tests:** -- t/022Wrap.t (2 failures: tests 1-2) +**Tests Fixed:** +- t/022Wrap.t (2 tests) - `%T` format now correctly filters internal frames ### Issue 3: DESTROY During Global Destruction @@ -400,7 +401,7 @@ For chmod/umask: ## Progress Tracking -### Current Status: 11/700 subtests failing (was 18/700) +### Current Status: 8/700 subtests failing (was 10/700) ### Completed - [x] *{NAME} glob slot accessor (2026-03-18) @@ -411,19 +412,18 @@ For chmod/umask: - [x] exit() inside BEGIN blocks (2026-03-19) - [x] local $Pkg::Var bug fix (2026-03-19, PR #333) - [x] caller() line number fix (2026-03-19) - Fixed 7/8 failures +- [x] eval block "(eval)" name in caller() (2026-03-19) - Fixed test 62 +- [x] local $OurVariable fix (2026-03-19) - Fixed %T stack trace format ### Active Issues -- [ ] caller() test 62 (1 test) - needs investigation -- [ ] %T stack trace format (2 tests) - [ ] DESTROY during global destruction (1 test) - [ ] chmod/file permissions (3 tests) - [ ] Safe.pm restrictions (3 tests) - [ ] Source filters (1 test) ### Next Steps -1. Investigate remaining caller() test 62 failure -2. Consider improving Carp.pm @CARP_NOT handling for %T format -3. Investigate DESTROY during global destruction +1. Investigate DESTROY during global destruction +2. Investigate chmod/file permissions issue --- diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index d7be38c76..26f9e4a5e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3535,9 +3535,23 @@ void compileVariableReference(OperatorNode node, String op) { emit(currentSubroutineBeginId); lastResultReg = rd; - } else if (hasVariable(varName)) { - // Lexical variable - use existing register + } else if (hasVariable(varName) && !isOurVariable(varName)) { + // Lexical variable (my/state) - use existing register lastResultReg = getVariableRegister(varName); + } else if (hasVariable(varName) && isOurVariable(varName)) { + // 'our' variable - must load from global table to see local() changes + // This ensures 'local $Pkg::Var' modifications are visible inside subroutines + String globalVarName = varName.substring(1); // Remove $ sigil + globalVarName = NameNormalizer.normalizeVariableName( + globalVarName, + getCurrentPackage() + ); + int rd = allocateRegister(); + int nameIdx = addToStringPool(globalVarName); + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(rd); + emit(nameIdx); + lastResultReg = rd; } else { // Global variable - check strict vars then load if (shouldBlockGlobalUnderStrictVars(varName)) { @@ -3643,9 +3657,17 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(arrayReg); emit(nameIdx); emit(currentSubroutineBeginId); - } else if (hasVariable(varName)) { - // Lexical array - use existing register + } else if (hasVariable(varName) && !isOurVariable(varName)) { + // Lexical array (my/state) - use existing register arrayReg = getVariableRegister(varName); + } else if (hasVariable(varName) && isOurVariable(varName)) { + // 'our' array - must load from global table to see local() changes + arrayReg = allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, getCurrentPackage()); + int nameIdx = addToStringPool(globalArrayName); + emit(Opcodes.LOAD_GLOBAL_ARRAY); + emitReg(arrayReg); + emit(nameIdx); } else { // Global array - load it arrayReg = allocateRegister(); @@ -3750,8 +3772,17 @@ void compileVariableReference(OperatorNode node, String op) { emitReg(hashReg); emit(nameIdx); emit(currentSubroutineBeginId); - } else if (hasVariable(varName)) { + } else if (hasVariable(varName) && !isOurVariable(varName)) { + // Lexical hash (my/state) - use existing register hashReg = getVariableRegister(varName); + } else if (hasVariable(varName) && isOurVariable(varName)) { + // 'our' hash - must load from global table to see local() changes + hashReg = allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, getCurrentPackage()); + int nameIdx = addToStringPool(globalHashName); + emit(Opcodes.LOAD_GLOBAL_HASH); + emitReg(hashReg); + emit(nameIdx); } else { hashReg = allocateRegister(); String globalHashName = NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, getCurrentPackage()); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java index f55a2ee17..1ac7b6c6c 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java @@ -25,12 +25,21 @@ static void handleLocal(EmitterVisitor emitterVisitor, OperatorNode node) { Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); if (node.operand instanceof OperatorNode opNode && opNode.operator.equals("$")) { - // Check if the variable is global + // Check if the variable is global or 'our' variable if (opNode.operand instanceof IdentifierNode idNode) { String varName = opNode.operator + idNode.name; int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); - if (varIndex == -1) { - // Variable is global + // Use GlobalRuntimeScalar.makeLocal() for: + // 1. Truly global variables (not in symbol table) + // 2. 'our' variables (even if in symbol table, they're global package variables) + // This ensures 'local $OurVar' works correctly inside subroutines + boolean isOurVariable = false; + if (varIndex != -1) { + var symbolEntry = emitterVisitor.ctx.symbolTable.getSymbolEntry(varName); + isOurVariable = symbolEntry != null && "our".equals(symbolEntry.decl()); + } + if (varIndex == -1 || isOurVariable) { + // Variable is global or 'our' - use makeLocal String fullName = NameNormalizer.normalizeVariableName(idNode.name, emitterVisitor.ctx.symbolTable.getCurrentPackage()); mv.visitLdcInsn(fullName); mv.visitMethodInsn(Opcodes.INVOKESTATIC, diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index 467bc608f..8e81ab867 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -94,7 +94,8 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { // definition context. Only anonymous subs (my sub, state sub, or true anonymous subs) should // capture variables. This prevents issues like defining 'sub bar::foo' inside a block with // 'our sub foo' from incorrectly capturing the 'our sub' as a closure variable. - boolean isPackageSub = node.name != null && !node.name.equals(""); + // Note: "(eval)" is a special name for eval blocks which should capture variables like anonymous subs + boolean isPackageSub = node.name != null && !node.name.equals("") && !node.name.equals("(eval)"); if (isPackageSub) { // Package subs should not capture any closure variables // They can only access global variables and their parameters @@ -127,7 +128,12 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { // Copy package, subroutine, and flags from the current context newSymbolTable.setCurrentPackage(ctx.symbolTable.getCurrentPackage(), ctx.symbolTable.currentPackageIsClass()); - newSymbolTable.setCurrentSubroutine(ctx.symbolTable.getCurrentSubroutine()); + // For eval blocks "(eval)", set the subroutine name so caller() reports it correctly + if ("(eval)".equals(node.name)) { + newSymbolTable.setCurrentSubroutine("(eval)"); + } else { + newSymbolTable.setCurrentSubroutine(ctx.symbolTable.getCurrentSubroutine()); + } newSymbolTable.warningFlagsStack.pop(); newSymbolTable.warningFlagsStack.push(ctx.symbolTable.warningFlagsStack.peek()); newSymbolTable.featureFlagsStack.pop(); diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 1e284c6e7..e17dc03bc 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "8d1f29243"; + public static final String gitCommitId = "327451495"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 2547d933c..bf382d8ec 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -70,7 +70,15 @@ static AbstractNode parseEval(Parser parser, String operator) { if (token.type == OPERATOR && token.text.equals("{")) { // If the next token is '{', parse a block TokenUtils.consume(parser, OPERATOR, "{"); - block = ParseBlock.parseBlock(parser); + // Set subroutine context to "(eval)" BEFORE parsing the block + // This ensures source locations are saved with the correct context + String previousSubroutine = parser.ctx.symbolTable.getCurrentSubroutine(); + parser.ctx.symbolTable.setCurrentSubroutine("(eval)"); + try { + block = ParseBlock.parseBlock(parser); + } finally { + parser.ctx.symbolTable.setCurrentSubroutine(previousSubroutine); + } TokenUtils.consume(parser, OPERATOR, "}"); // Perl semantics: eval BLOCK behaves like a bare block for loop control. // `last/next/redo` inside the eval block must target the eval block itself, @@ -80,8 +88,9 @@ static AbstractNode parseEval(Parser parser, String operator) { } // transform: eval { 123 } // into: sub { 123 }->() with useTryCatch flag + // Use name "(eval)" so caller() reports this as an eval block (Perl behavior) return new BinaryOperatorNode("->", - new SubroutineNode(null, null, null, block, true, parser.tokenIndex), ParserNodeUtils.atUnderscoreArgs(parser), index); + new SubroutineNode("(eval)", null, null, block, true, parser.tokenIndex), ParserNodeUtils.atUnderscoreArgs(parser), index); } else { // Otherwise, parse an expression, and default to $_ operand = ListParser.parseZeroOrOneList(parser, 0); diff --git a/src/main/java/org/perlonjava/runtime/operators/Directory.java b/src/main/java/org/perlonjava/runtime/operators/Directory.java index 95cc77eb4..f65c56b50 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Directory.java +++ b/src/main/java/org/perlonjava/runtime/operators/Directory.java @@ -17,6 +17,7 @@ import static org.perlonjava.runtime.runtimetypes.RuntimeIO.handleIOException; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarFalse; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarTrue; +import static org.perlonjava.runtime.operators.UmaskOperator.applyUmask; public class Directory { @@ -259,7 +260,9 @@ public static RuntimeScalar mkdir(RuntimeList args) { // Set permissions only if the file system supports POSIX permissions if (FileSystems.getDefault().supportedFileAttributeViews().contains("posix")) { - Set permissions = getPosixFilePermissions(mode); + // Apply umask to the mode (Perl: effective_mode = mode & ~umask) + int effectiveMode = applyUmask(mode); + Set permissions = getPosixFilePermissions(effectiveMode); Files.setPosixFilePermissions(path, permissions); } // On Windows and other non-POSIX systems, permissions are handled by the OS diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java index 582a91ee8..f68d3b372 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ExceptionFormatter.java @@ -157,7 +157,8 @@ private static ArrayList> formatThrowable(Throwable t) { } String subName = frame.subroutineName(); - if (subName != null && !subName.isEmpty() && !subName.contains("::")) { + // Don't add package prefix if subName already contains "::" or is a special name like "(eval)" + if (subName != null && !subName.isEmpty() && !subName.contains("::") && !subName.startsWith("(")) { subName = pkg + "::" + subName; } @@ -180,7 +181,8 @@ private static ArrayList> formatThrowable(Throwable t) { String subName = loc.subroutineName(); // Prepend package name if subroutine name doesn't already include it - if (subName != null && !subName.isEmpty() && !subName.contains("::")) { + // Don't add package prefix for special names like "(eval)" + if (subName != null && !subName.isEmpty() && !subName.contains("::") && !subName.startsWith("(")) { subName = loc.packageName() + "::" + subName; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 913f11e84..df591143d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -33,6 +33,33 @@ public RuntimeGlob(String globName) { this.IO = new RuntimeScalar(); } + /** + * Creates a detached copy of this glob that shares the current IO slot reference. + * Used when assigning a glob to a scalar: `my $fh = *FH` + * + *

This is crucial for `local *GLOB` semantics. When you do: + *

+     *   local *FH;
+     *   open FH, ...; 
+     *   my $captured = *FH;
+     *   return $captured;
+     * 
+ * After the local scope ends, *FH's IO is restored, but $captured should + * still have the IO that was opened. This method creates a new RuntimeGlob + * that points to the CURRENT IO object, so when local restores the original + * glob, the captured copy is unaffected. + * + *

Subclasses (like RuntimeStashEntry) should override this to return + * the same instance, preserving their special ref() behavior. + * + * @return A new RuntimeGlob with the same globName and IO reference. + */ + public RuntimeGlob createDetachedCopy() { + RuntimeGlob copy = new RuntimeGlob(this.globName); + copy.IO = this.IO; // Share the current IO reference + return copy; + } + public static boolean isGlobAssigned(String globName) { return GlobalVariable.globalGlobs.getOrDefault(globName, false); } @@ -365,7 +392,14 @@ public RuntimeScalar getIO() { } public RuntimeGlob setIO(RuntimeScalar io) { - this.IO = io; + // If IO slot is tied (TIED_SCALAR with TieHandle), replace it entirely + // Otherwise use set() to modify in place, preserving sharing with detached copies + if (this.IO.type == RuntimeScalarType.TIED_SCALAR) { + this.IO = io; + } else { + this.IO.type = io.type; + this.IO.value = io.value; + } // If the IO scalar contains a RuntimeIO, set its glob name if (io.value instanceof RuntimeIO runtimeIO) { runtimeIO.globName = this.globName; @@ -376,7 +410,14 @@ public RuntimeGlob setIO(RuntimeScalar io) { public RuntimeGlob setIO(RuntimeIO io) { // Set the glob name in the RuntimeIO for proper stringification io.globName = this.globName; - this.IO = new RuntimeScalar(io); + // If IO slot is tied (TIED_SCALAR with TieHandle), replace it entirely + // Otherwise modify in place, preserving sharing with detached copies + if (this.IO.type == RuntimeScalarType.TIED_SCALAR) { + this.IO = new RuntimeScalar(io); + } else { + this.IO.type = RuntimeScalarType.GLOB; // RuntimeIO is stored as GLOB type + this.IO.value = io; + } return this; } @@ -612,22 +653,29 @@ public void dynamicSaveState() { RuntimeArray savedArray = GlobalVariable.getGlobalArray(this.globName); RuntimeHash savedHash = GlobalVariable.getGlobalHash(this.globName); RuntimeScalar savedCode = GlobalVariable.getGlobalCodeRef(this.globName); - globSlotStack.push(new GlobSlotSnapshot(this.globName, savedScalar, savedArray, savedHash, savedCode)); + // Save the current IO object reference (not its state) so we can restore it later. + // This allows captured glob references to keep the "local" IO even after restore. + RuntimeScalar savedIO = this.IO; + globSlotStack.push(new GlobSlotSnapshot(this.globName, savedScalar, savedArray, savedHash, savedCode, savedIO)); savedCode.dynamicSaveState(); savedArray.dynamicSaveState(); savedHash.dynamicSaveState(); savedScalar.dynamicSaveState(); GlobalVariable.getGlobalFormatRef(this.globName).dynamicSaveState(); - this.IO.dynamicSaveState(); + // Create a NEW IO slot for the local scope. + // Any code that captures this glob during local will get this new IO object. + this.IO = new RuntimeScalar(); } @Override public void dynamicRestoreState() { - this.IO.dynamicRestoreState(); - GlobSlotSnapshot snap = globSlotStack.pop(); + // Restore the saved IO object reference (not modify in place). + // This leaves any captured references pointing to the "local" IO. + this.IO = snap.io; + GlobalVariable.globalVariables.put(snap.globName, snap.scalar); snap.scalar.dynamicRestoreState(); @@ -649,6 +697,7 @@ private record GlobSlotSnapshot( RuntimeScalar scalar, RuntimeArray array, RuntimeHash hash, - RuntimeScalar code) { + RuntimeScalar code, + RuntimeScalar io) { } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index c75462db8..7a68b007a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -148,6 +148,11 @@ public RuntimeScalar(RuntimeGlob value) { this.type = UNDEF; } else { this.type = value.type; + // Create a detached copy so that `local *GLOB` restore doesn't affect + // scalars that captured the glob value during the local scope. + // This implements Perl's behavior where `my $fh = *FH` inside a local + // scope retains the IO even after the scope ends. + value = value.createDetachedCopy(); } this.value = value; } @@ -173,7 +178,7 @@ public RuntimeScalar(Object value) { case RuntimeGlob v -> { RuntimeScalar tmp = new RuntimeScalar(v); this.type = tmp.type; - this.value = v; + this.value = tmp.value; // Use the detached copy from the constructor } case RuntimeIO v -> { RuntimeScalar tmp = new RuntimeScalar(v); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java index 3f12650fd..7a02f31c8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java @@ -25,6 +25,17 @@ public RuntimeStashEntry(String globName, boolean isDefined) { // System.out.println("Stash Entry create: " + globName + " " + isDefined); } + /** + * Stash entries should not be detached - they need to preserve their type + * for ref() to return empty string correctly. + * + * @return this same instance + */ + @Override + public RuntimeGlob createDetachedCopy() { + return this; + } + // Note on Stash Operations: // // In Perl, a typeglob is a structure that holds a symbol table entry and a key (or slot).