Found by the adversarial correctness review of main @ 396a216 (task-core track, finding R2-F5). LOW — needs a guest sharing a subtask handle across tasks, which is unusual but legal.
Shape
createSubtaskCancel (runtime/src/intrinsics/async_builtins.ts, the #92 determinacy park) sets hasSyncWaiter = true before onCancel for both forms and, when it parks, clears it only in produce/onSettled. The reference (definitions.py canon_subtask_cancel ~2453-2461) clears has_sync_waiter before returning BLOCKED in the async form — the flag is held only across the synchronous claim window.
So during the park any ready thread of the caller's instance may run; one that executes waitable.join(i, set) on the same subtask handle traps "waitable cannot be used synchronously..." where the reference (async form having returned BLOCKED with the flag clear) succeeds.
The named divergence #92 (docs/architecture.md §6) promises only a reordering within Store.tick freedom ("every interleaved sibling was already at a block point"); it does not license a new trap condition.
Repro sketch
async_builtins_test.ts style, jspi mode: a FACT-style subtask whose calleeTask thread is hop-parked (awaiting, not in waiting, not done) so determinate() is false; call createSubtaskCancel({async:true}, inst, "jspi")(i) (returns a Promise); then createWaitableJoin(inst)(i, si) — expect success, ours traps.
Fix options
Either clear the flag when the park begins (the async form has no claim to hold once onCancel returned), or document the widened window under #92 in arch §6.
Found by the adversarial correctness review of
main@ 396a216 (task-core track, finding R2-F5). LOW — needs a guest sharing a subtask handle across tasks, which is unusual but legal.Shape
createSubtaskCancel(runtime/src/intrinsics/async_builtins.ts, the #92 determinacy park) setshasSyncWaiter = truebeforeonCancelfor both forms and, when it parks, clears it only inproduce/onSettled. The reference (definitions.pycanon_subtask_cancel~2453-2461) clearshas_sync_waiterbefore returningBLOCKEDin the async form — the flag is held only across the synchronous claim window.So during the park any ready thread of the caller's instance may run; one that executes
waitable.join(i, set)on the same subtask handle traps "waitable cannot be used synchronously..." where the reference (async form having returned BLOCKED with the flag clear) succeeds.The named divergence #92 (docs/architecture.md §6) promises only a reordering within
Store.tickfreedom ("every interleaved sibling was already at a block point"); it does not license a new trap condition.Repro sketch
async_builtins_test.tsstyle, jspi mode: a FACT-style subtask whosecalleeTaskthread is hop-parked (awaiting, not inwaiting, not done) sodeterminate()is false; callcreateSubtaskCancel({async:true}, inst, "jspi")(i)(returns a Promise); thencreateWaitableJoin(inst)(i, si)— expect success, ours traps.Fix options
Either clear the flag when the park begins (the async form has no claim to hold once
onCancelreturned), or document the widened window under #92 in arch §6.