From 5a0edb331049443de611df972bf5a0a305ca176b Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 19 Mar 2026 13:48:49 +0100 Subject: [PATCH 1/7] docs: Update log4perl table to reflect caller() fix results t/024WarnDieCarp.t now has 1/73 failures (was 8/73 before caller() fix) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/design/log4perl-compatibility.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dev/design/log4perl-compatibility.md b/dev/design/log4perl-compatibility.md index 2be816ef6..eb1ef5493 100644 --- a/dev/design/log4perl-compatibility.md +++ b/dev/design/log4perl-compatibility.md @@ -33,7 +33,7 @@ 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 | 1/73 failed | caller() line number fix (getLineNumberAccurate) | ### Resolved: t/020Easy.t Carp.pm Error From 09cbf651c1228b13f54aae42d1c7ba88d0498361 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 19 Mar 2026 15:03:11 +0100 Subject: [PATCH 2/7] Fix eval block reporting "(eval)" in caller() stack traces When code is inside `eval { BLOCK }`, caller() should report "(eval)" as the subroutine name. This matches Perl's behavior. Changes: - OperatorParser.java: Set subroutine context to "(eval)" before parsing the eval block, so source locations are saved with correct context - EmitSubroutine.java: Treat "(eval)" like anonymous subs for closure capture - ExceptionFormatter.java: Don't add package prefix to special names like "(eval)" Results: - Log::Log4perl t/024WarnDieCarp.t: All 73 tests now pass (was 1 failure) - Total Log4perl: 10/700 failing (was 11/700) Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/design/log4perl-compatibility.md | 40 ++++++++++--------- .../backend/jvm/EmitSubroutine.java | 10 ++++- .../frontend/parser/OperatorParser.java | 13 +++++- .../runtimetypes/ExceptionFormatter.java | 6 ++- 4 files changed, 44 insertions(+), 25 deletions(-) diff --git a/dev/design/log4perl-compatibility.md b/dev/design/log4perl-compatibility.md index eb1ef5493..6d0c5da1f 100644 --- a/dev/design/log4perl-compatibility.md +++ b/dev/design/log4perl-compatibility.md @@ -10,19 +10,18 @@ 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 5/73 test programs +Failed 10/700 subtests ``` -**Improvement from previous:** Was 18/700 subtests failing. Fixed 7 caller() line number issues. +**Improvement from previous:** Was 11/700 subtests failing. Fixed eval block name in caller(). ### 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/022Wrap.t | 2/5 | %T (stack trace) format - @CARP_NOT handling | | 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 +32,7 @@ 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 | 1/73 failed | caller() line number fix (getLineNumberAccurate) | +| t/024WarnDieCarp.t | 11/73 failed | All pass | caller() line number fix + eval block name | ### Resolved: t/020Easy.t Carp.pm Error @@ -172,18 +171,21 @@ 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` @@ -400,7 +402,7 @@ For chmod/umask: ## Progress Tracking -### Current Status: 11/700 subtests failing (was 18/700) +### Current Status: 10/700 subtests failing (was 11/700) ### Completed - [x] *{NAME} glob slot accessor (2026-03-18) @@ -411,19 +413,19 @@ 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 ### Active Issues -- [ ] caller() test 62 (1 test) - needs investigation -- [ ] %T stack trace format (2 tests) +- [ ] %T stack trace format (2 tests) - needs @CARP_NOT handling - [ ] 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. Implement @CARP_NOT handling in Carp.pm for %T format (2 tests) +2. Investigate DESTROY during global destruction +3. Investigate chmod/file permissions issue --- 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/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/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; } From d7b3a31bb307431f40ef769e39002dc9e2a34f79 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 19 Mar 2026 15:21:20 +0100 Subject: [PATCH 3/7] Fix local $OurVariable not seeing localized values When local $VarName was used inside a subroutine where $VarName was declared with our in an outer scope, the localization did not work correctly: 1. JVM Backend: EmitOperatorLocal checked if variable was in symbol table and used wrong path (DynamicVariableManager instead of GlobalRuntimeScalar.makeLocal) 2. Interpreter Backend: BytecodeCompiler used cached register for our variables instead of loading from global table Fixes: - 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 This fixes Carp::longmess() with $Carp::CarpLevel, enabling proper stack trace filtering for Log::Log4perl %T format. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../backend/bytecode/BytecodeCompiler.java | 41 ++++++++++++++++--- .../backend/jvm/EmitOperatorLocal.java | 15 +++++-- 2 files changed, 48 insertions(+), 8 deletions(-) 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, From 95676bcc5ebdb9a38632799eb3610c810dce7ad3 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 19 Mar 2026 15:22:08 +0100 Subject: [PATCH 4/7] Update design doc with local $OurVariable fix Log::Log4perl compatibility improved from 10/700 to 8/700 failing subtests. t/022Wrap.t now passes - %T stack trace format works correctly. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- dev/design/log4perl-compatibility.md | 40 +++++++++++++--------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/dev/design/log4perl-compatibility.md b/dev/design/log4perl-compatibility.md index 6d0c5da1f..70355cf68 100644 --- a/dev/design/log4perl-compatibility.md +++ b/dev/design/log4perl-compatibility.md @@ -10,18 +10,17 @@ This document tracks the work needed to make `./jcpan Log::Log4perl` fully pass ``` Files=73, Tests=700 -Failed 5/73 test programs -Failed 10/700 subtests +Failed 4/73 test programs +Failed 8/700 subtests ``` -**Improvement from previous:** Was 11/700 subtests failing. Fixed eval block name in caller(). +**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 - @CARP_NOT handling | | 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,6 +32,7 @@ Failed 10/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 | 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 @@ -189,23 +189,22 @@ BEGIN failed--compilation aborted at -e line 1, near "" **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 @@ -402,7 +401,7 @@ For chmod/umask: ## Progress Tracking -### Current Status: 10/700 subtests failing (was 11/700) +### Current Status: 8/700 subtests failing (was 10/700) ### Completed - [x] *{NAME} glob slot accessor (2026-03-18) @@ -414,18 +413,17 @@ For chmod/umask: - [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 -- [ ] %T stack trace format (2 tests) - needs @CARP_NOT handling - [ ] DESTROY during global destruction (1 test) - [ ] chmod/file permissions (3 tests) - [ ] Safe.pm restrictions (3 tests) - [ ] Source filters (1 test) ### Next Steps -1. Implement @CARP_NOT handling in Carp.pm for %T format (2 tests) -2. Investigate DESTROY during global destruction -3. Investigate chmod/file permissions issue +1. Investigate DESTROY during global destruction +2. Investigate chmod/file permissions issue --- From 57265e3fed455fface9d208ae4b399d06994923a Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 19 Mar 2026 15:41:16 +0100 Subject: [PATCH 5/7] Apply umask to mkdir permissions - Perl compatibility fix mkdir() now applies the process umask to the specified mode, matching Perl behavior where effective_mode = mode & ~umask. This fixes Log::Log4perl t/026FileApp.t test 25 which creates directories with mkpath_umask settings. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- src/main/java/org/perlonjava/core/Configuration.java | 2 +- .../java/org/perlonjava/runtime/operators/Directory.java | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 1e284c6e7..a9a24bd49 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 = "ffe3ac00f"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). 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 From c210383cc211b22086cd41faf41fc994821fb688 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 19 Mar 2026 15:54:15 +0100 Subject: [PATCH 6/7] Fix local *GLOB semantics for captured file handles When assigning a glob to a scalar (my $fh = *FH), create a detached copy that shares the current IO slot reference. This ensures that: 1. Captured globs inside a 'local *GLOB' scope retain their IO after the local scope ends (Perl behavior) 2. Multiple file handles from 'local *FH; open FH, ...; return *FH' remain independent and usable Also fix the IO slot save/restore in local to replace the object reference instead of modifying in place. RuntimeStashEntry overrides createDetachedCopy() to return itself, preserving ref() = '' behavior for stash entries. Fixes Log::Log4perl t/026FileApp.t tests 6-7 (file appender with multiple simultaneous log files). Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../runtime/runtimetypes/RuntimeGlob.java | 45 ++++++++++++++++--- .../runtime/runtimetypes/RuntimeScalar.java | 7 ++- .../runtimetypes/RuntimeStashEntry.java | 11 +++++ 3 files changed, 57 insertions(+), 6 deletions(-) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 913f11e84..dee7721e3 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); } @@ -612,22 +639,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 +683,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). From 964267400b466666af4bdddbc52fa25ffbca4232 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Thu, 19 Mar 2026 16:13:41 +0100 Subject: [PATCH 7/7] Fix detached glob IO sharing - setIO handles tied handles When a glob is assigned to a scalar ($_ = *foo), a detached copy is created that shares the same IO slot. Previously, setIO would replace the IO slot entirely, breaking this sharing. Now setIO modifies the slot in place (type and value) to preserve sharing, BUT if the IO slot is tied (TIED_SCALAR with TieHandle), it replaces the slot entirely. This prevents the TiedVariableBase cast error when opening through a PVLV after the original glob was tied. Key fix: setIO(RuntimeIO) now correctly sets type to GLOB (not REFERENCE). Fixes test 188 in uni/gv.t (PVLV: sv_2io stringifieth not). Preserves test 193 (PVLV can be the first arg to open). Restores op/filetest.t and op/postfixderef.t tests. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin --- .../org/perlonjava/core/Configuration.java | 2 +- .../runtime/runtimetypes/RuntimeGlob.java | 18 ++++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index a9a24bd49..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 = "ffe3ac00f"; + 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/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index dee7721e3..df591143d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -392,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; @@ -403,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; }