This document tracks CPAN client support for PerlOnJava. The jcpan command provides full CPAN functionality for pure Perl modules.
Working:
jcpan install Module::Name- Install pure Perl modules from CPANjcpan -f install Module::Name- Force install (skip tests)jcpan -t Module::Name- Test a module- Interactive CPAN shell via
jcpan - DateTime - Full functionality including timezone support (99.7% test pass rate)
Known Limitations:
- XS modules require manual porting (see
.agents/skills/port-cpan-module/) - Module::Build-only modules need Module::Build installed separately
- Tests that heavily use fork may fail or skip
- Safe.pm compartment restrictions are not enforced (uses trusted eval)
| Category | Modules |
|---|---|
| File I/O | File::Spec, File::Basename, File::Copy, File::Find, File::Path, File::Temp |
| Text | Text::ParseWords, Text::Wrap |
| Core | Config, Carp, Cwd, Exporter, Fcntl |
| I/O | FileHandle, IO::File, IO::Handle, IO::Socket |
| Network | HTTP::Tiny, Net::FTP |
| Archive | Archive::Tar, Archive::Zip, Compress::Zlib |
| Crypto | Digest::MD5, Digest::SHA, MIME::Base64 |
| Data | YAML, JSON |
| Process | IPC::Open2, IPC::Open3 |
| Module | Implementation | Notes |
|---|---|---|
| Safe | Stub using eval |
CPAN metadata is trusted |
| ExtUtils::MakeMaker | Custom | Installs directly, no make needed |
| Module::Build::Base | Stub | Disables fork pipes |
| namespace::autoclean | Stub (no-op) | Skips cleanup to allow imported functions |
| Module | Reason |
|---|---|
| Opcode | Requires Perl opcode internals |
| LWP::UserAgent | Use HTTP::Tiny instead |
Safe.pm is used by CPAN.pm to evaluate metadata. It depends on Opcode.pm which manipulates Perl's internal opcode tree. Since PerlOnJava compiles to JVM bytecode (not Perl opcodes), implementing Opcode would require significant architectural work.
Current solution: Safe.pm stub uses eval with no strict 'vars'. CPAN metadata is trusted, so this is sufficient for normal use.
| Phase | Description | Key Deliverables |
|---|---|---|
| 1 | Low-hanging fruit | DirHandle, Dumpvalue, Sys::Hostname, flock() |
| 2 | Archive/Network | IO::Socket, Archive::Tar, Net::FTP |
| 3 | Process Control | IPC::Open2, IPC::Open3 via Java ProcessBuilder |
| 4 | Archive::Zip | Java implementation using java.util.zip |
| 5 | ExtUtils::MakeMaker | Direct installation without make |
| 6 | CPAN.pm Support | Safe.pm stub, parser fixes, CPAN shell working |
| 7 | Errno & Regex | $! dualvar, literal {} braces in regex |
| 8 | User Experience | jcpan wrapper script |
| 9 | Polish | YAML version update, Module::Build partial support |
| 11 | DateTime Support | namespace::autoclean stub, keyword autoquoting parser fix |
| 12 | DateTime Java XS | refaddr fix, POSIX math functions |
| 13 | Overload Stringification | Single-variable interpolation now forces stringify |
DateTime installation via jcpan completed but the module had issues loading due to its complex dependency chain involving namespace::autoclean.
Two fixes were required:
-
namespace::autoclean stub - Created
src/main/perl/lib/namespace/autoclean.pmthat provides the interface but skips cleanup. This allows imported functions (like Try::Tiny'stry/catch) to remain available. -
Parser fix for keyword autoquoting - Extended
ListParser.javato handle keywords likeuntil,while,for,if,unless,foreachas bareword hash keys when followed by=>. Previously these keywords would incorrectly terminate list parsing.
./jperl -MDateTime -e '
my $dt = DateTime->new(
year => 2024,
month => 3,
day => 15,
hour => 14,
minute => 30,
time_zone => "America/New_York"
);
print $dt->datetime, "\n"; # 2024-03-15T14:30:00
$dt->add(days => 5, hours => 2);
print $dt->datetime, "\n"; # 2024-03-20T16:30:00
'| Issue | Fix | File |
|---|---|---|
${ $stash{NAME} } dereference |
Fixed symbol table access | |
| GLOBREFERENCE scalar dereference | $$globref now returns the glob itself |
|
| map/grep @_ access | Blocks now access outer subroutine's @_ | |
| B::Hooks::EndOfScope NPE | Null check for fileName | |
| namespace::autoclean cleanup | Stub that skips cleanup | src/main/perl/lib/namespace/autoclean.pm |
| Keywords as hash keys | Extended autoquoting to more keywords | ListParser.java |
Test and verify DateTime uses the Java XS fallback mechanism instead of pure Perl fallback, providing better performance via native Java date/time operations.
DateTime now uses Java XS implementation ($DateTime::IsPurePerl = 0)
| Issue | Fix | File |
|---|---|---|
| Missing POSIX math functions | Added floor, ceil, fmod, fabs, pow, trig functions |
POSIX.pm |
refaddr returning inconsistent values |
Fixed to return identity hash of underlying referenced object | ScalarUtil.java |
| Specio enum validation failing | Fixed by refaddr fix - env var names now stable |
- |
| DateTime truncate/today failing | Fixed by Specio fix | - |
-
Java XS Loading:
XSLoader::load("DateTime")successfully loadsDateTime.javawhich provides:_rd2ymd- Rata Die to year/month/day conversion usingjava.time.JulianFields_ymd2rd- Year/month/day to Rata Die conversion_is_leap_year- Usingjava.time.Year.isLeap()_time_as_seconds,_seconds_as_components- Time arithmetic_normalize_tai_seconds,_normalize_leap_seconds- TAI/UTC handling_day_length,_day_has_leap_second,_accumulated_leap_seconds- Leap second support
-
refaddr Bug: The
Scalar::Util::refaddrfunction was returningSystem.identityHashCode(scalar)wherescalaris the RuntimeScalar wrapper, causing different values each time when called via a method. Fixed to return identity hash code of the underlyingscalar.valuefor reference types. -
POSIX Math Functions: Added complete set of POSIX math functions:
floor,ceil- Rounding functionsfmod- Floating-point modulofabs,pow- Absolute value and powerasin,acos,atan,tan- Trigonometric functionssinh,cosh,tanh- Hyperbolic functionslog10,ldexp,frexp,modf- Logarithmic and mantissa functions
DateTime test suite: 3506/3513 subtests passed (99.8%), 7 failures
DateTime tests (t/20infinite.t, t/31formatter.t) were failing with StackOverflowError when comparing stringified DateTime objects using eq.
When a double-quoted string contained only a single interpolated variable like "$obj", the parser was optimizing it to just return the variable directly, without forcing stringification. This caused:
- The
eqoverload handler does:return "$a" eq "$b" - PerlOnJava was treating
"$a"as just$a(no stringification) - This caused the
eqoverload to call itself infinitely → StackOverflowError
Fixed StringDoubleQuoted.createJoinNode() to ensure that single non-string segments in string interpolation are wrapped in a join() operation, which forces proper stringification.
The fix does NOT apply in regex context (isRegex=true) because regex patterns should use the qr overload, not stringify.
src/main/java/org/perlonjava/frontend/parser/StringDoubleQuoted.java- Fixed single-variable string interpolation
DateTime test suite had 47 failures related to:
- Leap second handling (second=60 not accepted, wrong RD calculations)
- End-of-month arithmetic (wrap mode not working)
cmpoverload returning 0 instead of -1/1 (breaking sort)
Root Cause: The Java XS _ymd2rd function was clamping day values to valid range instead of allowing overflow/underflow.
Fix: Changed from clamping to LocalDate.plusDays() which correctly handles:
day=0→ last day of previous monthday > month_length→ overflow to next month(s)day < 1→ underflow to previous month(s)
This is critical for end-of-month arithmetic with 'wrap' mode.
Tests Fixed: t/06add.t, t/10subtract.t, t/11duration.t (partial)
Root Cause: The leap second table had incorrect RD values (~8000 days off) due to incorrect epoch calculation.
Fix: Recalculated all RD values using DateTime->_ymd2rd():
- First leap second: July 1, 1972 → RD 720075 (was 728714)
- Accumulated count starts at 1 (was 10)
Tests Fixed: t/19leap-second.t (all 204 pass), t/32leap-second2.t (all 57 pass)
Root Cause: DateTime's _string_compare_overload uses goto $meth to delegate to _compare_overload. The goto creates a TAILCALL marker, but tryOverload() wasn't handling it.
Fix: Added trampoline loop to execute TAILCALL markers:
while (result instanceof RuntimeControlFlowList) {
RuntimeControlFlowList flow = (RuntimeControlFlowList) result;
if (flow.getControlFlowType() == TAILCALL) {
RuntimeScalar codeRef = flow.getTailCallCodeRef();
RuntimeArray args = flow.getTailCallArgs();
result = RuntimeCode.apply(codeRef, args, SCALAR);
} else {
break;
}
}Tests Fixed: t/07compare.t, t/27delta.t, t/38local-subtract.t
DateTime test suite: 1987/2064 subtests passed (96.3%), 77 failures
(Note: Phase 15 improved this to 99.7% by fixing overload method name resolution)
| Test | Failures | Reason |
|---|---|---|
| t/11duration.t | 1 | TODO test for fractional units |
| t/29overload.t | 2 | Missing Test::Warnings dependency |
| t/33seconds-offset.t | 3 | TODO tests for second offsets near leap seconds |
| t/48rt-115983.t | 1 | Test::Fatal error message format mismatch |
src/main/java/org/perlonjava/runtime/perlmodule/DateTime.java- Fixed_ymd2rd, corrected leap second tablesrc/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java- Added TAILCALL trampoline
DateTime tests were failing at ~96.3% pass rate (1987/2064 subtests) with many tests showing errors about Specio type validation and stringification issues.
When debugging, we discovered that Specio type objects (like DateTime::Types::t("Locale")) were stringifying to an empty string "" instead of their type name.
Investigation path:
$type->namereturned "Locale" correctly$type->_stringifyreturned "Locale" correctly- But
"$type"returned ""
Root Cause: Perl's overload pragma allows two ways to specify operator implementations:
# Method 1: Code reference (works in PerlOnJava)
use overload '""' => \&_stringify;
# Method 2: Method name string (was NOT working in PerlOnJava)
use overload '""' => '_stringify';When a method name string is used, Perl's overload.pm stores:
- CODE slot:
\&overload::nil(a no-op function) - SCALAR slot: the method name string (e.g., "_stringify")
The ov_method() function in overload.pm handles this by checking if CODE is \&nil, and if so, looking up the method name from SCALAR and calling $obj->can($method).
PerlOnJava was missing this logic - it just executed the CODE slot (\&nil) and got undef.
Modified OverloadContext.tryOverload() to:
- Check if the found method is
overload::nil(by examiningpackageNameandsubName) - If so, look up the SCALAR slot of the glob to get the actual method name
- Follow glob references (e.g.,
*Package::Method) if the SCALAR contains one - Resolve the actual method using
can()semantics
src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java- Added
resolveOverloadMethodName()helper method - Modified
tryOverload()to detect and handleoverload::nil
- Added
| Metric | Before | After | Change |
|---|---|---|---|
| Total tests | 2064 | 3522 | +1458 (more tests now run!) |
| Passing | 1987 | 3513 | +1526 |
| Failing | 77 | 9 | -68 |
| Pass rate | 96.3% | 99.7% | +3.4% |
| Test | Failures | Reason |
|---|---|---|
| t/11duration.t | 1 | TODO test for fractional units |
| t/29overload.t | 2 | Warning location info missing (pre-existing limitation) |
| t/33seconds-offset.t | 3 | TODO tests for leap second edge cases |
| t/48rt-115983.t | 1 | Error message format ("subroutine" vs "method") |
These failures are due to:
- TODO tests (t/11duration.t, t/33seconds-offset.t) - Expected failures for known edge cases
- Warning location info (t/29overload.t) - Warnings are now emitted but without file/line info
- Error message format (t/48rt-115983.t) - "Undefined subroutine" vs "Can't locate object method"
All major DateTime issues have been fixed. The 7 remaining test failures are:
- 4 TODO tests - Known limitations even in native Perl (fractional units, leap second edge cases)
- 2 warning location tests - Warnings work but don't include file/line info yet
- 1 error format test - Cosmetic difference in error message wording
When installing DateTime with empty caches, CPAN::Meta::YAML parsing would fail with:
Read an invalid UTF-8 string (maybe mixed UTF-8 and 8-bit character set).
Did you decode with lax ":utf8" instead of strict ":encoding(UTF-8)"?
This error prevented proper parsing of META.yml/MYMETA.yml files, which meant test dependencies like Test::Without::Module and CPAN::Meta::Check were not being properly detected.
CPAN::Meta::YAML validates strings before parsing:
if ( utf8::is_utf8($string) && ! utf8::valid($string) ) {
die "Read an invalid UTF-8 string...";
}The utf8::valid() function in PerlOnJava was using CharsetDetector which was fundamentally wrong:
- It converted the string to bytes using the default charset
- Then tried to detect if those bytes were UTF-8
- This always failed for properly decoded Unicode strings
Rewrote utf8::valid() in Utf8.java to correctly check string validity:
- For character strings (UTF-8 flag on): Validates that surrogate pairs are properly formed
- For byte strings (UTF-8 flag off): Attempts to decode bytes as UTF-8
src/main/java/org/perlonjava/runtime/perlmodule/Utf8.java- Fixedvalid()method
The fix allows CPAN::Meta::YAML to properly parse MYMETA.yml files, enabling CPAN.pm to detect and install test dependencies.
| Issue | Status | Impact |
|---|---|---|
| File::stat.pm missing | Not implemented | DateTime::Locale installation fails |
| IPC::Open3 read-only error | Bug in IPCOpen3.java | Some module tests fail |
| Test::Harness UTF-8 error | Pre-existing | Some test output parsing fails |
xsloader.md- XSLoader/Java integrationmakemaker_perlonjava.md- ExtUtils::MakeMaker implementation.agents/skills/port-cpan-module/- Skill for porting CPAN modulesdocs/guides/using-cpan-modules.md- User documentation