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
123 changes: 123 additions & 0 deletions dev/modules/excel_writer_xlsx.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Excel::Writer::XLSX Fix Plan

## Overview

**Module**: Excel::Writer::XLSX 1.15
**Test command**: `./jcpan -j 8 -t Excel::Writer::XLSX`
**Status**: 1247/1247 programs pass (100%), 5115/5115 subtests pass (100%)

## Dependency Tree

| Dependency | Status | Notes |
|-----------|--------|-------|
| **Archive::Zip** >= 1.30 | PASS | Java-backed impl; `setErrorHandler` stub added, `use FileHandle ()` added |
| **File::Temp** >= 0.19 | PASS | Core module, works |
| **IO::File** >= 1.14 | PASS | Core module, works |

## Test Results Summary

### Current Status: 1247/1247 programs pass, 5115/5115 subtests pass (100%)

Tests are in subdirectories: `t/chart/`, `t/chartsheet/`, `t/drawing/`, `t/package/`, `t/regression/`, `t/utility/`, `t/workbook/`, `t/worksheet/`

| Test Group | Total | Pass | Fail | Notes |
|-----------|-------|------|------|-------|
| t/chart/ | ~47 | 47 | 0 | All pass |
| t/chartsheet/ | 4 | 4 | 0 | All pass (password tests fixed) |
| t/drawing/ | ~23 | 23 | 0 | All pass |
| t/package/ | ~50 | 50 | 0 | All pass |
| t/regression/ | ~800+ | ~800+ | 0 | All pass |
| t/utility/ | ~15 | 15 | 0 | All pass (quote_sheetname fixed) |
| t/workbook/ | ~18 | 18 | 0 | All pass |
| t/worksheet/ | ~90 | 90 | 0 | All pass |

---

## Completed Phases

### Phase 0: Fix glob() and MakeMaker (COMPLETED 2026-04-03)

| Step | Description | File | Status |
|------|-------------|------|--------|
| 0.1 | Add recursive glob expansion for directory wildcards | `ScalarGlobOperator.java` | DONE |
| 0.2 | Extract and use `test => { TESTS => ... }` parameter | `ExtUtils/MakeMaker.pm` | DONE |
| 0.3 | Verify `glob("t/*/*.t")` returns 1152 files | - | DONE |
| 0.4 | Verify `make` passes | - | DONE |

**Result**: `jcpan -t` now discovers and runs all 1247 test files instead of 0.

### Phase 1: Implement missing Archive::Zip features (COMPLETED 2026-04-03)

| Step | Description | File | Status |
|------|-------------|------|--------|
| 1.1 | Add `setErrorHandler` as stub function | `Archive/Zip.pm` | DONE |
| 1.2 | Add `use FileHandle ()` to match CPAN Archive::Zip | `Archive/Zip.pm` | DONE |
| 1.3 | Verify regression tests unblocked | - | DONE |

**Result**: Unblocked all ~800+ regression tests + 133 image/hyperlink tests that needed FileHandle.

### Phase 2: Fix `\p{Emoticons}` regex support (COMPLETED 2026-04-03)

| Step | Description | File | Status |
|------|-------------|------|--------|
| 2.1 | Add `\p{}`/`\P{}` translation inside character classes | `RegexPreprocessorHelper.java` | DONE |
| 2.2 | Add Emoticons Unicode block mapping | `UnicodeResolver.java` | DONE |
| 2.3 | Verify regex `[^\w\.\p{Emoticons}]` compiles and matches | - | DONE |

**Result**: Emoticons regex property works in patterns and character classes.

### Phase 3: Fix split scalar context for password hash (COMPLETED 2026-04-03)

| Step | Description | File | Status |
|------|-------------|------|--------|
| 3.1 | Fix JVM backend: `emitSplitArgs` with SCALAR context | `EmitOperator.java` | DONE |
| 3.2 | Fix interpreter backend: split special case | `CompileBinaryOperator.java` | DONE |
| 3.3 | Verify `split //, reverse $str` matches Perl | - | DONE |

**Result**: Password hash tests pass. `split //, reverse $str` correctly reverses the string.

