Skip to content

Commit 8528172

Browse files
authored
Merge pull request #968 from fglock/fix/cpan-compiler-tooling-batch
Fix jcpan compiler and tooling blockers
2 parents c8182f2 + 0b9fa26 commit 8528172

16 files changed

Lines changed: 730 additions & 16 deletions

File tree

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
# jcpan Compiler and Tooling Batch
2+
3+
## Goal
4+
5+
Remove reusable compiler/runtime/tooling blockers found while testing:
6+
7+
- `Dist::Zilla::Plugin::Manifest::Read`
8+
- `Plack::App::Proxy::Selective`
9+
- `Text::MetaText`
10+
- `Test::Class::Filter::Tags`
11+
- `Catmandu::Exporter::HTML`
12+
- `Map::Tube::Sydney`
13+
- `App::SQLiteUtils`
14+
- `Char::Latin2`
15+
16+
Distributions which also fail under system Perl are recorded and excluded from
17+
PerlOnJava-specific fixes.
18+
19+
## Progress Tracking
20+
21+
### Current Status: implementation complete (2026-08-16)
22+
23+
### Completed Phases
24+
25+
- [x] Phase 1: System-Perl eligibility baseline (2026-08-15)
26+
- `Dist::Zilla::Plugin::Manifest::Read` passes system Perl.
27+
- `Plack::App::Proxy::Selective`, `Text::MetaText`,
28+
`Test::Class::Filter::Tags`, `Catmandu::Exporter::HTML`, and
29+
`Map::Tube::Sydney` fail in system Perl or in their prerequisite closure and
30+
require no PerlOnJava-specific distribution workaround.
31+
- `App::SQLiteUtils` is excluded because its required system-Perl closure
32+
fails in Data::Sah/lib::filter and cannot provide a valid comparison.
33+
- `Char::Latin2` passes system Perl (210 files / 5,725 tests).
34+
35+
- [x] Phase 2: Shared Dist::Zilla blockers (2026-08-15)
36+
- Added structural translation for supported `(*PRUNE)` regex forms.
37+
- Made the Unix and Windows launchers cache and reuse an absolute Java path,
38+
allowing nested `$^X` calls after build tools replace `PATH`.
39+
- Preserved Perl's `((), @array)` iteration-list snapshot idiom in the parser
40+
and both execution backends.
41+
- Made direct array iteration follow the live array length when the loop body
42+
mutates its structure.
43+
- `Dist::Zilla::Plugin::Manifest::Read` now passes 2 files / 8 tests with its
44+
complete CPAN prerequisite path.
45+
- Files: `jperl`, `jperl.bat`, `RegexPreprocessor.java`, `ParseInfix.java`,
46+
`EmitLiteral.java`, `BytecodeCompiler.java`, `InlineOpcodeHandler.java`,
47+
`RuntimeArray.java`, `RuntimeList.java`, and focused tests.
48+
49+
- [x] Phase 3: Char::Latin2 compatibility and final validation (2026-08-16)
50+
- Reused the established Char::Latin7/Char::Windows1258 compatibility design
51+
for the historical `jperl` executable-name collision and Perl regex-code
52+
source transformer.
53+
- Registered the generic patch through CPAN bootstrap configuration; no
54+
installed-user preference edits are required.
55+
- `Char::Latin2` passes PerlOnJava (210 files / 5,725 tests).
56+
- A clean `jcpan -t Dist::Zilla::Plugin::Manifest::Read` passes (2 files / 8
57+
tests) and exits successfully.
58+
- Full `make` passes.
59+
60+
### Next Steps
61+
62+
1. Monitor pull-request CI.
63+
2. Merge after review when CI is green.
64+
65+
### Open Questions
66+
67+
- None.
68+
69+
## Related Skills and Documents
70+
71+
- `.agents/skills/debug-perlonjava/SKILL.md`
72+
- `AGENTS.md`

jperl

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,15 @@
66
# Repository: github.com/fglock/PerlOnJava
77
#
88

