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
19 changes: 19 additions & 0 deletions contracts/plan-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,22 @@ Additions/corrections from the M0 integration, normative as of v0.1:
use table 0, both resolving to `ResourceIndex` 0 via
`resourceTables[n].resource`. Consumers keying per-resource state must
key by the resolved `ResourceIndex`, treating table indices as aliases.

## CM#705 adoption amendment (2026-08-30, polyengine#173)

1. **The instance-tree question is retired, wire-form-free forever.**
Upstream CM#705 (adopted at submodule pin `2f13265`) deleted
`ComponentInstance.parent`, `entering_set`, and the whole
`may_enter` enter/leave model from the reference: reentrance into a
live instance is valid, and no reachable semantics consult instance
ancestry at all. Accordingly: v1 amendment 4's "open gap: no wire form
for the component-instance tree" is void (there is no tree to carry),
and v3 amendment 4's runtime-side closure — the synthetic
per-instantiation root and its `mayEnterFrom`/`enterFrom`/`leaveTo`
participation — has been **deleted from the runtime**, not merely
bypassed. The "reopens only if a future upstream shape makes nesting
depth observable" clause carries over to this amendment unchanged. No
`formatVersion` bump: the plan wire format never carried any of this.
What survives at entry sites is per-instance poisoning refusal, a
named divergence documented in docs/architecture.md §6 — a runtime
policy with no plan-format footprint.
25 changes: 12 additions & 13 deletions harness/src/xfail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -840,19 +840,18 @@ export const XFAIL: XfailEntry[] = [
// polyengine's own reentrance implementation. Only SIBLING adapters
// compile to real fused code at this pin. Classed `fact-reentrance-47`,
// https://github.com/polymorph-components/polyengine/issues/248 (pending-capability: wasmtime-environ bump).
// - "cannot enter component instance ${index} (reentrance forbidden)" (NO
// `wasm trap:` prefix) is polyengine's OWN `mayEnterFrom`/`enterFrom` gate
// (runtime/src/exec/boundary.ts, intrinsics/fact_calls.ts), produced in
// JS, not wasm — this is the class a prior triage round misattributed
// every row in this file to (`cm705-gate-removal`). ZERO corpus rows in
// this file actually hit that path: every failing row below carries the
// `wasm trap:` prefix, so all are FACT-47 static-stub trips, not the
// runtime's stale gate. CM#705's removal of may_enter/entering_set/
// enter_from/leave_to from definitions.py (polyengine#173) is real and
// still-open work, but this corpus cannot prove or disprove it: FACT-47
// masks every row that would exercise the runtime gate before the gate
// itself ever runs. #173 is tracked/pinned by runtime unit tests, not by
// this file. See also the correction note at
// - "cannot enter component instance ${index}" (NO `wasm trap:` prefix)
// is polyengine's OWN entry refusal (runtime/src/exec/boundary.ts,
// intrinsics/fact_calls.ts), produced in JS, not wasm — since the
// CM#705 adoption landed (#251/#252/#255 + the model deletion,
// polyengine#173) that refusal fires ONLY for a poisoned instance
// (the per-instance corpse divergence, docs/architecture.md §6); the
// transient reentrance gate it once signified is gone. ZERO corpus
// rows in this file hit that path: every failing row below carries
// the `wasm trap:` prefix, so all are FACT-47 static-stub trips.
// The adoption cannot be proven or disproven here: FACT-47's stubs
// trap before any polyengine runtime code runs. #173 is pinned by
// runtime unit tests, not by this file. See also the correction note at
// https://github.com/polymorph-components/polyengine/issues/248#issuecomment-5471308919. ---
{
file: "async/reentrance.json",
Expand Down
22 changes: 20 additions & 2 deletions runtime/src/cabi/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,32 @@ export function requireMemory(opts: LiftOptions): MemInst {

/**
* Minimal component-instance stand-in for the value interpreter: a handle
* table plus the `may_leave` gate. The full ComponentInstance (may_enter,
* backpressure, threads, ...) belongs to the deferred task machinery.
* table plus the `may_leave` gate. The full ComponentInstance (backpressure,
* threads, ...) belongs to the task machinery, which cabi must not import.
*/
export interface ComponentInstanceLike {
handles: Table<unknown>;
mayLeave: boolean;
}

/**
* Brand marking a value as a REAL component instance (task/mod.ts
* `ComponentInstanceState`), as opposed to the many structural
* `ComponentInstanceLike` stand-ins — imported/host resources carry no
* instance at all, and test harnesses supply bare `{handles, mayLeave}`
* doubles. cabi must not depend on task/, so the symbol lives here and
* `ComponentInstanceState` declares it; cabi/handles.ts `isComponentInstance`
* is the only reader.
*
* It replaced a structural match on the pre-CM#705 reentrance methods
* (`may_enter_from`/`enter_from`/`leave_to`), which polyengine#173 deleted
* along with the rest of the transient reentrance model. `ComponentInstanceLike`
* stays deliberately structural: the brand is NOT part of it.
*/
export const COMPONENT_INSTANCE: unique symbol = Symbol(
"polyengine.ComponentInstance",
);

/**
* Borrow scopes (definitions.py `LiftLowerContext.borrow_scope`):
* - lifting a borrow requires the *subtask* side: `add_lender`.
Expand Down
65 changes: 37 additions & 28 deletions runtime/src/cabi/handles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
PendingCapability,
entryRefusal,
} from "../task/scheduler.ts";
import { COMPONENT_INSTANCE } from "./context.ts";
import type {
ComponentInstanceLike,
LiftLowerContext,
Expand Down Expand Up @@ -162,28 +163,35 @@ export function canonResourceNew(
}

/**
* The slice of `ComponentInstance` a dtor call needs to identify a real
* component instance (as opposed to an imported/host resource, which has no
* instance at all). `ResourceTypeInfo.impl` is typed as the
* deliberately-minimal `InstanceLike` (cabi must not depend on task/), so it
* is recognised structurally; the concrete implementor is `task/mod.ts`
* `ComponentInstanceState`. The reentrance members are inert since CM#705
* (polyengine#173) and are matched only as the structural discriminator,
* pending the contract amendment that deletes the model.
* The slice of a real component instance a dtor call needs: its handle table
* (for the poisoning walk) plus the identity `entryRefusal` keys on.
*/
interface ReentranceGate {
mayEnterFrom(caller: unknown): boolean;
enterFrom(caller: unknown): void;
leaveTo(caller: unknown): void;
interface RealComponentInstance {
handles: Iterable<unknown>;
}

function asGate(x: unknown): ReentranceGate | null {
/**
* Is `x` a REAL component instance (`task/mod.ts` `ComponentInstanceState`),
* as opposed to something that has no instance behind it at all?
*
* Two populations must answer false, and both are load-bearing for
* `callDtorGated`: an imported/host-implemented resource, whose
* `ResourceTypeInfo.impl` is `null` by construction (exec/executor.ts
* `bindImportedResources`), and the bare `{handles, mayLeave}` doubles test
* harnesses supply — neither has an instance to refuse entry into or to
* poison. So this is deliberately NOT a structural match on
* `ComponentInstanceLike`, which those doubles satisfy: it reads the
* `COMPONENT_INSTANCE` brand, declared on `ComponentInstanceState` and
* defined in ./context.ts so that cabi does not have to import task/.
*
* (It replaced a structural match on `may_enter_from`/`enter_from`/`leave_to`,
* the reentrance methods CM#705 and polyengine#173 deleted. Same population,
* by construction: `ComponentInstanceState` was their only implementor.)
*/
function isComponentInstance(x: unknown): RealComponentInstance | null {
if (x === null || typeof x !== "object") return null;
const g = x as Partial<ReentranceGate>;
return typeof g.mayEnterFrom === "function" &&
typeof g.enterFrom === "function" && typeof g.leaveTo === "function"
? (x as ReentranceGate)
return (x as Record<symbol, unknown>)[COMPONENT_INSTANCE] === true
? (x as RealComponentInstance)
: null;
}

Expand Down Expand Up @@ -234,14 +242,15 @@ export function callDtorGated(
rep: number,
caller: unknown,
): void {
const impl = asGate(rt.impl);
const impl = isComponentInstance(rt.impl);
// Always the raw synchronous dtor: `dtorHost` is the host path's lifted
// entry, which is not callable from inside a guest activation.
const dtorFn = rt.dtor;
// No gate available: an imported (host-implemented) resource has
// `impl === null` by construction (executor.ts `bindImportedResources`),
// and there is no component instance to gate entry into. Test doubles that
// supply a bare `{handles, mayLeave}` instance land here too.
// No component instance behind the resource: an imported (host-implemented)
// resource has `impl === null` by construction (executor.ts
// `bindImportedResources`), so there is no instance to refuse entry into and
// none to poison. Test doubles that supply a bare `{handles, mayLeave}`
// instance land here too — see `isComponentInstance`.
if (impl === null) {
const r = dtorFn?.(rep) as unknown;
trapIf(
Expand All @@ -250,16 +259,16 @@ export function callDtorGated(
);
return;
}
// definitions.py `entering_set` (line 230): `self_and_ancestors() -
// caller.self_and_ancestors()`. The caller is only meaningful when it is a
// real component instance; a host-initiated drop passes null, which is the
// reference's `caller = None` (Store.invoke).
const callerInst = asGate(caller) === null ? null : caller;
// The caller is only meaningful when it is a real component instance; a
// host-initiated drop passes null, which is the reference's `caller = None`
// (Store.invoke). It feeds `entryRefusal`'s `caller !== callee` guard below.
const callerInst = isComponentInstance(caller) === null ? null : caller;

// A poisoned target's refusal names the original trap (polyengine#145).
// `callerInst` can legitimately BE `impl` here (a guest dropping its own
// resource): `entryRefusal`'s vacuous-pass guard keeps that entry allowed
// even against a marked instance, matching the empty entering set.
// even against a marked instance, matching the pre-CM#705 reference's
// vacuous pass on an empty entering set.
{
const refusal = entryRefusal(
impl,
Expand Down
48 changes: 21 additions & 27 deletions runtime/src/exec/boundary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
EventCode,
withActivation,
hasRealHostCall,
dispatchableTail,
type EventTuple,
NeedsJspi,
needsJspi,
Expand Down Expand Up @@ -218,7 +217,7 @@
memory: opts.memory,
realloc: opts.realloc === null ? null : (o, os, a, n) => {
const realloc = require(opts.realloc, "realloc")!;
const p = callCore(realloc, [o, os, a, n]);

Check warning on line 220 in runtime/src/exec/boundary.ts

View workflow job for this annotation

GitHub Actions / core (ubuntu-24.04-arm)

unable to analyze dynamic import

Check warning on line 220 in runtime/src/exec/boundary.ts

View workflow job for this annotation

GitHub Actions / core (ubuntu-24.04)

unable to analyze dynamic import
trapIf(p.length !== 1 || typeof p[0] !== "number", "realloc result");
return (p[0] as number) >>> 0;
},
Expand Down Expand Up @@ -1078,18 +1077,18 @@
(t) => !queued.has(t),
);
if (parked.length === 0) {
// Every awaiting thread's settle is deferred on a non-enterable
// instance. INERT since CM#705 (polyengine#173): nothing is ever
// non-enterable now, so `dispatchableTail` never defers and this
// branch is unreachable by construction rather than by argument. Kept
// textually intact pending the contract amendment that deletes the
// reentrance model.
//
// The way out is the lock holder finishing, and the only
// await-spanning host-entry lock is the async-dtor bracket, which
// registers in `pendingHostCalls` — so park on those, plus the
// driver-arrival one-shot: every park in this loop races it, so the
// stand-down below is prompt wherever we happen to be waiting.
// UNREACHABLE BY CONSTRUCTION since polyengine#173 deleted the
// reentrance model. `parked` is `store.awaiting` minus the threads
// whose tails are already queued in `store.settled`, and we only get
// here with `awaiting` non-empty and `hasServiceableSettled()` false
// — which now means the settled queue is EMPTY, so nothing was
// excluded. (Pre-CM#705 a queue of reentrance-deferred tails answered
// false while still excluding every parked thread; issue #156. The
// way out was the lock holder finishing, and the only await-spanning
// host-entry lock was the async-dtor bracket, which registers in
// `pendingHostCalls` — hence the park below, plus the driver-arrival
// one-shot every park in this loop races.) Retained as a wedge
// detector, not as expected behavior.
if (store.pendingHostCalls.size > 0) {
await Promise.race([
...store.pendingHostCalls,
Expand Down Expand Up @@ -1150,9 +1149,9 @@
// What holds regardless is the invariant the `driverDepth` note names:
// a genuine resumption is preceded by `SuspensionPoint.resume`'s OWN
// entry (jspi/bridge.ts, minted before the settle), and every
// resumption site here re-checks membership, promise identity and
// `dispatchableTail` synchronously — mechanisms (a) and (b), which is
// where that note already puts the weight.
// resumption site here re-checks membership and promise identity
// synchronously — mechanisms (a) and (b), which is where that note
// already puts the weight.
const sole = storeDriverDepth(store) === 1;
if (sole) store.addPendingResumption(chosen);
let winner: AwaitWinner | null;
Expand Down Expand Up @@ -1182,13 +1181,7 @@
// has already consumed. Compare promise identity too.
if (
winner !== null && store.awaiting.has(winner.t) &&
winner.t.awaiting === winner.p &&
// Dispatch guard, the same predicate `Store.serviceSettled` uses
// (issue #156): never resume into an instance that is not
// host-enterable. The entry is (also) queued in `store.settled` by
// `noteAwaiting`'s continuation, and `serviceSettled` owns it once
// the lock releases.
dispatchableTail(winner.t)
winner.t.awaiting === winner.p
) {
winner.t.resumeWith(winner.value, winner.failure);
}
Expand Down Expand Up @@ -1770,18 +1763,19 @@
*
* Before #160 the host-initiated path (embedder `drop()`, the GC backstop,
* `dropOwn`) hand-rolled the bracket in cabi/handles.ts `callDtorGated`: a
* bare call to the dtor with `enterFrom(null)` HELD across the returned
* promise. Three defects followed from having no Task/Thread behind the
* bare call to the dtor with the pre-CM#705 host-entry bracket HELD across
* the returned promise. Three defects followed from having no Task/Thread behind the
* activation:
*
* - **#160 itself**: the held bracket left the impl instance non-enterable,
* so `Store.tick`'s enterability filter (#155) could never resume a
* suspension point belonging to the dtor's own activation. The completion
* promise sat in `pendingHostCalls` looking like external work, and every
* driver parked on it forever.
* - it was the runtime's only `enterFrom(null)` bracket spanning an await —
* - it was the runtime's only host-entry bracket spanning an await —
* the macro-scale reachability window of the #156 class, through which a
* sibling instance looked non-enterable from the synthetic root.
* sibling instance looked non-enterable (the shared per-instantiation
* root of the since-deleted reentrance model, polyengine#173).
* - built-ins reached inside the dtor had no ambient task (`currentTask()`
* → `PendingCapability`, or a foreign-task misattribution, the #24 class).
*
Expand Down Expand Up @@ -2028,7 +2022,7 @@
}
yield* awaitCore(core, flatArgs, thread);
task.exitImplicitThread(thread);
return;

Check warning on line 2025 in runtime/src/exec/boundary.ts

View workflow job for this annotation

GitHub Actions / core (ubuntu-24.04-arm)

unable to analyze dynamic import

Check warning on line 2025 in runtime/src/exec/boundary.ts

View workflow job for this annotation

GitHub Actions / core (ubuntu-24.04)

unable to analyze dynamic import
}

// --- callback ABI (definitions.py lines 2183-2214) ----------------------
Expand Down Expand Up @@ -2074,7 +2068,7 @@
* callback, and the scheduler delivers the SUBTASK event once the promise
* settles. This is the flagship capability of this phase: an ordinary
* `async` JS function is a valid Component Model async import.
* * sync lower — the guest's wasm frame would have to block

Check warning on line 2071 in runtime/src/exec/boundary.ts

View workflow job for this annotation

GitHub Actions / core (ubuntu-24.04-arm)

unable to analyze dynamic import

Check warning on line 2071 in runtime/src/exec/boundary.ts

View workflow job for this annotation

GitHub Actions / core (ubuntu-24.04)

unable to analyze dynamic import
* (`thread.wait_until(subtask.resolved)`, line 2286), so: `needsJspi`.
*/
export function createLoweredImport(input: {
Expand Down
Loading
Loading