### Phase 4: Fix regex Unicode semantics for UTF-8 Latin-1 strings (COMPLETED 2026-04-03)

| Step | Description | File | Status |
|------|-------------|------|--------|
| 4.1 | Use Unicode regex pattern for all UTF-8 strings (not just >U+00FF) | `RuntimeRegex.java` | DONE |
| 4.2 | Verify `\w` matches `é` with `use utf8` | - | DONE |
| 4.3 | Run uni/ tests to verify no regressions | - | DONE |

**Result**: `quote_sheetname.t` all 100 tests pass. Latin-1 accented chars match `\w` when UTF-8 flagged.

### Phase 5: Fix `open '>' \$scalar` to preserve undef (COMPLETED 2026-04-03)

| Step | Description | File | Status |
|------|-------------|------|--------|
| 5.1 | Identify root cause: `open '>', \$scalar` sets undef to `''` | `RuntimeIO.java` | DONE |
| 5.2 | Fix: only truncate if scalar was already defined | `RuntimeIO.java` | DONE |
| 5.3 | Verify `open '>', \$undef_var` keeps undef (matches Perl) | - | DONE |
| 5.4 | Verify `open '>', \$defined_var` truncates to `''` (matches Perl) | - | DONE |
| 5.5 | Run `make` to verify unit tests pass | - | DONE |
| 5.6 | Run full Excel::Writer::XLSX test suite | - | DONE |

**Result**: All 1247 programs pass, all 5115 subtests pass. 100% pass rate achieved.

---

## Summary

| Phase | Description | Tests fixed | Status |
|-------|-----------|------------|--------|
| 0 | glob() + MakeMaker | ALL (1247 discovered) | COMPLETED |
| 1 | Archive::Zip (setErrorHandler + FileHandle) | ~933 programs | COMPLETED |
| 2 | `\p{Emoticons}` regex | ~20+ tests | COMPLETED |
| 3 | split scalar context (password hash) | 3 subtests | COMPLETED |
| 4 | regex Unicode semantics (Latin-1 \w) | 2 subtests | COMPLETED |
| 5 | `open '>' \$scalar` undef preservation | 3 programs, 5 subtests | COMPLETED |

## Branch & PR

- Branch: `fix/glob-directory-wildcards`
- PR: https://github.com/fglock/PerlOnJava/pull/430

## Related Documents

- `dev/modules/spreadsheet_parseexcel.md` -- similar module fix plan
- `dev/modules/makemaker_perlonjava.md` -- MakeMaker implementation details
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,40 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
}
}

// Handle split specially: each argument (EXPR, LIMIT) should be in SCALAR context,
// but the result is assembled into a list for the SPLIT opcode.
// This ensures `split //, reverse $str` evaluates `reverse` in scalar context
// (string reverse) not list context (list reverse).
if (node.operator.equals("split")) {
bytecodeCompiler.compileNode(node.left, -1, RuntimeContextType.SCALAR);
int rs1 = bytecodeCompiler.lastResultReg;

int rs2;
if (node.right instanceof ListNode listNode && !listNode.elements.isEmpty()) {
// Compile each element in SCALAR context, then assemble into a list
java.util.List<Integer> argRegs = new java.util.ArrayList<>();
for (Node element : listNode.elements) {
bytecodeCompiler.compileNode(element, -1, RuntimeContextType.SCALAR);
argRegs.add(bytecodeCompiler.lastResultReg);
}
rs2 = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.CREATE_LIST);
bytecodeCompiler.emitReg(rs2);
bytecodeCompiler.emit(argRegs.size());
for (int argReg : argRegs) {
bytecodeCompiler.emitReg(argReg);
}
} else {
bytecodeCompiler.compileNode(node.right, -1, RuntimeContextType.SCALAR);
rs2 = bytecodeCompiler.lastResultReg;
}

int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(
bytecodeCompiler, node.operator, rs1, rs2, node.getIndex());
bytecodeCompiler.lastResultReg = rd;
return;
}