9-
# Get the directory where this script is located, resolving symlinks
10-
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
9+
# Get the directory where this script is located without depending on the
10+
# external dirname utility. CPAN build tools can intentionally replace PATH
11+
# with prerequisite script directories before launching a nested $^X.
12+
SCRIPT_SOURCE="${BASH_SOURCE[0]}"
13+
case "$SCRIPT_SOURCE" in
14+
*/*) SCRIPT_PARENT="${SCRIPT_SOURCE%/*}" ;;
15+
*) SCRIPT_PARENT="." ;;
16+
esac
17+
SCRIPT_DIR="$(cd "$SCRIPT_PARENT" && pwd -P)"
1118

1219
# Get the full path to this script to set $^X correctly
1320
JPERL_PATH="$(readlink -f "${BASH_SOURCE[0]}" 2>/dev/null || echo "$SCRIPT_DIR/jperl")"
@@ -58,11 +65,24 @@ JVM_OPTS="--enable-native-access=ALL-UNNAMED"
5865

5966
# Java 23+ warns about sun.misc.Unsafe usage (JEP 471). Add flag to suppress
6067
# warnings from transitive libraries (ASM, ICU4J, etc.) that still use it.
61-
if ! command -v java >/dev/null 2>&1; then
68+
JAVA_BIN="${PERLONJAVA_JAVA_BIN:-}"
69+
if [ -z "$JAVA_BIN" ] && [ -n "${JAVA_HOME:-}" ] && [ -x "$JAVA_HOME/bin/java" ]; then
70+
JAVA_BIN="$JAVA_HOME/bin/java"
71+
fi
72+
if [ -z "$JAVA_BIN" ]; then
73+
JAVA_BIN="$(command -v java 2>/dev/null || true)"
74+
fi
75+
if [ -z "$JAVA_BIN" ] || [ ! -x "$JAVA_BIN" ]; then
6276
echo "ERROR: PerlOnJava requires Java 24 or later; java was not found in PATH." >&2
6377
exit 1
6478
fi
65-
JAVA_VERSION=$(java -version 2>&1 | head -1 | sed 's/.*version "\([0-9]*\).*/\1/')
79+
export PERLONJAVA_JAVA_BIN="$JAVA_BIN"
80+
JAVA_VERSION_OUTPUT="$("$JAVA_BIN" -version 2>&1)"
81+
if [[ "$JAVA_VERSION_OUTPUT" =~ version\ \"([0-9]+) ]]; then
82+
JAVA_VERSION="${BASH_REMATCH[1]}"
83+
else
84+
JAVA_VERSION=""
85+
fi
6686
if ! [[ "$JAVA_VERSION" =~ ^[0-9]+$ ]] || [ "$JAVA_VERSION" -lt 24 ]; then
6787
echo "ERROR: PerlOnJava requires Java 24 or later (found: ${JAVA_VERSION:-unknown})." >&2
6888
exit 1
@@ -77,4 +97,4 @@ if [ -n "$CLASSPATH" ]; then
7797
else
7898
CP="$PERLONJAVA_CP"
7999
fi
80-
exec java $JVM_OPTS ${JPERL_OPTS} -cp "$CP" org.perlonjava.app.cli.Main "$@"
100+
exec "$JAVA_BIN" $JVM_OPTS ${JPERL_OPTS} -cp "$CP" org.perlonjava.app.cli.Main "$@"

jperl.bat

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,24 @@ rem Override via `JPERL_OPTS=-Xmx<size>` in the environment if needed.
3232

3333
rem Java 23+ warns about sun.misc.Unsafe usage (JEP 471). Add flag to suppress
3434
rem warnings from transitive libraries (ASM, ICU4J, etc.) that still use it.
35+
if not defined PERLONJAVA_JAVA_BIN (
36+
for /f "delims=" %%j in ('where java 2^>nul') do if not defined PERLONJAVA_JAVA_BIN set "PERLONJAVA_JAVA_BIN=%%j"
37+
)
38+
if not defined PERLONJAVA_JAVA_BIN (
39+
echo ERROR: PerlOnJava requires Java 24 or later; java was not found in PATH. 1>&2
40+
exit /b 1
41+
)
3542
set JAVA_VERSION=
36-
for /f "tokens=3" %%v in ('java -version 2^>^&1 ^| findstr /i "version"') do (
43+
rem CALL is required when a quoted absolute executable starts the command
44+
rem parsed by FOR /F; without it cmd.exe treats the nested quote as syntax.
45+
for /f "tokens=3" %%v in ('call "%PERLONJAVA_JAVA_BIN%" -version 2^>^&1') do if not defined JAVA_VERSION (
3746
for /f "tokens=1 delims=." %%m in ("%%~v") do (
3847
set JAVA_VERSION=%%m
3948
if %%m GEQ 23 set JVM_OPTS=%JVM_OPTS% --sun-misc-unsafe-memory-access=allow
4049
)
4150
)
4251
if not defined JAVA_VERSION (
43-
echo ERROR: PerlOnJava requires Java 24 or later; java was not found in PATH. 1>&2
52+
echo ERROR: PerlOnJava requires Java 24 or later; Java version could not be determined. 1>&2
4453
exit /b 1
4554
)
4655
if %JAVA_VERSION% LSS 24 (
@@ -63,4 +72,4 @@ if exist "%SCRIPT_DIR%target\perlonjava-5.44.0.jar" (
6372
)
6473

6574
rem Launch Java
66-
java %JVM_OPTS% %JPERL_OPTS% -cp "%CLASSPATH%;%PERLONJAVA_CP%" org.perlonjava.app.cli.Main %*
75+
"%PERLONJAVA_JAVA_BIN%" %JVM_OPTS% %JPERL_OPTS% -cp "%CLASSPATH%;%PERLONJAVA_CP%" org.perlonjava.app.cli.Main %*

src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5554,7 +5554,11 @@ public void visit(ArrayLiteralNode node) {
55545554
int listReg = allocateRegister();
55555555
emit(Opcodes.CREATE_LIST);
55565556
emitReg(listReg);
5557-
emit(node.elements.size()); // count
5557+
// A negative encoded count asks CREATE_LIST to flatten aggregate
5558+
// membership immediately. This preserves Perl's `((), @array)` copy
5559+
// idiom without cloning the scalar aliases themselves.
5560+
boolean forceListSnapshot = Boolean.TRUE.equals(node.getAnnotation("forceListSnapshot"));
5561+
emit(forceListSnapshot ? -node.elements.size() - 1 : node.elements.size()); // count
55585562

55595563
// Emit register numbers for each element
55605564
for (int elemReg : elementRegs) {
@@ -6986,7 +6990,7 @@ public void visit(ListNode node) {
69866990
int listReg = allocateRegister();
69876991
emit(Opcodes.CREATE_LIST);
69886992
emitReg(listReg);
6989-
emit(1); // count = 1
6993+
emit(Boolean.TRUE.equals(node.getAnnotation("forceListSnapshot")) ? -2 : 1); // count = 1
69906994
emitReg(elemReg);
69916995
lastResultReg = listReg;
69926996
return;

src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -904,6 +904,10 @@ public static int executeScalarToList(int[] bytecode, int pc, RuntimeBase[] regi
904904
public static int executeCreateList(int[] bytecode, int pc, RuntimeBase[] registers) {
905905
int rd = bytecode[pc++];
906906
int count = bytecode[pc++];
907+
boolean forceListSnapshot = count < 0;
908+
if (forceListSnapshot) {
909+
count = -count - 1;
910+
}
907911

908912
if (count == 0) {
909913
// Empty list - fastest path
@@ -912,15 +916,23 @@ public static int executeCreateList(int[] bytecode, int pc, RuntimeBase[] regist
912916
// Single element - avoid loop overhead
913917
int rs = bytecode[pc++];
914918
RuntimeList list = new RuntimeList();
915-
list.add(registers[rs]);
919+
if (forceListSnapshot) {
920+
list.addSnapshot(registers[rs]);
921+
} else {
922+
list.add(registers[rs]);
923+
}
916924
registers[rd] = list;
917925
} else {
918926
// Multiple elements - preallocate and populate
919927
RuntimeList list = new RuntimeList();
920928

921929
for (int i = 0; i < count; i++) {
922930
int rs = bytecode[pc++];
923-
list.add(registers[rs]);
931+
if (forceListSnapshot) {
932+
list.addSnapshot(registers[rs]);
933+
} else {
934+
list.add(registers[rs]);
935+
}
924936
}
925937

926938
registers[rd] = list;

src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,6 +512,7 @@ public static void emitList(EmitterVisitor emitterVisitor, ListNode node) {
512512
// Stack: []
513513

514514
// Populate the list with elements
515+
boolean forceListSnapshot = Boolean.TRUE.equals(node.getAnnotation("forceListSnapshot"));
515516
for (Node element : node.elements) {
516517
// Generate code for the element with an empty operand stack so non-local control flow
517518
// cannot leak extra operands.
@@ -524,7 +525,12 @@ public static void emitList(EmitterVisitor emitterVisitor, ListNode node) {
524525
emitterVisitor.ctx.javaClassInfo.releaseSpillRef(elementRef);
525526

526527
// Add the element to the list
527-
addElementToList(mv, element, contextType);
528+
if (forceListSnapshot) {
529+
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, RuntimeDescriptorConstants.LIST_CLASS,
530+
"addSnapshot", "(" + RuntimeDescriptorConstants.BASE_TYPE + ")V", false);
531+
} else {
532+
addElementToList(mv, element, contextType);
533+
}
528534
// Stack: []
529535
}
530536

src/main/java/org/perlonjava/frontend/parser/ParseInfix.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,9 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence)
206206
switch (token.text) {
207207
case ",":
208208
case "=>":
209+
boolean forceListSnapshot = token.text.equals(",")
210+
&& left instanceof ListNode leftList
211+
&& leftList.elements.isEmpty();
209212
if (token.text.equals("=>") && left instanceof IdentifierNode) {
210213
// Autoquote - Convert IdentifierNode to StringNode.
211214
// Strip trailing "::" so that `Foo::Bar:: => ...` autoquotes to "Foo::Bar",
@@ -222,7 +225,15 @@ public static Node parseInfixOperation(Parser parser, Node left, int precedence)
222225
return ListNode.makeList(left);
223226
}
224227
right = parser.parseExpression(precedence);
225-
return ListNode.makeList(left, right);
228+
ListNode combined = ListNode.makeList(left, right);
229+
if (forceListSnapshot) {
230+
// Perl code uses `((), @array)` to force a structural copy of
231+
// the iteration list while retaining aliases to its scalar
232+
// elements. Do not erase that distinction when flattening
233+
// the comma expression into a ListNode.
234+
combined.setAnnotation("forceListSnapshot", true);
235+
}
236+
return combined;
226237
case "?":
227238
// Handle ternary operator
228239
Node middle = parser.parseExpression(0);

0 commit comments

Comments
 (0)