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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ up via JSPI where the engine provides it.

Pre-1.0, but densely gated:

- **Official Component Model test suite**: 1284 passing / 0 failing commands
- **Official Component Model test suite**: 1285 passing / 0 failing commands
across all directories (remaining: named xfail classes — deferred threads
([#12](https://github.com/polymorph-components/polyengine/issues/12)),
cm705-sync-sched ([#249](https://github.com/polymorph-components/polyengine/issues/249)),
Expand Down
4 changes: 2 additions & 2 deletions harness/browser/expectations/chromium.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,9 @@ export const chromium: LaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/browser/expectations/firefox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export const firefox: LaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/bun-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ export const bunPinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/jsc-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export const jscPinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/jsc-trunk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,9 @@ export const jscTrunk: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/node-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ export const nodePinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/sm-nightly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,9 @@ export const smNightly: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
4 changes: 2 additions & 2 deletions harness/shell/expectations/sm-pinned.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,9 @@ export const smPinned: ShellLaneExpectation = {
totals: {
commands: 1511,
executed: 1411,
passed: 1284,
passed: 1285,
failed: 0,
xfail: 127,
xfail: 126,
pendingRuntime: 95,
pendingCapability: 0,
unsupportedDirective: 5,
Expand Down
24 changes: 0 additions & 24 deletions harness/src/xfail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,30 +353,6 @@ export const XFAIL: XfailEntry[] = [
// shim decoder gap).
// =====================================================================
// --- async/builtin-trap-poisons-instance.json: root cause: STREAMS ---
// --- async/cancel-and-exclusive-lock.json: CM#707 "always deliver
// cancellation as soon as possible" (third_party/component-model commit
// 1af0b35, pulled in by the CM#705 pin advance polyengine#173) changed
// when a pending cancellation must be delivered; polyengine's task
// scheduler still implements the pre-#707 delivery timing, so the
// cross-instance exclusive-lock scenario this file drives deadlocks
// instead of the callee observing cancellation. Classed `cm707-cancel`,
// https://github.com/polymorph-components/polyengine/issues/250.
//
// NOTE on hang risk (dispatch warning): this file does NOT wedge the
// harness. It completes crisply with our OWN "deadlock detected: event
// loop cannot make further progress" trap rather than looping forever —
// the pre-#707 runtime deadlock manifests as a clean trap, not a stall.
// No harness-visible skip mechanism was needed. ---
{
file: "async/cancel-and-exclusive-lock.json",
line: 196,
reason:
"expected return, got trap: wasm trap: deadlock detected: event " +
"loop cannot make further progress — polyengine has not implemented " +
"CM#707's immediate-cancellation-delivery timing yet, so the " +
"callee never observes the pending cancellation and the scheduler " +
"finds no ready thread; cm707-cancel, https://github.com/polymorph-components/polyengine/issues/250",
},
// --- async/cancel-stream.json: root cause: STREAMS ---
// --- async/closed-stream.json: root cause: STREAMS ---
// --- async/cross-abi-calls.json: root cause: FACT-ASYNC ---
Expand Down
28 changes: 24 additions & 4 deletions runtime/src/task/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,9 +349,7 @@ export class Task {
resume(cancelled?: boolean): void;
};
let candidates: Cancellable[] = this.threads.filter((t) => t.cancellable);
const excludeImplicit = this.ft.async === true && this.needsExclusive() &&
this.inst.exclusiveThread !== null &&
this.inst.exclusiveThread !== this.implicitThread;
const excludeImplicit = !this.implicitThreadCancellable();
if (excludeImplicit) {
candidates = candidates.filter((t) => t !== this.implicitThread);
}
Expand Down Expand Up @@ -407,9 +405,31 @@ export class Task {
}
}

/**
* Is the implicit thread cancellable *right now*?
*
* The reference makes cancellability a live predicate — the callback loop
* passes `cancellable = lock_available` (definitions.py 2167/2175), false
* while a sibling activation of the instance holds the exclusive slot. We
* carry a static flag per block point instead, so this is where the "and
* the lock is free" conjunct lives: both `request_cancellation`'s candidate
* filter and `Thread.wait_until`'s pending-cancel wakeup disjunct consult
* it.
*/
implicitThreadCancellable(): boolean {
return !(this.ft.async === true && this.needsExclusive() &&
this.inst.exclusiveThread !== null &&
this.inst.exclusiveThread !== this.implicitThread);
}

/** definitions.py `Task.has_pending_cancel` (line 533). */
hasPendingCancel(): boolean {
return this.state === "pending-cancel";
}

/** definitions.py `Task.deliver_pending_cancel` (line 536). */
deliverPendingCancel(cancellable: boolean): boolean {
if (cancellable && this.state === "pending-cancel") {
if (cancellable && this.hasPendingCancel()) {
this.state = "cancel-delivered";
return true;
}
Expand Down
28 changes: 26 additions & 2 deletions runtime/src/task/thread.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,14 @@ export class Thread implements SchedulableThread {
/** Slot in `inst.threads`, assigned by `Task.registerThread`. */
index: number | null = null;

/** definitions.py `Thread.cancellable` — set at each block point. */
/**
* definitions.py `Thread.cancellable` — set at each block point, cleared
* while the thread runs. The reference evaluates it as a live predicate
* (`cancellable = lock_available` in the callback loop, line 2167), so a
* thread that is not parked is never a `request_cancellation` candidate;
* clearing on resume gives the same answer for the only shape that differs
* (a running implicit thread that still holds the exclusive slot).
*/
cancellable = false;

#state: ThreadState = "suspended";
Expand Down Expand Up @@ -196,6 +203,7 @@ export class Thread implements SchedulableThread {

#resumeInternal(sendValue: unknown, failure?: { error: unknown }): void {
this.#state = "running";
this.cancellable = false;
pushCurrentThread(this);
let step: IteratorResult<BlockRequest, void>;
try {
Expand Down Expand Up @@ -258,7 +266,23 @@ export class Thread implements SchedulableThread {
): Generator<BlockRequest, Cancelled, Cancelled> {
assert_(this.running(), "waitUntil on a non-running thread");
if (this.task.deliverPendingCancel(cancellable)) return CANCELLED_TRUE;
const cancelled = yield { readyFunc, cancellable };
// definitions.py `ready_or_cancelled` (line 369): a cancel that arrived
// while this task was not cancellable (parked as `pending-cancel`) makes
// the thread ready on its own — otherwise the wakeup is lost until some
// unrelated event happens to satisfy `readyFunc`. The reference's
// `cancellable()` is a live predicate; ours is the static flag AND
// `Task.implicitThreadCancellable` (the "lock is free" conjunct the
// callback loop's `lock_available` supplies there).
const readyOrCancelled = () =>
readyFunc() ||
(cancellable && this.task.hasPendingCancel() &&
(this !== this.task.implicitThread ||
this.task.implicitThreadCancellable()));
const cancelled = yield { readyFunc: readyOrCancelled, cancellable };
// AFTER the block (line 372): converts a plain wakeup taken through the
// pending-cancel disjunct into Cancelled.TRUE, and wins over any event
// that became pending in the meantime.
if (this.task.deliverPendingCancel(cancellable)) return CANCELLED_TRUE;
return cancelled;
}

Expand Down
86 changes: 86 additions & 0 deletions runtime/tests/stale_cancellable_flag_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// `Thread.cancellable` is set at each block point and never cleared on resume,
// so a callback task's implicit thread still looks cancellable while its frame
// RUNS holding `inst.exclusiveThread`. `Task.requestCancellation` then picks it
// (`excludeImplicit` is false — the holder IS the implicit thread) and calls
// `Thread.resume`, which asserts "resume() on a running or finished thread".
// The reference evaluates cancellability live (`cancellable = lock_available`,
// definitions.py 2167/2175): with the slot held it is False, so
// `request_cancellation` (499-503) records PENDING_CANCEL and delivers it at
// the task's next cancellable wait.

import { assertEq } from "./support/asserts.ts";
import {
type BlockRequest,
type Cancelled,
ComponentInstanceState,
Store,
Task,
type TaskOptions,
Thread,
} from "../src/task/mod.ts";
import type { FuncType } from "../src/cabi/types.ts";

const ASYNC_FT: FuncType = { params: [], results: [], async: true };

const CALLBACK_OPTS: TaskOptions = {
async_: true,
callback: true,
stringEncoding: "utf8",
memory: null,
};

function spawn(
task: Task,
body: (t: Thread) => Generator<BlockRequest, void, Cancelled>,
): Thread {
let thread!: Thread;
thread = new Thread(
task,
(function* (): Generator<BlockRequest, void, Cancelled> {
yield* body(thread);
})(),
);
return thread;
}

Deno.test("requestCancellation reaching a RUNNING implicit thread parks as pending-cancel", () => {
const store = new Store();
const inst = new ComponentInstanceState(0, store);
const a = new Task(ASYNC_FT, CALLBACK_OPTS, inst, () => [], () => {});

let thrown: unknown = null;
let stateAfterRequest = "";

const ta = spawn(a, function* (thread) {
yield* a.enterImplicitThread(thread); // takes inst.exclusiveThread
a.start();
// A cancellable wait that returns normally; `thread.cancellable` stays true.
yield* thread.waitUntil(() => true, true);
// Now the frame is RUNNING and holds the exclusive slot. This stands in
// for a nested task of the caller's instance running `subtask.cancel` on
// A's handle through a FACT sync start-call.
try {
a.requestCancellation(null);
} catch (e) {
thrown = e;
}
stateAfterRequest = a.state;
// Reference: the request is pending and is delivered at the next
// cancellable block point.
const cancelled = yield* thread.waitUntil(() => true, true);
if (cancelled) a.cancel();
else a.return_([]);
a.exitImplicitThread(thread);
});
ta.resume();
for (let i = 0; i < 20 && store.tick(); i++);

if (thrown !== null) {
throw new Error(
`requestCancellation on a running implicit thread threw instead of ` +
`parking the request: ${thrown}`,
);
}
assertEq(stateAfterRequest, "pending-cancel");
assertEq(a.state, "resolved");
});
Loading
Loading