Skip to content

Stop a failed future rendering as "Completed", and a tuple of none rendering as NPE - #649

Merged
lagergren merged 3 commits into
masterfrom
lagergren/569-8-future
Sep 26, 2026
Merged

lagergren merged 3 commits into
masterfrom
lagergren/569-8-future

Conversation

@lagergren

@lagergren lagergren commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Future display currently throws for a FutureTupleHandle containing no futures and labels failed or cancelled futures as "Completed". Rendering those failures also calls Utils.translate() to construct an exception handle solely for its display text.

This change checks the future's state before reading a successful result. The final diff changes only xFuture.java: +23 / −8. It continues the display series (#640–#643, #648).

State Before After
Tuple without futures NullPointerException <no future>
Pending Not completed Unchanged
Successful Completed: <value> Unchanged
Cancelled Completed: <translated exception> <cancelled>
Failed Completed: <translated exception> <failed>

Cancellation is checked before exceptional completion, since cancellation is also exceptional completion. Successful results use getNow(null); the unused toSafeString() is removed. The previous get() call was guarded by isDone(), so this fixes display behavior, not a blocking read.

Verification

After removing the standalone test fixture:

env RUN_INTEGRATION_TESTS=true ./gradlew :xdk:test --rerun :javatools:test --rerun --no-build-cache spotlessCheck --console=plain
git diff --check
  • XDK: 17 passed, zero failures, errors or skips.
  • Java tools: 414 cases: 378 passed, 36 existing skips, zero failures or errors.
  • Formatting and whitespace checks passed.

Before its removal, the standalone fixture exercised real handles in a native container. With xFuture.java replaced byte-for-byte by the then-current master implementation, 2 of 4 tests failed: the tuple display threw an NPE, and failed-future display accessed the stored exception's message. Restoring the fix made all four pass with zero skips. This is historical reproduction evidence; the fixture is no longer included in the final diff.

Suggested follow-up coverage

The 134-line standalone fixture was removed following review to keep this small fix focused. This PR therefore leaves no dedicated automated regression test for future display.

A shared Java runtime/debugger-display smoke suite could initialize a native container once and exercise actual handle toString() methods across handle types. Future cases should include tuples with no futures and pending, successful, failed and cancelled futures. Assertions should check that display succeeds, preserves state and avoids rendering stored failures, without prescribing exact labels or punctuation.

Ordinary .x future tests do not exercise Java handle toString(), so they cannot replace that coverage. The shared suite is a proposal, not part of this PR.

Comment thread javatools/src/test/java/org/xvm/runtime/template/annotations/xFutureTest.java Outdated
Comment thread xdk/src/test/java/org/xvm/xdk/FutureDisplayTest.java Outdated
…ndering as NPE

FutureHandle.toString() described a done future by calling toSafeString():

    return String.valueOf(getFuture().get());
    // catch (Throwable e) -> Utils.translate(e).toString()

Three problems, only one of which is about purity.

A future that has failed is done, so this took the catch branch and produced
"Completed: <translated exception>" - the word "Completed" about a failure. A cancelled future is
also done, and rendered the same way.

FutureTupleHandle inherits this toString(), and its getFuture() returns null when f_ahValue holds no
FutureHandle, so toString() threw NullPointerException on the isDone() call. Rendering a value is
not a reasonable place to throw.

Utils.translate(e) constructs an exception handle in the owning container, from whatever thread
happened to be rendering. That is the impurity, and it only ever ran to build a string that was
mislabelled anyway.

describe() answers from the future's own state - absent, pending, cancelled, failed, or completed -
using getNow(null), which neither blocks nor throws. Cancellation is tested before exceptional
completion because a cancelled future is also completed exceptionally. Behaviour is unchanged for
the two cases that were already right: a pending future still reads "Not completed", and a
successfully completed one still reads "Completed: <value>".

toSafeString() had no other callers and is removed with it.

The test exercises describe() directly rather than through a handle, since the point of the method
is that it needs no container, composition or running service. Four of its six cases fail against
the previous implementation - the absent, cancelled and failed ones - and the two that pass are the
two whose behaviour must not change, which is what they are there to pin.
@lagergren
lagergren force-pushed the lagergren/569-8-future branch from 6ec1ef4 to 8780a08 Compare September 26, 2026 21:34
@lagergren
lagergren merged commit 1708502 into master Sep 26, 2026
4 checks passed
@lagergren
lagergren deleted the lagergren/569-8-future branch September 26, 2026 21:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants