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
78 changes: 78 additions & 0 deletions dev/modules/cpan_tooling_batch_20260816_biox.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# CPAN compiler/tooling batch: BioX and related modules

## Goal

Make the requested `jcpan -t` targets use reusable PerlOnJava compiler,
runtime, and CPAN-tooling behavior. Distribution preferences are not used as
the primary fix.

## Progress Tracking

### Current Status: complete

### Completed Phases

- [x] Baseline classification (2026-08-16)
- `Template::Plugin::Markdown` already passes.
- `BioX::Seq` failed because overloaded `.=` replaced the blessed object.
- `Math::Aronson` failed because boolean operands took the string XOR path.
- `Changes` failed in the `Wanted`/locale dependency boundary.
- `Catmandu::Importer::Parltrack` failed while `Class::XSAccessor` used
optional `Sub::Util::set_subname` metadata.
- `Nephia::Plugin::FormValidator::Lite` cannot resolve its `Nephia::Plugin`
and `Plack::Test` dependencies; the isolated system Perl also lacks those
modules, so it is unsupported under the requested rule.
- [x] Reusable compiler/runtime fixes
- Preserve overloaded object results for compound string concatenation.
- Treat boolean scalars as numeric operands for `^`.
- Implement `no overloading` dispatch for arithmetic and boolean contexts.
- Permit guarded fully-qualified optional constant names under `strict subs`.
- Compile fully-qualified `CORE::` control-flow and infix operations.
- Preserve caller context, including `OBJECT`, across JVM and interpreter
wrapper frames and non-local returns.
- Apply case folding to POSIX regex character classes.
- [x] CPAN tooling compatibility
- Reinitialize Java-backed XS modules when an isolated CPAN worker has stale
`%INC` state but an undefined stash entry.
- Snapshot named coderefs at compile time instead of retaining mutable stash
scalars, fixing generated `Eval::TypeTiny` callbacks.
- Add reusable Java-module loading and the `Wanted` context compatibility
surface.
- Expose the bundled SQLite JDBC implementation through DBD::SQLite metadata
and constants instead of introducing another native dependency.
- Complete reusable Fcntl and POSIX constants/functions required by the
dependency graph.
- [x] Unit validation
- All ten new compatibility tests were validated with system Perl: eight
passed and two correctly skipped when their optional modules were absent.
- Final `make` passed in 3m43s after all compiler and tooling changes.
- [x] Target verification
- `BioX::Seq`: 7 files, 127 tests, PASS.
- `Changes`: 26 files, 375 tests, PASS.
- `Math::Aronson`: 4 files, 55 tests, PASS.
- `Catmandu::Importer::Parltrack`: 3 tests, PASS.
- `Template::Plugin::Markdown`: 2 files, 3 tests, PASS.
- `Nephia::Plugin::FormValidator::Lite`: unsupported under the requested
system-Perl exception. Its own system-Perl suite fails, and the current
CPAN package index no longer contains Nephia or three declared companion
plugins. MetaCPAN identifies those releases as BackPAN-only.
- [x] Pull request verification (2026-08-16)
- PR #979 contains the complete compiler, runtime, CPAN tooling, and test
changes.
- GitHub Actions passed on Windows in 18m55s.
- GitHub Actions passed on Ubuntu, including the pinned Perl thread
compatibility gate, in 23m12s.

### Next Steps

1. Review and merge PR #979.

### Open Questions

- Whether PerlOnJava should eventually add an opt-in BackPAN resolver for
distributions whose live CPAN metadata still declares withdrawn modules.

## Related References

- [`debug-perlonjava`](../../.agents/skills/debug-perlonjava/SKILL.md)
- [`AGENTS.md`](../../AGENTS.md)
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import org.perlonjava.runtime.debugger.DebugState;
import org.perlonjava.runtime.perlmodule.Attributes;
import org.perlonjava.runtime.perlmodule.Strict;
import org.perlonjava.runtime.perlmodule.XSLoader;
import org.perlonjava.runtime.runtimetypes.*;

import java.math.BigInteger;
Expand Down Expand Up @@ -689,6 +690,14 @@ boolean isNoOverloadingEnabled() {
return getEffectiveSymbolTable().isStrictOptionEnabled(Strict.HINT_NO_AMAGIC);
}

short gotoIfFalseOpcode() {
return isNoOverloadingEnabled() ? Opcodes.GOTO_IF_FALSE_NO_OVERLOAD : Opcodes.GOTO_IF_FALSE;
}

short gotoIfTrueOpcode() {
return isNoOverloadingEnabled() ? Opcodes.GOTO_IF_TRUE_NO_OVERLOAD : Opcodes.GOTO_IF_TRUE;
}

boolean shouldBlockGlobalUnderStrictVars(String varName) {
boolean strictEnabled = getEffectiveSymbolTable().isStrictOptionEnabled(Strict.HINT_STRICT_VARS);
if (!strictEnabled) {
Expand Down Expand Up @@ -1756,7 +1765,14 @@ public void visit(IdentifierNode node) {
return;
}
// This is a bareword (no sigil)
if (getEffectiveSymbolTable().isStrictOptionEnabled(Strict.HINT_STRICT_SUBS)) {
// A fully-qualified all-caps name is commonly a constant
// supplied by an optional XS module. Perl parses it even
// when the guarded branch is disabled; do not reject the
// source merely because that optional module is absent.
boolean qualifiedConstant = varName.contains("::")
&& varName.matches(".*::[A-Z][A-Z0-9_]*");
if (getEffectiveSymbolTable().isStrictOptionEnabled(Strict.HINT_STRICT_SUBS)
&& !qualifiedConstant) {
throwCompilerException("Bareword \"" + varName + "\" not allowed while \"strict subs\" in use");
}
if (currentCallContext == RuntimeContextType.VOID) {
Expand Down Expand Up @@ -2577,19 +2593,19 @@ private void handleShortCircuitAssignment(BinaryOperatorNode node) {
emitReg(condReg);
emitReg(targetReg);
jumpPos = bytecode.size();
emit(Opcodes.GOTO_IF_TRUE);
emit(gotoIfTrueOpcode());
emitReg(condReg);
emitInt(0); // placeholder for end target
} else if (op.equals("||=")) {
// For ||=, skip RHS if LHS is truthy
jumpPos = bytecode.size();
emit(Opcodes.GOTO_IF_TRUE);
emit(gotoIfTrueOpcode());
emitReg(targetReg);
emitInt(0); // placeholder for end target
} else {
// For &&=, skip RHS if LHS is falsy
jumpPos = bytecode.size();
emit(Opcodes.GOTO_IF_FALSE);
emit(gotoIfFalseOpcode());
emitReg(targetReg);
emitInt(0); // placeholder for end target
}
Expand Down Expand Up @@ -4976,6 +4992,23 @@ void compileVariableReference(OperatorNode node, String op) {
if (codeRef == null) {
codeRef = GlobalVariable.getGlobalCodeRefForFreshLookup(subName);
}
if (codeRef.type == RuntimeScalarType.CODE
&& codeRef.value instanceof RuntimeCode unresolved
&& !unresolved.defined()) {
int separator = subName.lastIndexOf("::");
if (separator > 0
&& XSLoader.tryInitializeJavaModule(subName.substring(0, separator))) {
codeRef = GlobalVariable.getGlobalCodeRefForFreshLookup(subName);
}
}

// A compiled reference captures the current CV, not the mutable
// stash slot that points at it. namespace::clean can remove the
// glob later while existing \&name references remain callable.
RuntimeScalar codeSnapshot = new RuntimeScalar();
codeSnapshot.type = codeRef.type;
codeSnapshot.value = codeRef.value;
codeRef = codeSnapshot;

// Allocate register and load from constant pool
int rd = allocateOutputRegister();
Expand Down Expand Up @@ -6587,7 +6620,7 @@ public void visit(For3Node node) {
}

// Step 8: If condition is true, jump back to start
emit(Opcodes.GOTO_IF_TRUE);
emit(gotoIfTrueOpcode());
emitReg(condReg);
emitInt(loopStartPc);
}
Expand All @@ -6610,7 +6643,7 @@ public void visit(For3Node node) {
}

// Step 4: If condition is false, jump to end
emit(Opcodes.GOTO_IF_FALSE);
emit(gotoIfFalseOpcode());
emitReg(condReg);
loopEndJumpPc = bytecode.size();
emitInt(0); // Placeholder for jump target (will be patched)
Expand Down Expand Up @@ -6714,9 +6747,9 @@ public void visit(IfNode node) {
int ifFalsePos = bytecode.size();
// Invert condition for 'unless'
if ("unless".equals(node.operator)) {
emit(Opcodes.GOTO_IF_TRUE);
emit(gotoIfTrueOpcode());
} else {
emit(Opcodes.GOTO_IF_FALSE);
emit(gotoIfFalseOpcode());
}
emitReg(condReg);
emitInt(0); // Placeholder for else/end target
Expand Down Expand Up @@ -6777,7 +6810,7 @@ public void visit(TernaryOperatorNode node) {
int condReg = lastResultReg;

int ifFalsePos = bytecode.size();
emit(Opcodes.GOTO_IF_FALSE);
emit(gotoIfFalseOpcode());
emitReg(condReg);
emitInt(0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,32 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {
}
}

case Opcodes.GOTO_IF_FALSE_NO_OVERLOAD -> {
int condReg = bytecode[pc++];
int target = readInt(bytecode, pc);
pc += 1;
RuntimeBase condBase = registers[condReg];
RuntimeScalar cond = (condBase instanceof RuntimeScalar)
? (RuntimeScalar) condBase
: condBase.scalar();
if (!cond.getBooleanNoOverload()) {
pc = target;
}
}

case Opcodes.GOTO_IF_TRUE_NO_OVERLOAD -> {
int condReg = bytecode[pc++];
int target = readInt(bytecode, pc);
pc += 1;
RuntimeBase condBase = registers[condReg];
RuntimeScalar cond = (condBase instanceof RuntimeScalar)
? (RuntimeScalar) condBase
: condBase.scalar();
if (cond.getBooleanNoOverload()) {
pc = target;
}
}

// =================================================================
// REGISTER OPERATIONS
// =================================================================
Expand Down Expand Up @@ -1191,7 +1217,7 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {

case Opcodes.COMPARE_NUM, Opcodes.COMPARE_STR, Opcodes.EQ_NUM, Opcodes.NE_NUM,
Opcodes.LT_NUM, Opcodes.GT_NUM, Opcodes.LE_NUM, Opcodes.GE_NUM, Opcodes.EQ_STR,
Opcodes.NE_STR, Opcodes.NOT -> {
Opcodes.NE_STR, Opcodes.NOT, Opcodes.NOT_NO_OVERLOAD -> {
pc = executeComparisons(opcode, bytecode, pc, registers);
}

Expand Down Expand Up @@ -1504,6 +1530,8 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {
// resolve it from the actual calling context in register 2.
if (context == RuntimeContextType.RUNTIME) {
context = ((RuntimeScalar) registers[2]).getInt();
} else if (context == RuntimeContextType.INHERITED) {
context = RuntimeCode.currentRawCallContext();
}

// Auto-convert coderef to scalar if needed
Expand Down Expand Up @@ -1574,10 +1602,13 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {
CallerStack.pop();
}

// Convert to scalar if called in scalar or lvalue context
if (context == RuntimeContextType.SCALAR || context == RuntimeContextType.LVALUE) {
// OBJECT is a raw Wanted parent-op context, but its
// value semantics are the same as scalar context.
if (context == RuntimeContextType.SCALAR || context == RuntimeContextType.LVALUE
|| context == RuntimeContextType.OBJECT) {
RuntimeBase scalarResult = result.scalar();
registers[rd] = context == RuntimeContextType.SCALAR && isImmutableProxy(scalarResult)
registers[rd] = (context == RuntimeContextType.SCALAR
|| context == RuntimeContextType.OBJECT) && isImmutableProxy(scalarResult)
? ensureMutableScalar(scalarResult) : scalarResult;
} else {
registers[rd] = result;
Expand Down Expand Up @@ -1652,6 +1683,8 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {
// Resolve RUNTIME context from register 2 (wantarray)
if (context == RuntimeContextType.RUNTIME) {
context = ((RuntimeScalar) registers[2]).getInt();
} else if (context == RuntimeContextType.INHERITED) {
context = RuntimeCode.currentRawCallContext();
}

RuntimeScalar invocant = (RuntimeScalar) registers[invocantReg];
Expand Down Expand Up @@ -1696,10 +1729,13 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) {
CallerStack.pop();
}

// Convert to scalar if called in scalar or lvalue context
if (context == RuntimeContextType.SCALAR || context == RuntimeContextType.LVALUE) {
// OBJECT is a raw Wanted parent-op context, but its
// value semantics are the same as scalar context.
if (context == RuntimeContextType.SCALAR || context == RuntimeContextType.LVALUE
|| context == RuntimeContextType.OBJECT) {
RuntimeBase scalarResult = result.scalar();
registers[rd] = context == RuntimeContextType.SCALAR && isImmutableProxy(scalarResult)
registers[rd] = (context == RuntimeContextType.SCALAR
|| context == RuntimeContextType.OBJECT) && isImmutableProxy(scalarResult)
? ensureMutableScalar(scalarResult) : scalarResult;
} else {
registers[rd] = result;
Expand Down Expand Up @@ -3170,6 +3206,15 @@ private static int executeComparisons(int opcode, int[] bytecode, int pc,
RuntimeScalarCache.scalarFalse : RuntimeScalarCache.scalarTrue;
return pc;
}
case Opcodes.NOT_NO_OVERLOAD -> {
int rd = bytecode[pc++];
int rs = bytecode[pc++];
RuntimeScalar val = (registers[rs] instanceof RuntimeScalar)
? (RuntimeScalar) registers[rs] : registers[rs].scalar();
registers[rd] = val.getBooleanNoOverload()
? RuntimeScalarCache.scalarFalse : RuntimeScalarCache.scalarTrue;
return pc;
}

case Opcodes.AND -> {
// AND is short-circuit and handled in compiler typically
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,13 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
invocantNode = new StringNode(className, invocantNode.getIndex());
}

if (invocantNode instanceof BinaryOperatorNode innerArrow
&& "->".equals(innerArrow.operator)
&& innerArrow.right instanceof BinaryOperatorNode innerCall
&& "(".equals(innerCall.operator)) {
innerArrow.setAnnotation("wantedObjectContext", true);
}

// Convert method name to string if needed
if (methodNode instanceof OperatorNode methodOp) {
// &method is introduced by parser if method is predeclared
Expand Down Expand Up @@ -293,7 +300,11 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
bytecodeCompiler.emitReg(methodReg);
bytecodeCompiler.emitReg(currentSubReg);
bytecodeCompiler.emitReg(argsReg);
bytecodeCompiler.emit(bytecodeCompiler.currentCallContext);
bytecodeCompiler.emit(node.getBooleanAnnotation("wantedObjectContext")
? RuntimeContextType.OBJECT
: node.getBooleanAnnotation("inheritRawCallContext")
? RuntimeContextType.INHERITED
: bytecodeCompiler.currentCallContext);

bytecodeCompiler.lastResultReg = rd;
return;
Expand Down Expand Up @@ -503,7 +514,7 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
bytecodeCompiler.emitAliasWithTarget(rd, rs1);

int skipRightPos = bytecodeCompiler.bytecode.size();
bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE);
bytecodeCompiler.emit(bytecodeCompiler.gotoIfFalseOpcode());
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitInt(0);

Expand All @@ -530,7 +541,7 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
bytecodeCompiler.emitAliasWithTarget(rd, rs1);

int skipRightPos = bytecodeCompiler.bytecode.size();
bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE);
bytecodeCompiler.emit(bytecodeCompiler.gotoIfTrueOpcode());
bytecodeCompiler.emitReg(rd);
bytecodeCompiler.emitInt(0);

Expand Down
Loading
Loading