Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 31 additions & 31 deletions dev/design/log4perl-compatibility.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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

Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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

---

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)) {
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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());
Expand Down
15 changes: 12 additions & 3 deletions src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java
Original file line number Diff line number Diff line change
Expand Up @@ -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("<anon>");
// Note: "(eval)" is a special name for eval blocks which should capture variables like anonymous subs
boolean isPackageSub = node.name != null && !node.name.equals("<anon>") && !node.name.equals("(eval)");
if (isPackageSub) {
// Package subs should not capture any closure variables
// They can only access global variables and their parameters
Expand Down Expand Up @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
13 changes: 11 additions & 2 deletions src/main/java/org/perlonjava/frontend/parser/OperatorParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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<PosixFilePermission> permissions = getPosixFilePermissions(mode);
// Apply umask to the mode (Perl: effective_mode = mode & ~umask)
int effectiveMode = applyUmask(mode);
Set<PosixFilePermission> permissions = getPosixFilePermissions(effectiveMode);
Files.setPosixFilePermissions(path, permissions);
}
// On Windows and other non-POSIX systems, permissions are handled by the OS
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ private static ArrayList<ArrayList<String>> 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;
}

Expand All @@ -180,7 +181,8 @@ private static ArrayList<ArrayList<String>> 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;
}

Expand Down
Loading
Loading