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
190 changes: 34 additions & 156 deletions dev/modules/future.md
Original file line number Diff line number Diff line change
@@ -1,176 +1,54 @@
# Future 0.52 Support for PerlOnJava

## Status: Phase 3 Complete -- 41/56 test programs pass (was 36/56)
## Status: Passing unpatched

- **Module version**: Future 0.52 (PEVANS/Future-0.52.tar.gz)
- **Date started**: 2026-04-08
- **Branch**: `docs/future-module-plan`
- **Last verified**: 2026-06-17
- **Test command**: `./jcpan -t Future`
- **Build system**: Module::Build (auto-installed as dependency, all 53 tests pass)
- **Result**: 56/56 test programs passed, 786/786 subtests passed
- **Patch status**: The former `Future-0.52` CPAN patch is no longer needed.

## Background

Future is a foundational async programming module for Perl, used by IO::Async and many
event-driven frameworks. It provides promise/future objects for deferred computations.
Future 0.52 is pure-Perl (Future::PP) with an optional XS backend (Future::XS).
Future is a foundational async programming module for Perl, used by IO::Async and
event-driven frameworks. It provides promise/future objects for deferred
computations. Future 0.52 is pure Perl, with optional Future::XS acceleration.

The module builds and loads correctly under PerlOnJava. Most core functionality works --
creating futures, resolving/failing them, chaining with `then`/`else`/`catch`, combinators
(`wait_all`, `needs_all`, etc.), transforms, subclassing, labels, and utilities.
PerlOnJava now runs the full pure-Perl Future test suite without a distro patch.
Future::XS tests are skipped normally when Future::XS is not installed.

## Results History
## Key Fixes

| Date | Programs Failed | Subtests Failed | Total | Key Fix |
|------|----------------|-----------------|-------|---------|
| 2026-04-08 | 20/56 | 105/763 | - | Initial analysis |
| 2026-04-09 | **15/56** | **32/778** | - | Phase 1-3: REFCNT, B::COP, do FILE $@ |
The historical failures were not Future-specific. They were symptoms of missing or
incorrect PerlOnJava runtime behavior:

## Current Test Results (After Fixes)
| Area | Fix |
|------|-----|
| `B::SV::REFCNT` | Returns a stable nonzero value compatible with `Test2::Tools::Refcount` checks. |
| B optree walking | `B::OP::next`, `B::NULL`, `B::COP`, and `B::CV::START` provide enough structure for Future's debug helpers. |
| `do FILE` exception state | Successful `do FILE` clears `$@`, matching Perl behavior. |
| Weak-reference temporary cleanup | Nested weak sweep cleanup preserves temporaries long enough for Future::Mutex refcount-sensitive paths. |

### Passing Tests (22 PP + 18 XS-skipped + 1 skip = 41)
## Former CPAN Patch

| Test File | Result | Notes |
|-----------|--------|-------|
| t/00use.t | **ok** | Module loads |
| t/09transform-pp.t | **ok** | |
| t/20get-pp.t | **ok** | |
| t/20subclass-pp.t | **ok** | |
| t/22wrap_cb-pp.t | **ok** | |
| t/23exception-pp.t | **ok** | **NEW** (was exit 255) |
| t/24label-pp.t | **ok** | |
| t/26wrap-unwrap-pp.t | **ok** | |
| t/27udata-pp.t | **ok** | |
| t/30utils-call.t | **ok** | **NEW** (was exit 255) |
| t/31utils-call-with-escape.t | **ok** | **NEW** (was 1 failure) |
| t/32utils-repeat.t | **ok** | **NEW** (was exit 255) |
| t/33utils-repeat-generate.t | **ok** | |
| t/34utils-repeat-foreach.t | **ok** | |
| t/35utils-map-void.t | **ok** | |
| t/36utils-map.t | **ok** | |
| t/40mutex.t | **ok** | **NEW** (was 4 failures) |
| t/51test-future-deferred.t | **ok** | |
| t/99pod.t | skipped | Test::Pod not installed |
| t/*-xs.t (18 files) | skipped | No Future::XS -- expected |
The removed CPAN patch changed `Future::Mutex` to keep an extra reference to an
active returned future, and changed `t/40mutex.t` to expect an extra refcount under
PerlOnJava. After the weak-reference temporary cleanup fix, upstream Future passes
as-is, so the distro preference and patch were removed from the bundled CPAN
configuration.

### Remaining Failures (15/56)
## Verification

| Test File | Failed/Total | Root Cause |
|-----------|-------------|------------|
| t/01future-pp.t | 4/85 | Refcount=2 |
| t/02cancel-pp.t | 4/38 | Refcount=2 |
| t/03then-pp.t | 1/56 | Refcount=2 |
| t/04else-pp.t | 1/52 | Refcount=2 |
| t/05then-else-pp.t | 2/21 | Refcount=2 |
| t/06followed_by-pp.t | 2/40 | Refcount=2 |
| t/07catch-pp.t | 1/28 | Refcount=2 |
| t/10wait_all-pp.t | 2/40 | Refcount=2 |
| t/11wait_any-pp.t | 2/42 | Refcount=2 |
| t/12needs_all-pp.t | 2/38 | Refcount=2 |
| t/13needs_any-pp.t | 2/48 | Refcount=2 |
| t/21debug-pp.t | 3/15 | DESTROY not implemented |
| t/25retain-pp.t | 3/18 | Refcount=2 |
| t/50test-future.t | 3/5 | Refcount=2 + line number |
| t/52awaitable-future-pp.t | 0/0 (exit 2) | exit(0) handling |
```text
Files=56, Tests=786
Result: PASS
```

**All remaining refcount failures expect refcount=2 but get 1.** This is an inherent JVM
limitation -- there is no way to know the real reference count on the JVM.

## Issues Found

### P0: `B::SV::REFCNT` returns 0 instead of 1 -- FIXED

- **Impact**: ~100 of 105 failed subtests across 15 test files
- **Root cause**: `B::SV::REFCNT` returned `0` while `Internals::SvREFCNT()` and
`Devel::Peek::SvREFCNT()` returned `1`. This inconsistency caused all refcount
checks via `Test2::Tools::Refcount` to fail.
- **Fix**: Changed `B::SV::REFCNT` to return `1`, aligning all three refcount stubs.
- **File**: `src/main/perl/lib/B.pm`
- **Result**: Fixed 73 of 105 failing subtests (those expecting refcount=1).
26 subtests remain (expecting refcount=2+, unfixable JVM limitation).

### P1: `B::OP::next` returns `undef` instead of `B::NULL` -- FIXED

- **Impact**: 4 test files crashed with exit 255
- **Root cause**: Future.pm's `CvNAME_FILE_LINE()` walks the B optree looking for
`B::COP` or `B::NULL` nodes. `B::OP::next()` returned `undef`, causing the walk
to terminate with `$cop = undef`, then `$cop->file` crashed.
- **Fix**: Three changes to `src/main/perl/lib/B.pm`:
1. `B::OP::next` returns `B::NULL->new()` instead of `undef`
2. `B::NULL::next` returns `$_[0]` (self) to prevent infinite loops
3. Added `B::COP` class with `file` and `line` methods
4. `B::CV::START` returns `B::COP->new("-e", 0)` so optree walkers find file/line info
- **Files**: `src/main/perl/lib/B.pm`
- **Result**: All 4 crashes eliminated. t/30utils-call.t and t/32utils-repeat.t fully pass.

### P2: DESTROY not implemented -- UNFIXABLE (JVM limitation)

- **Impact**: 3 subtests in t/21debug-pp.t
- **Root cause**: Future's debug mode requires DESTROY for "Lost Future" warnings.
PerlOnJava does not call DESTROY for blessed objects (JVM uses tracing GC).
- **Status**: Known JVM limitation, documented in AGENTS.md.

### P3: `$@` leakage in `do FILE` -- FIXED

- **Impact**: t/23exception-pp.t exited 255 despite all subtests passing
- **Root cause**: `do FILE` did not clear `$@` after successful execution. In Perl,
`do FILE` is like `eval STRING` and clears `$@` when the file completes normally.
PerlOnJava's `doFile()` cleared `$@` at the start but not after successful execution,
so `$@` from inner `eval { die ... }` blocks leaked through to the caller.
- **Fix**: Added `GlobalVariable.setGlobalVariable("main::@", "")` after successful
execution in `ModuleOperators.doFile()`.
- **File**: `src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java`
- **Result**: t/23exception-pp.t now passes cleanly.

### P4: `caller()` line number discrepancy -- OPEN (minor)

- **Impact**: 1 subtest in t/50test-future.t
- **Root cause**: Test expects error at line 37 but PerlOnJava reports line 35.
- **Status**: Low priority.

### P5: `exit(0)` inside skip_all -- OPEN (minor)

- **Impact**: t/52awaitable-future-pp.t exits with code 2 instead of 0
- **Status**: Low priority, cosmetic.

## Progress Tracking

### Phase 1: Fix `B::SV::REFCNT` -- COMPLETED (2026-04-09)

- Changed `return 0` to `return 1` in `B::SV::REFCNT`
- File: `src/main/perl/lib/B.pm`

### Phase 2: Fix B optree walking + add `B::COP` -- COMPLETED (2026-04-09)

- `B::OP::next` returns `B::NULL->new()` instead of `undef`
- `B::NULL::next` returns self (terminal sentinel)
- Added `B::COP` class with `file`/`line` methods
- `B::CV::START` returns `B::COP` instead of `B::OP`
- File: `src/main/perl/lib/B.pm`

### Phase 3: Fix `$@` leakage in `do FILE` -- COMPLETED (2026-04-09)

- Clear `$@` after successful file execution in `ModuleOperators.doFile()`
- File: `src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java`

## Files Changed

| File | Change |
|------|--------|
| `src/main/perl/lib/B.pm` | REFCNT returns 1; B::OP::next returns B::NULL; added B::COP class; B::CV::START returns B::COP |
| `src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java` | Clear $@ after successful do FILE |
| `dev/modules/future.md` | This plan document |

## Remaining Failures Summary

| Category | Count | Status |
|----------|-------|--------|
| Refcount=2 (JVM limitation) | 26 subtests / 13 programs | Unfixable |
| DESTROY (JVM limitation) | 3 subtests / 1 program | Unfixable |
| caller() line number | 1 subtest | Low priority |
| exit(0) handling | 1 program | Low priority |
The final verification run used a cleared local CPAN Future patch/pref cache to
ensure `jcpan` did not apply the stale local patch.

## Related Documents

- `dev/modules/xs_fallback.md` -- XS fallback mechanism (relevant for Future::XS skip)
- `dev/design/destroy_and_weak_refs.md` -- DESTROY implementation plan
- AGENTS.md -- Documents `weaken`/`isweak`/DESTROY limitations
- `dev/modules/xs_fallback.md` - XS fallback mechanism
- `dev/design/destroy_and_weak_refs.md` - weak reference and DESTROY behavior
- `dev/architecture/weaken-destroy.md` - current weak reference and DESTROY implementation
15 changes: 10 additions & 5 deletions dev/modules/moose_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ through helper subs `Moose::Exporter::_set_flag`/`_get_flag`.
path doesn't kick in (also done by the same helper);
- skips `make` and `install` (`PerlOnJava::Distroprefs::Moose::noop`,
cross-platform replacement for POSIX `true`);
- runs `prove --exec jperl -r t/` against the unpacked tarball.
- runs a bounded smoke set with `prove --exec jperl` against the unpacked
tarball. Set `PERLONJAVA_MOOSE_FULL_TESTS=1` to run the full upstream
`t/` tree manually.

`jcpan` / `jcpan.bat` prepend the project directory to `PATH` so
shell-spawned subprocesses (CPAN's distroprefs commandlines, prove's
Expand All @@ -325,13 +327,16 @@ helper installs only `Moo`, the real runtime dependency of the shim.

Because `prove --exec` invokes `jperl` per test file without adding
`lib/` or `blib/lib/` to `@INC`, the **bundled shim from the jar** wins
over the unpacked upstream `lib/Moose.pm`. So you can run the entire
upstream suite end-to-end and see honestly which tests pass, without
patching Moose's `Makefile.PL` or shipping a fragile diff.
over the unpacked upstream `lib/Moose.pm`. The default smoke set keeps
`jcpan -t Moose` inside the random tester timeout while still exercising
the shim's common surfaces. The full upstream suite remains available for
baseline collection with `PERLONJAVA_MOOSE_FULL_TESTS=1`, without patching
Moose's `Makefile.PL` or shipping a fragile diff.

The same recipe is the model for any future "test against shim, don't
install" scenario — define a distroprefs entry that overrides `pl` /
`make` / `install` with no-ops and `test` with a `prove --exec` line.
`make` / `install` with no-ops and `test` with a bounded `prove --exec`
line plus an opt-in full-suite mode.

### Quick-path baseline (Moose 2.4000)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1421,7 +1421,7 @@ public void visit(NumberNode node) {
emit(constIdx);
} else {
// Floating-point number - create RuntimeScalar with double value
RuntimeScalar doubleScalar = new RuntimeScalar(Double.parseDouble(value));
RuntimeScalar doubleScalar = new RuntimeScalar(Double.parseDouble(value), value);
int constIdx = addToConstantPool(doubleScalar);
emit(Opcodes.LOAD_CONST);
emitReg(rd);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,15 @@ static RuntimeScalar ensureMutableScalar(RuntimeBase val) {
RuntimeScalar copy = new RuntimeScalar();
copy.type = ro.type;
copy.value = ro.value;
copy.numericLiteralText = ro.numericLiteralText;
return copy;
}
if (val instanceof ScalarSpecialVariable sv) {
RuntimeScalar src = sv.getValueAsScalar();
RuntimeScalar copy = new RuntimeScalar();
copy.type = src.type;
copy.value = src.value;
copy.numericLiteralText = src.numericLiteralText;
return copy;
}
return (RuntimeScalar) val;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,15 @@ static RuntimeScalar ensureMutableScalar(RuntimeBase val) {
RuntimeScalar copy = new RuntimeScalar();
copy.type = ro.type;
copy.value = ro.value;
copy.numericLiteralText = ro.numericLiteralText;
return copy;
}
if (val instanceof ScalarSpecialVariable sv) {
RuntimeScalar src = sv.getValueAsScalar();
RuntimeScalar copy = new RuntimeScalar();
copy.type = src.type;
copy.value = src.value;
copy.numericLiteralText = src.numericLiteralText;
return copy;
}
return (RuntimeScalar) val;
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java
Original file line number Diff line number Diff line change
Expand Up @@ -610,9 +610,10 @@ public static void emitNumber(EmitterContext ctx, NumberNode node) {
mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/runtimetypes/RuntimeScalar");
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn(Double.valueOf(value));
mv.visitLdcInsn(value);
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/runtimetypes/RuntimeScalar",
"<init>", "(D)V", false);
"<init>", "(DLjava/lang/String;)V", false);
}
} else {
// Unboxed context: push primitive values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ public static RuntimeScalar getConstantValue(Node node) {
// to preserve precision (32-bit Perl emulation)
return new RuntimeScalar(value);
} else {
return new RuntimeScalar(Double.parseDouble(value));
return new RuntimeScalar(Double.parseDouble(value), value);
}
} catch (NumberFormatException e) {
// Fallback to string for unusual number formats
Expand Down
11 changes: 10 additions & 1 deletion src/main/java/org/perlonjava/runtime/operators/Time.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,14 @@ private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) {
}

public static RuntimeScalar sleep(RuntimeScalar runtimeScalar) {
return sleepInternal(runtimeScalar, false);
}

public static RuntimeScalar sleepPrecise(RuntimeScalar runtimeScalar) {
return sleepInternal(runtimeScalar, true);
}

private static RuntimeScalar sleepInternal(RuntimeScalar runtimeScalar, boolean preciseReturn) {
RuntimeIO.flushAllHandles();

long s = (long) (runtimeScalar.getDouble() * 1000);
Expand All @@ -289,7 +297,8 @@ public static RuntimeScalar sleep(RuntimeScalar runtimeScalar) {
}
long endTime = System.nanoTime();
long actualSleepTime = endTime - startTime;
return new RuntimeScalar(actualSleepTime / 1_000_000_000.0);
double sleptSeconds = actualSleepTime / 1_000_000_000.0;
return new RuntimeScalar(preciseReturn ? sleptSeconds : Math.floor(sleptSeconds));
}

/**
Expand Down
16 changes: 13 additions & 3 deletions src/main/java/org/perlonjava/runtime/operators/WarnDie.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,18 @@ private static String signalHandlerSubName(RuntimeScalar sigHandler) {
return pkg + "::" + code.subName;
}

private static void writeWarningToStderr(String message) {
RuntimeIO stderrIO = getGlobalIO("main::STDERR").getRuntimeIO();
if (stderrIO == null) {
stderrIO = RuntimeIO.stderr;
}
if (stderrIO != null) {
stderrIO.write(message);
} else {
System.err.print(message);
}
}

public static RuntimeException maybeInvokeUnhandledDieHandler(RuntimeException e) {
Throwable unwrapped = unwrapException(e);
if (unwrapped instanceof PerlDieException || unwrapped instanceof PerlExitException) {
Expand Down Expand Up @@ -349,9 +361,7 @@ public static RuntimeBase warn(RuntimeBase message, RuntimeScalar where, String
return new RuntimeScalar(1);
}

// Get the RuntimeIO for STDERR and write the message
RuntimeIO stderrIO = getGlobalIO("main::STDERR").getRuntimeIO();
stderrIO.write(finalMessage.toString());
writeWarningToStderr(finalMessage.toString());

return new RuntimeScalar(1); // Perl's warn() always returns 1
}
Expand Down
Loading
Loading