// Compile left and right operands (for non-short-circuit operators).
// For arithmetic/bitwise operators, force SCALAR context to prevent
// parenthesized expressions from producing RuntimeList in LIST context.
Expand Down
57 changes: 55 additions & 2 deletions src/main/java/org/perlonjava/backend/jvm/EmitOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,9 @@ static void handleSubstr(EmitterVisitor emitterVisitor, BinaryOperatorNode node)
// Handles the 'split' operator
static void handleSplit(EmitterVisitor emitterVisitor, BinaryOperatorNode node) {
// Accept the left operand in SCALAR context and the right operand in LIST context.
// IMPORTANT: split's EXPR argument (the string to split) must be evaluated in
// SCALAR context per Perl semantics. This matters for functions like `reverse`
// which behave differently in list vs scalar context.
// Spill the left operand before evaluating the right side so non-local control flow
// propagation can't jump to returnLabel with an extra value on the JVM operand stack.
if (ENABLE_SPILL_BINARY_LHS) {
Expand All @@ -792,7 +795,11 @@ static void handleSplit(EmitterVisitor emitterVisitor, BinaryOperatorNode node)
}
mv.visitVarInsn(Opcodes.ASTORE, leftSlot);

node.right.accept(emitterVisitor.with(RuntimeContextType.LIST));
// Evaluate the right side (EXPR [, LIMIT]) - each argument in SCALAR context
// but produce a RuntimeList for the split runtime method.
// We use SCALAR context here because split's EXPR must be in scalar context
// (e.g., `reverse $str` should reverse the string, not reverse a list).
emitSplitArgs(emitterVisitor, node.right);

mv.visitVarInsn(Opcodes.ALOAD, leftSlot);
mv.visitInsn(Opcodes.SWAP);
Expand All @@ -802,12 +809,58 @@ static void handleSplit(EmitterVisitor emitterVisitor, BinaryOperatorNode node)
}
} else {
node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
node.right.accept(emitterVisitor.with(RuntimeContextType.LIST));
emitSplitArgs(emitterVisitor, node.right);
}
emitterVisitor.pushCallContext();
emitOperator(node, emitterVisitor);
}

/**
* Emits split's argument list, evaluating each element in SCALAR context
* but producing a RuntimeList result. This ensures that expressions like
* `reverse $str` are evaluated in scalar context (string reverse) not
* list context (list reverse).
*/
private static void emitSplitArgs(EmitterVisitor emitterVisitor, Node argsNode) {
if (argsNode instanceof ListNode listNode && !listNode.elements.isEmpty()) {
EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR);
MethodVisitor mv = emitterVisitor.ctx.mv;

// Create a new RuntimeList and store in a spill slot
mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/runtimetypes/RuntimeList");
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKESPECIAL,
"org/perlonjava/runtime/runtimetypes/RuntimeList", "<init>", "()V", false);

JavaClassInfo.SpillRef listRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable);
emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, listRef);

// Add each argument evaluated in SCALAR context
for (Node element : listNode.elements) {
element.accept(scalarVisitor);
JavaClassInfo.SpillRef elemRef = emitterVisitor.ctx.javaClassInfo.acquireSpillRefOrAllocate(emitterVisitor.ctx.symbolTable);
emitterVisitor.ctx.javaClassInfo.storeSpillRef(mv, elemRef);

emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, listRef);
emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, elemRef);
emitterVisitor.ctx.javaClassInfo.releaseSpillRef(elemRef);

// RuntimeList.add(RuntimeBase)
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL,
"org/perlonjava/runtime/runtimetypes/RuntimeList", "add",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;)V",
false);
}

// Load the completed list onto the stack
emitterVisitor.ctx.javaClassInfo.loadSpillRef(mv, listRef);
emitterVisitor.ctx.javaClassInfo.releaseSpillRef(listRef);
} else {
// Fallback: evaluate as LIST (no elements or not a ListNode)
argsNode.accept(emitterVisitor.with(RuntimeContextType.LIST));
}
}

// Handles the 'repeat' operator, which repeats a string or list a specified number of times.
static void handleRepeat(EmitterVisitor emitterVisitor, BinaryOperatorNode node) {
MethodVisitor mv = emitterVisitor.ctx.mv;
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 = "1b7317697";
public static final String gitCommitId = "7a045da61";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
Loading
Loading