diff --git a/contracts/plan-format.md b/contracts/plan-format.md index 5564966..256ff62 100644 --- a/contracts/plan-format.md +++ b/contracts/plan-format.md @@ -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. diff --git a/harness/src/xfail.ts b/harness/src/xfail.ts index 6b81c3a..cfbf832 100644 --- a/harness/src/xfail.ts +++ b/harness/src/xfail.ts @@ -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", diff --git a/runtime/src/cabi/context.ts b/runtime/src/cabi/context.ts index a8f6d33..ec10501 100644 --- a/runtime/src/cabi/context.ts +++ b/runtime/src/cabi/context.ts @@ -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; 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`. diff --git a/runtime/src/cabi/handles.ts b/runtime/src/cabi/handles.ts index 320ea15..f173538 100644 --- a/runtime/src/cabi/handles.ts +++ b/runtime/src/cabi/handles.ts @@ -19,6 +19,7 @@ import { PendingCapability, entryRefusal, } from "../task/scheduler.ts"; +import { COMPONENT_INSTANCE } from "./context.ts"; import type { ComponentInstanceLike, LiftLowerContext, @@ -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; } -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; - return typeof g.mayEnterFrom === "function" && - typeof g.enterFrom === "function" && typeof g.leaveTo === "function" - ? (x as ReentranceGate) + return (x as Record)[COMPONENT_INSTANCE] === true + ? (x as RealComponentInstance) : null; } @@ -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( @@ -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, diff --git a/runtime/src/exec/boundary.ts b/runtime/src/exec/boundary.ts index 05513ba..2d95042 100644 --- a/runtime/src/exec/boundary.ts +++ b/runtime/src/exec/boundary.ts @@ -34,7 +34,6 @@ import { EventCode, withActivation, hasRealHostCall, - dispatchableTail, type EventTuple, NeedsJspi, needsJspi, @@ -1078,18 +1077,18 @@ async function driveAsync( (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, @@ -1150,9 +1149,9 @@ async function driveAsync( // 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; @@ -1182,13 +1181,7 @@ async function driveAsync( // 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); } @@ -1770,8 +1763,8 @@ function dtorOptions(instance: ComponentInstanceState): ResolvedOptions { * * 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, @@ -1779,9 +1772,10 @@ function dtorOptions(instance: ComponentInstanceState): ResolvedOptions { * 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). * diff --git a/runtime/src/task/mod.ts b/runtime/src/task/mod.ts index fb35eac..2f59f21 100644 --- a/runtime/src/task/mod.ts +++ b/runtime/src/task/mod.ts @@ -13,6 +13,7 @@ // defers that feature with memory64. import { Table } from "../cabi/handles.ts"; +import { COMPONENT_INSTANCE } from "../cabi/context.ts"; import type { ComponentInstanceLike } from "../cabi/context.ts"; import type { ComponentValue, FuncType } from "../cabi/types.ts"; import { assert_, trapIf } from "../cabi/trap.ts"; @@ -37,17 +38,6 @@ export * from "./waitable.ts"; export * from "./subtask.ts"; export * from "./streams.ts"; -/** - * Synthetic-root registry: one root per `Store` (see - * `ComponentInstanceState.rootOf`). A `WeakMap` so a dead store's root dies - * with it. - */ -const syntheticRoots = new WeakMap(); -/** `index` of the synthetic root — outside the real instance index space. */ -const ROOT_INDEX = -1; -/** Constructor marker: "this one IS the root, do not give it a parent". */ -const ROOT_TOKEN = Symbol("synthetic-root"); - /** Anything a component instance's handle table can hold. */ export type HandleTableEntry = unknown; @@ -58,8 +48,19 @@ export type HandleTableEntry = unknown; * `mayLeave` is backed by a real `WebAssembly.Global(i32, mutable)` because * FACT adapters import that global (`flags` namespace) and read/write it as * the may_leave boolean (wasmtime 47 FACT treats the whole flags global as - * may_leave; there is no bitmask). Initial value 1 (true). `mayEnter` is - * host-side state: nothing wasm-visible reads it. + * may_leave; there is no bitmask). Initial value 1 (true). + * + * There is no `may_enter` counterpart and no instance tree: upstream + * component-model PR #705 (definitions.py @ 2f13265) deleted `may_enter`, + * `parent`, `entering_set`, `enter_from` and `leave_to` outright, so nothing + * gates entry into a live instance. polyengine#173 followed. What polyengine + * keeps beyond the reference is per-instance POISONING — a named divergence + * living entirely in ./scheduler.ts (`isInstancePoisoned`, `entryRefusal`), + * not in any state on this class. + * + * `COMPONENT_INSTANCE` brands this class as a real component instance for + * the layers that only see the structural `ComponentInstanceLike` + * (cabi/handles.ts `isComponentInstance`; cabi must not import task/). */ export class ComponentInstanceState implements ComponentInstanceLike { readonly index: number; @@ -67,83 +68,20 @@ export class ComponentInstanceState implements ComponentInstanceLike { handles: Table = new Table(); /** definitions.py `ComponentInstance.threads` — a Table, so `thread.index`. */ readonly threads: Table = new Table(); - mayEnter = true; + /** cabi's real-instance discriminator; see the class doc. */ + readonly [COMPONENT_INSTANCE] = true; /** definitions.py `backpressure: int` — a *counter* (backpressure.{inc,dec}). */ backpressure = 0; /** definitions.py `num_waiting_to_enter`. */ numWaitingToEnter = 0; /** definitions.py `exclusive_thread`. */ exclusiveThread: Thread | null = null; - /** - * definitions.py `ComponentInstance.parent`. - * - * The plan still gives us a flat instance space, but the tree is no longer - * needed: every instance of one instantiation gets the same **synthetic - * root** as its parent (contracts/plan-format.md v3 amendment 4 / - * polyengine#101). See `enteringSet` for why that is observably equivalent to - * the real chain. The root itself has no parent. - */ - parent: ComponentInstanceState | null; readonly store: Store; - /** - * The synthetic per-instantiation root (v3 amendment 4). One per `Store`: - * a `Store` is exactly one component instantiation's scheduling scope, so - * "all `ComponentInstanceState`s sharing a `Store`" is the set that shares - * a top-level component — which is the granularity wasmtime's own - * top-level-instance-id comparison uses (concurrent.rs:1876-1886). - * - * It is a real `ComponentInstanceState` (index -1) rather than a bare flag - * so it flows through `selfAndAncestors`/`enteringSet` unchanged; its - * handle table stays empty and no task ever runs on it. - */ - static rootOf(store: Store): ComponentInstanceState { - let root = syntheticRoots.get(store); - if (root === undefined) { - root = new ComponentInstanceState(ROOT_INDEX, store, ROOT_TOKEN); - syntheticRoots.set(store, root); - } - return root; - } - - /** Is this the synthetic root (never a real component instance)? */ - get isSyntheticRoot(): boolean { - return this.index === ROOT_INDEX && this.parent === null; - } - - constructor(index: number, store?: Store, root?: typeof ROOT_TOKEN) { + constructor(index: number, store?: Store) { this.index = index; this.store = store ?? new Store(); this.flags = new WebAssembly.Global({ value: "i32", mutable: true }, 1); - // The root is its own tree's top; everything else hangs off it. Built - // lazily here so no call site has to remember to wire it up. - this.parent = root === ROOT_TOKEN - ? null - : ComponentInstanceState.rootOf(this.store); - } - - /** - * Release the synthetic root after a trap broke the enter/leave bracket - * (v3 amendment 4, and a **named divergence** from the reference). - * - * definitions.py poisons the whole entering set: `Store.lift` never reaches - * `leave_to`, so the root — which is in every host entry's entering set — - * stays `may_enter == False` forever and NO instance of the component can - * be entered again. wasmtime is the same by other means (it poisons the - * store). polyengine deliberately supports post-trap re-entry of instances the - * trap did not touch (exec/boundary.ts `poison`: "sibling instances stay - * usable, which is why the lock is released per-instance rather than by - * poisoning a whole store the way wasmtime does"), and the synthetic root - * must not silently convert that documented divergence into store-wide - * poisoning. So a trap poisons the LEAF set only, and the root is released - * here — the reentrance gate the root exists for (a *second, concurrent* - * host entry) is about a live entry, and after a trap unwinds to the host - * there is none. - */ - releaseSyntheticRootOnPoison(): void { - for (const inst of this.selfAndAncestors()) { - if (inst.isSyntheticRoot) inst.mayEnter = true; - } } get mayLeave(): boolean { @@ -154,87 +92,6 @@ export class ComponentInstanceState implements ComponentInstanceLike { this.flags.value = v ? 1 : 0; } - /** definitions.py `ComponentInstance.self_and_ancestors` (line 236). */ - selfAndAncestors(): Set { - const s = new Set([this]); - let a = this.parent; - while (a !== null) { - s.add(a); - a = a.parent; - } - return s; - } - - /** - * definitions.py `ComponentInstance.entering_set` (line 230): - * `self_and_ancestors() - caller.self_and_ancestors()`. - * - * CONTRACT (contracts/plan-format.md v3 amendment 4, polyengine#101): the plan - * still carries no wire form for the component-instance tree, and it no - * longer needs one. Every instance's parent is the synthetic - * per-instantiation root, so: - * - * * host entry (`caller === null`): `{this, root}` — the reference's - * entering set for a host entry is `self_and_ancestors()`, which always - * contains the top-level root, so a second host entry anywhere in the - * tree trips on the root either way. This is the divergence #101 - * reported (host -> A.f -> host import -> host enters a *different* - * instance): now caught. - * * guest-to-guest (`caller !== null`): `{this}` — the root is in the - * caller's ancestor set and cancels out. Intermediate ancestors would - * be the only difference from the real chain, and they are never - * reachably consulted: FACT compiles same-instance and ancestor calls - * to unconditional compile-time traps, and sibling cycles are - * unreachable because instance imports form a DAG (polyengine#99 - * adjudication). - * - * So the synthetic root is observably equivalent to the full chain, and it - * matches wasmtime's own shortcut — a top-level instance-id comparison - * (concurrent.rs:1876-1886) — by construction. This reopens only if some - * future upstream shape makes nesting depth observable. - * - * One deliberate departure remains, at the trap path rather than here: see - * `releaseSyntheticRootOnPoison`. - */ - enteringSet(caller: ComponentInstanceState | null): Set { - const mine = this.selfAndAncestors(); - if (caller === null) return mine; - for (const c of caller.selfAndAncestors()) mine.delete(c); - return mine; - } - - /** definitions.py `ComponentInstance.may_enter_from` (line 214). */ - mayEnterFrom(caller: ComponentInstanceState | null): boolean { - for (const inst of this.enteringSet(caller)) { - if (!inst.mayEnter) return false; - } - return true; - } - - /** definitions.py `ComponentInstance.enter_from` (line 220). */ - enterFrom(caller: ComponentInstanceState | null): void { - for (const inst of this.enteringSet(caller)) { - assert_(inst.mayEnter, "enter_from without may_enter"); - inst.mayEnter = false; - } - } - - /** definitions.py `ComponentInstance.leave_to` (line 225). */ - leaveTo(caller: ComponentInstanceState | null): void { - for (const inst of this.enteringSet(caller)) { - assert_(!inst.mayEnter, "leave_to without a matching enter_from"); - inst.mayEnter = true; - } - } - - /** Backwards-compatible host-entry helpers (the M0 spelling). */ - enter(): void { - this.enterFrom(null); - } - - leave(): void { - this.leaveTo(null); - } } /** definitions.py `Task.State` (line 445). */ diff --git a/runtime/src/task/scheduler.ts b/runtime/src/task/scheduler.ts index d72658a..65c66c6 100644 --- a/runtime/src/task/scheduler.ts +++ b/runtime/src/task/scheduler.ts @@ -165,23 +165,6 @@ export function isInstancePoisoned(inst: object): boolean { return poisonedInstances.has(inst); } -/** - * May a settled activation tail for parked thread `t` be DISPATCHED now - * (issue #156)? True iff its instance is host-enterable — `Thread.resumeWith` - * brackets the resumption with `enterFrom(null)` — or POISONED, in which case - * `resumeWith`'s early return retires it and deferring would leak forever. - * - * CONTRACT: a parked entry without a reachable `task.inst` (the partial - * thread doubles the host-pump tests park in `Store.awaiting`) holds no - * reentrance state, so there is nothing to defer on: dispatchable. - */ -// deno-lint-ignore no-explicit-any -export function dispatchableTail(t: any): boolean { - const inst = t?.task?.inst; - if (inst === undefined || inst === null) return true; - return isInstancePoisoned(inst) || inst.mayEnterFrom(null); -} - /** * The recorded cause of an instance's poisoning: the original trap that * broke the enter/leave bracket (polyengine#145). `undefined` when the instance @@ -219,10 +202,10 @@ export function withPoisonCause(inst: object, base: string): string { * (definitions.py @ 2f13265) `may_enter`, `entering_set`, `enter_from`, * `leave_to` and `ComponentInstance.parent` no longer exist — `Store.lift` * runs `canon_lift` with no gate at all, so host-mediated reentrance into a - * live instance is simply VALID. The clause that consulted `mayEnterFrom` - * was deleted with the pin advance; #251's re-key onto the marker is what - * made that deletion a pure subtraction (the marker never depended on - * `may_enter`). + * live instance is simply VALID. The clause that consulted the transient + * gate was deleted with the pin advance and the model itself with + * polyengine#173; #251's re-key onto the marker is what made those deletions + * pure subtractions (the marker never depended on `may_enter`). * * What survives is polyengine's NAMED DIVERGENCE: per-instance poisoning. A * trapped instance is a corpse — entry is refused permanently, with the @@ -1018,35 +1001,20 @@ export class Store { * throw (trap unwinding); callers propagate or park it exactly as they do * for `tick`. * - * A tail whose instance is NOT host-enterable is DEFERRED IN PLACE — left - * in the queue, skipped here — until the lock releases (issue #156). - * NOTE (polyengine#173, CM#705): with the transient reentrance gate gone, - * `mayEnterFrom(null)` is constant-true and this deferral is INERT — every - * non-stale tail is dispatched immediately. The machinery is kept textually - * intact pending the contract amendment that deletes the model; the - * paragraphs below record why it existed. - * * `resumeWith` brackets the resumption with `enterFrom(null)`, and under - * the shared synthetic per-instantiation root a host entry into ANY - * instance of the graph locks the root, so while one instance is entered a - * sibling's tail cannot be dispatched: dispatching it tripped - * `resumeWith`'s enterability assert (which, mutating before asserting, - * also stranded the thread and lost the settle). + * Every non-stale tail is dispatched immediately, in queue order. (History, + * issue #156: tails whose instance was not host-enterable were deferred in + * place until the reentrance lock released. CM#705 / polyengine#173 deleted + * the reentrance model, so there is nothing left to defer on.) * - * Deferral is safe because `!inst.mayEnterFrom(null)` is EXACTLY `tick`'s - * candidate-filter predicate on the same instance: while a tail of `inst` - * is deferred, `tick` cannot resume any thread of `inst` either, so the - * phantom-state gate the queue exists to enforce is preserved per-instance - * by construction. + * The ordering discipline is therefore settle order, full stop — and it is + * the reason this queue exists rather than a direct resumption from the + * settle continuation: in definitions.py the tail runs atomically inside + * the entered bracket, so the phantom-state gate (`tick` refuses while an + * unserviced tail is queued, see `hasServiceableSettled`) is what keeps a + * parked activation's tail from being observed out of order. * - * The ordering discipline is therefore per-instance settle order. Cross- - * instance order relaxes only when enterability defers a tail, which is - * conforming schedule nondeterminism: in definitions.py the tail runs - * atomically inside the entered bracket, so a host entry admitted during a - * park necessarily orders before the parked activation's tail there. - * - * A POISONED instance's tail is still dispatched: `resumeWith`'s poison - * early-return retires it, and deferring it would leak forever — a - * poisoned leaf keeps its lock permanently. + * A POISONED instance's tail is dispatched like any other: `resumeWith`'s + * poison early-return retires it, so it drains rather than leaking. */ serviceSettled(): boolean { let did = false; @@ -1062,7 +1030,6 @@ export class Store { this.settled.splice(i, 1); continue scan; } - if (!dispatchableTail(s.t)) continue; this.settled.splice(i, 1); (s.t as { resumeWith(v: unknown, f?: { error: unknown }): void; @@ -1076,17 +1043,19 @@ export class Store { } /** - * "Would a `serviceSettled` call make progress right now?" — i.e. some - * entry is stale (would be removed) or serviceable (would be dispatched). - * A queue holding ONLY deferred tails (issue #156) answers false: `tick` - * must not be gated by them, and the driving loops must not spin on them. + * "Would a `serviceSettled` call make progress right now?" — i.e. is any + * entry queued at all. Every entry either dispatches or is dropped as + * stale, so a non-empty queue always makes progress. + * + * It exists to gate `tick` (and to keep the driving loops from parking) + * behind unserviced tails: resuming some other thread while a settled tail + * waits would expose the out-of-order state the queue is there to prevent. + * (Pre-CM#705 this had to inspect each entry, because a queue holding only + * reentrance-DEFERRED tails had to answer false — issue #156. That case is + * gone with the reentrance model, polyengine#173.) */ hasServiceableSettled(): boolean { - for (const s of this.settled) { - if (!this.awaiting.has(s.t)) return true; - if (dispatchableTail(s.t)) return true; - } - return false; + return this.settled.length > 0; } /** @@ -1221,9 +1190,9 @@ export class Store { // // Routed through `notifyInstancePoisoned` (not the raw hook) so the // poison MARKER is recorded too (polyengine#145): `Thread.resumeWith`'s - // quiet-retire of late settled tails and `dispatchableTail`'s - // dispatch-or-defer decision (#156) both read it, and without the - // marker a settled tail of this instance would defer forever. + // quiet-retire of late settled tails (#156) and `entryRefusal` both + // read it; without the marker a settled tail of this dead instance + // would be resumed as if healthy. notifyInstancePoisoned( inst as unknown as { handles: Iterable }, e, diff --git a/runtime/src/task/streams.ts b/runtime/src/task/streams.ts index e34d445..86a7c0f 100644 --- a/runtime/src/task/streams.ts +++ b/runtime/src/task/streams.ts @@ -1000,32 +1000,18 @@ interface PoisonedInstanceLike { * can no longer be entered (`tick` excludes poisoned instances). Host sentinels are * not instances at all, so they are always notified. * - * #100: THE HEALTH TEST IS "POISONED", NOT "`mayEnter === false`". The - * original test used non-enterability as a proxy for deadness. Settled twice - * over now: CM#705 (polyengine#173) deleted `may_enter` from the reference - * outright, so the marker is not merely the better test but the only one - * left. The original argument, kept because it explains what the marker is - * for — the proxy was unsound in one direction, and the unsoundness stranded - * healthy tasks: - * - * * (sound half, #84 audit) a healthy guest peer always parks with - * `mayEnter === true`. Every park — the callback ABI's waitable-set wait, - * and equally a sync-lowered/JSPI peer blocked inside `finishCopy`'s - * SITE 4 via `blockCurrentActivation` — yields the thread out of the - * scheduler's enter/leave bracket, and the bracket's `leaveTo` ran on the - * way out — `Thread.resumeWith`'s resume-side - * `assert_(mayEnterFrom(null))` would have fired otherwise. Blocking - * inside a wasm frame did NOT hold the enter bracket. (Both the bracket - * and that assert are gone as of polyengine#173.) - * * (unsound converse) `mayEnter === false` does not imply "poisoned". An - * instance that is merely mid-call is also non-enterable, and a CALLER - * instance stays non-enterable for the whole duration of a - * cross-component (FACT) call into an instance that traps - * (`ComponentInstanceState.enterFrom` clears `mayEnter` on the callee's - * entering set only, task/mod.ts). A *different*, healthy task of that - * caller, parked on an end of a stream/future the trapping callee also - * held, was classified dead here and retired silently — stranded, the - * exact outcome #66 exists to prevent. + * #100: THE HEALTH TEST IS "POISONED", NOT NON-ENTERABILITY. Historical: the + * original test used the transient reentrance gate (`may_enter === False`) as + * a proxy for deadness. That is settled twice over — CM#705 / polyengine#173 + * deleted the whole reentrance model, so the poison marker is not merely the + * better test but the only one left — and the original argument is kept only + * because it explains what the marker is FOR: the proxy was unsound in one + * direction, and the unsoundness stranded healthy tasks. A caller instance + * stayed non-enterable for the whole duration of a cross-component (FACT) + * call into an instance that trapped, so a *different*, healthy task of that + * caller, parked on an end of a stream/future the trapping callee also held, + * was classified dead here and retired silently — stranded, the exact + * outcome #66 exists to prevent. * * So the test consults the poison marker itself. It is per-instance and * recorded at the single seam every poisoning site routes through @@ -1035,17 +1021,15 @@ interface PoisonedInstanceLike { * and it is recorded *before* the retirement walk runs, so an instance's own * parked ends still see it during its own walk. `retiredInstances` is * consulted alongside it because the walk is also reachable directly (it is - * set at walk entry, so the two agree); neither ever contains the synthetic - * per-instantiation root, which every poison site skips or releases (plan v3 - * amendment 4, `releaseSyntheticRootOnPoison`). + * set at walk entry, so the two agree). * * Why this does not re-open review B2 (phantom events into a corpse): the * concern is that a DROPPED event queued onto a waitable of an instance that * can never be entered again would be serviced by a later driving loop and * resume machinery `tick` deliberately excludes. "Can never be entered - * again" is precisely poisoning — a mid-call instance's `mayEnter` is - * restored by its own `leaveTo` when the call returns, and its parked task - * then resumes normally and consumes the event. The narrowed predicate + * again" is precisely poisoning — a merely mid-call instance is entirely + * ordinary, and its parked task resumes normally and consumes the event. + * The narrowed predicate * therefore excludes exactly the population B2 is about, and admits only * peers that will run again. * diff --git a/runtime/tests/cancel_bracket_race_test.ts b/runtime/tests/cancel_bracket_race_test.ts index 48e6a41..33af676 100644 --- a/runtime/tests/cancel_bracket_race_test.ts +++ b/runtime/tests/cancel_bracket_race_test.ts @@ -7,7 +7,7 @@ // built-ins, `exit-sync-call`, etc.) runs on a LATER microtask. A concurrent // host EXPORT call can enter the same instance in between. // -// Originally the delivery was wrapped in an `enterFrom`/`leaveTo` bracket and +// Originally the delivery was wrapped in a host-entry bracket and // the question was whether that bracket closed too early. CM#705 // (polyengine#173) removed the bracket — and the whole reentrance gate — // from the reference and from this runtime, so the concurrent entry is now diff --git a/runtime/tests/cross_store_driver_test.ts b/runtime/tests/cross_store_driver_test.ts index baa951d..e9e16c0 100644 --- a/runtime/tests/cross_store_driver_test.ts +++ b/runtime/tests/cross_store_driver_test.ts @@ -31,9 +31,6 @@ function assert(cond: boolean, msg: string): asserts cond { function fakeInst() { return { - mayEnterFrom: (_: unknown) => true, - enterFrom: (_: unknown) => {}, - leaveTo: (_: unknown) => {}, }; } diff --git a/runtime/tests/deferred_test.ts b/runtime/tests/deferred_test.ts index de9b84c..c047013 100644 --- a/runtime/tests/deferred_test.ts +++ b/runtime/tests/deferred_test.ts @@ -57,8 +57,7 @@ const deferred: [name: string, reason: string][] = [ "test_cross_component_realloc", "needs the component instance *tree* (ComponentInstance.parent) so a " + "callee can reach a caller's realloc across a nested lift; the plan has " + - "no wire form for instance nesting (see the CONTRACT note on " + - "ComponentInstanceState.enteringSet) — v0.3 contract friction, not a " + + "no wire form for instance nesting — v0.3 contract friction, not a " + "scheduler gap", ], [ diff --git a/runtime/tests/dtor_normalization_test.ts b/runtime/tests/dtor_normalization_test.ts index 75c058c..02ef73c 100644 --- a/runtime/tests/dtor_normalization_test.ts +++ b/runtime/tests/dtor_normalization_test.ts @@ -4,7 +4,7 @@ // into a function instance and calls it through `Store.lift` with // `CanonicalOptions(async_ = False)` / `FuncType([U32Type()], [], async_ = // False)`. Before #160 the host-initiated path called `rt.dtor` bare while -// HOLDING `enterFrom(null)` across the returned promise, which produced two +// HOLDING a host-entry bracket across the returned promise, which produced two // observable defects pinned below: // // 1. the dtor's own suspension points were unresumable — `Store.tick`'s @@ -12,7 +12,7 @@ // host-enterable, and the held bracket made the impl exactly that, so // the completion promise (parked in `pendingHostCalls`, i.e. advertised // as *external* work) never settled and every driver waited forever; -// 2. the held bracket also locked the synthetic per-instantiation root for +// 2. the held bracket also locked the per-instantiation root for // the whole activation, so a SIBLING instance of the same component // looked non-enterable from the host — the macro-scale window of the // #156 class. diff --git a/runtime/tests/embedder/trap_retire_test.ts b/runtime/tests/embedder/trap_retire_test.ts index 0c55a05..672848a 100644 --- a/runtime/tests/embedder/trap_retire_test.ts +++ b/runtime/tests/embedder/trap_retire_test.ts @@ -2,7 +2,7 @@ // stream/future operation (contracts/embedder-api.md amendment A7). // // Mechanism under test: a trap breaks the enter/leave bracket, the instance -// is poisoned (mayEnter stays false forever), and the retirement walk +// is poisoned (permanently, by the poison marker), and the retirement walk // (task/streams.ts `retireInstanceAsyncEnds`, hooked at both bracket-break // sites) drops every live stream/future end in the poisoned table and // records the failure — so parked host peers settle and the conventions diff --git a/runtime/tests/host_pump_test.ts b/runtime/tests/host_pump_test.ts index 8b7ed2e..fd6ee4f 100644 --- a/runtime/tests/host_pump_test.ts +++ b/runtime/tests/host_pump_test.ts @@ -42,9 +42,6 @@ function assert(cond: boolean, msg: string): asserts cond { /** The slice of `ComponentInstance` that `Store.tick` touches. */ function fakeInst() { return { - mayEnterFrom: (_: unknown) => true, - enterFrom: (_: unknown) => {}, - leaveTo: (_: unknown) => {}, }; } diff --git a/runtime/tests/integration/e2e_async_test.ts b/runtime/tests/integration/e2e_async_test.ts index a31d35f..dcf9155 100644 --- a/runtime/tests/integration/e2e_async_test.ts +++ b/runtime/tests/integration/e2e_async_test.ts @@ -94,7 +94,6 @@ Deno.test("async-probe: the task model is left clean after the call", async () = const f = component.exports["wait-then-double"] as (x: number) => unknown; assertEq(await f(1), 2); const inst = component.componentInstances[0]; - assertEq(inst.mayEnter, true); assertEq(inst.mayLeave, true); // definitions.py `Task.exit_implicit_thread`: the exclusive thread is // released and the instance's thread table is empty again. @@ -153,7 +152,6 @@ Deno.test("async-probe: a terminating activation leaves nothing behind", async ( // released and the instance's thread table is empty again. assertEq(inst.exclusiveThread, null); assertEq([...inst.threads].length, 0); - assertEq(inst.mayEnter, true); // Nothing parked: neither on a scheduler condition nor mid-wasm-call. const store = (inst as unknown as { store: { waiting: unknown[]; awaiting: Set }; diff --git a/runtime/tests/integration/e2e_hello_test.ts b/runtime/tests/integration/e2e_hello_test.ts index 128dac9..450dcd4 100644 --- a/runtime/tests/integration/e2e_hello_test.ts +++ b/runtime/tests/integration/e2e_hello_test.ts @@ -63,9 +63,10 @@ Deno.test("hello: full pipeline shim -> plan -> executor -> greet()", async () = assertEq(component.stats.liftedCalls, 1); assertEq(component.stats.tasksResolved, 1); - // Reentrance gates released after the sync call resolved. + // The may_leave flag is released after the sync call resolved. (There is + // no may_enter counterpart any more: CM#705 / polyengine#173 deleted the + // transient reentrance model.) const inst = component.componentInstances[0]; - assert(inst.mayEnter, "may_enter must be restored after call"); assert(inst.mayLeave, "may_leave must be restored after call"); assertEq(inst.flags.value, 1); diff --git a/runtime/tests/integration/e2e_values_test.ts b/runtime/tests/integration/e2e_values_test.ts index 69f93b1..586558a 100644 --- a/runtime/tests/integration/e2e_values_test.ts +++ b/runtime/tests/integration/e2e_values_test.ts @@ -115,7 +115,6 @@ Deno.test("values: every call went through the task model", () => { assertEq(component.stats.liftedCalls, component.stats.tasksResolved); assertEq(component.stats.liftedCalls > 0, true); const inst = component.componentInstances[0]; - assertEq(inst.mayEnter, true); // `inst.threads` is the reference's `Table[Thread]` (definitions.py // `ComponentInstance.threads`), so "no threads left" is an empty iteration. assertEq([...inst.threads].length, 0); diff --git a/runtime/tests/resource_lifetime_test.ts b/runtime/tests/resource_lifetime_test.ts index 2718e4d..6d175bd 100644 --- a/runtime/tests/resource_lifetime_test.ts +++ b/runtime/tests/resource_lifetime_test.ts @@ -184,7 +184,7 @@ Deno.test("#85: a guest-initiated dtor that does not finish synchronously traps" Deno.test("#160: a host-initiated async dtor is not external work", async () => { // REVISED from the #85 pin "holds the gate until it settles". That - // behaviour was the bug: the held `enterFrom(null)` bracket made the impl + // behaviour was the bug: the held host-entry bracket made the impl // instance non-enterable for the whole activation, so `Store.tick`'s // enterability filter could never resume a suspension point belonging to // the dtor itself (#160). A host-initiated dtor is a full canonical lift diff --git a/runtime/tests/same_store_driver_test.ts b/runtime/tests/same_store_driver_test.ts index cb711fc..d724ae8 100644 --- a/runtime/tests/same_store_driver_test.ts +++ b/runtime/tests/same_store_driver_test.ts @@ -45,9 +45,6 @@ function assert(cond: boolean, msg: string): asserts cond { function fakeInst() { return { - mayEnterFrom: (_: unknown) => true, - enterFrom: (_: unknown) => {}, - leaveTo: (_: unknown) => {}, }; } diff --git a/runtime/tests/settled_deferral_test.ts b/runtime/tests/settled_deferral_test.ts index 79f57b8..5ee8fd3 100644 --- a/runtime/tests/settled_deferral_test.ts +++ b/runtime/tests/settled_deferral_test.ts @@ -2,13 +2,13 @@ // // #156's shape: instance B's thread parked on an already-settled // `awaitValue` (tail queued in `store.settled`) while sibling instance A held -// a host entry, which under the shared synthetic per-instantiation root made -// B non-enterable — so B's tail was DEFERRED IN PLACE and `driveAsync` had to -// park (not spin) until the lock released. +// a host entry, which under the transient reentrance model's shared +// per-instantiation root made B non-enterable — so B's tail was DEFERRED IN +// PLACE and `driveAsync` had to park (not spin) until the lock released. // // Deferral can no longer occur: definitions.py @ 2f13265 has no -// `may_enter`/`enter_from`/`leave_to`, the runtime takes no bracket anywhere, -// and `dispatchableTail` is constant-true for a live instance. The pin below +// `may_enter`/`enter_from`/`leave_to`, polyengine#173 deleted the model here +// too, and every non-stale tail dispatches on the spot. The pin below // is the merged behavior — the tail dispatches immediately, with an unrelated // outstanding host call in flight, and the driver still reaches quiescence // (the outstanding call must not be mistaken for a reason to wedge). diff --git a/runtime/tests/settlement_pump_test.ts b/runtime/tests/settlement_pump_test.ts index 3085aa6..4b1b731 100644 --- a/runtime/tests/settlement_pump_test.ts +++ b/runtime/tests/settlement_pump_test.ts @@ -31,9 +31,6 @@ function assert(cond: boolean, msg: string): asserts cond { /** The slice of `ComponentInstance` that `Store.tick` touches. */ function fakeInst() { return { - mayEnterFrom: (_: unknown) => true, - enterFrom: (_: unknown) => {}, - leaveTo: (_: unknown) => {}, }; } diff --git a/runtime/tests/streams_teardown_test.ts b/runtime/tests/streams_teardown_test.ts index c467f6a..937532f 100644 --- a/runtime/tests/streams_teardown_test.ts +++ b/runtime/tests/streams_teardown_test.ts @@ -346,7 +346,8 @@ Deno.test("#84(c'): a spec-dropped future (the writer delivered its value) is un Deno.test("#84: one end's failing notification does not strand the remaining ends", () => { const store = new Store(); const inst = new ComponentInstanceState(0, store); - const peer = { mayEnter: true }; + // A non-instance peer stand-in: not poisoned, so it is notified normally. + const peer = {}; // End 1: a stream whose parked peer's settler throws (a host callback, an // event thunk — anything the notification runs). @@ -534,7 +535,7 @@ Deno.test("#97: cancelRead resolves the read exactly like end-of-stream does", a // cross-component (FACT) call into instance B (callee). A DIFFERENT, // perfectly healthy task of A is parked on an end of a stream/future whose // peer end B holds. B traps; the poisoning walk runs over B's table and -// reaches A's parked side. The old health test (`mayEnter === false`) read A +// reaches A's parked side. The old health test (non-enterability) read A // as a corpse — A was non-enterable merely because it was mid-call — and // retired it in silence: stranded, the outcome #66 exists to prevent. The // narrowed test (task/scheduler.ts's per-instance poison marker, recorded at @@ -588,7 +589,7 @@ function inTask(inst: ComponentInstanceState, fn: () => T): T { /** * `caller` is mid-cross-component-call into `callee`. Post-CM#705 that state - * carries no instance-level flag at all (the pre-#705 `enterFrom` chain, whose + * carries no instance-level flag at all (the pre-#705 host-entry chain, whose * cleared `may_enter` on BOTH instances is what the old health test tripped * over, no longer exists), so this is documentation: neither instance is * marked, and only the marker decides. diff --git a/runtime/tests/task_test.ts b/runtime/tests/task_test.ts index 1c01a04..261d3e2 100644 --- a/runtime/tests/task_test.ts +++ b/runtime/tests/task_test.ts @@ -13,7 +13,6 @@ import { type Cancelled, chooseCandidate, ComponentInstanceState, - dispatchableTail, driveSyncLift, entryRefusal, EventCode, @@ -196,85 +195,24 @@ Deno.test("canon_lift sync loop: traps when no thread can make progress", () => }); // --------------------------------------------------------------------------- -// The RETAINED-BUT-INERT reentrance model (ComponentInstance.enter_from / -// may_enter_from) +// Post-CM#705 entry semantics (polyengine#173) // --------------------------------------------------------------------------- // // CM#705 (definitions.py @ 2f13265) removed `may_enter`, `entering_set`, -// `enter_from`, `leave_to` and `ComponentInstance.parent` outright, and -// polyengine#173 removed every CALL to them from the runtime: nothing gates -// entry any more except the poison marker. The model's own arithmetic is -// still defined in task/mod.ts pending the contract amendment that deletes -// it, and these tests pin that arithmetic so the deletion PR is a clean -// subtraction. They assert NOTHING about runtime behavior — no runtime path -// consults them. - -Deno.test("reentrance: an instance cannot be re-entered from the host", () => { - const inst = new ComponentInstanceState(0); - assertEq(inst.mayEnterFrom(null), true); - inst.enterFrom(null); - assertEq(inst.mayEnterFrom(null), false); - inst.leaveTo(null); - assertEq(inst.mayEnterFrom(null), true); -}); - -Deno.test("reentrance: the entering set excludes the caller's own ancestry", () => { - const inst = new ComponentInstanceState(0); - // definitions.py `entering_set`: `self_and_ancestors() - caller.self_and_ancestors()`. - // A call from an instance to *itself* enters nothing, so it is always - // permitted — that is what makes a self-recursive lift legal. - assertEq([...inst.enteringSet(inst)].length, 0); - inst.enterFrom(null); - assertEq(inst.mayEnterFrom(inst), true); -}); - -// --------------------------------------------------------------------------- -// Synthetic per-instantiation root (plan v3 amendment 4 / polyengine#101) -// --------------------------------------------------------------------------- - -Deno.test("root: a host entry locks the whole instantiation, not just the leaf", () => { - const store = new Store(); - const a = new ComponentInstanceState(0, store); - const b = new ComponentInstanceState(1, store); - assertEq(a.parent === b.parent, true, "siblings share one synthetic root"); - assertEq(a.parent!.isSyntheticRoot, true); - - // definitions.py `entering_set(None)` = `self_and_ancestors()`, which in the - // reference always contains the top-level root. So a host entry into A - // forbids a host entry into B — the reachable divergence #101 reported. - assertEq([...a.enteringSet(null)].length, 2, "{leaf, root}"); - a.enterFrom(null); - assertEq(a.mayEnterFrom(null), false); - assertEq(b.mayEnterFrom(null), false, "the sibling is locked through the root"); - a.leaveTo(null); - assertEq(b.mayEnterFrom(null), true); -}); - -Deno.test("root: instantiations are independent", () => { - const a = new ComponentInstanceState(0, new Store()); - const b = new ComponentInstanceState(0, new Store()); - a.enterFrom(null); - assertEq(b.mayEnterFrom(null), true, "a different Store is a different root"); -}); - -Deno.test("root: guest-to-guest entering sets are unchanged ({leaf})", () => { - const store = new Store(); - const a = new ComponentInstanceState(0, store); - const b = new ComponentInstanceState(1, store); - // The root cancels out (it is in the caller's ancestry), so a guest->guest - // call locks only the callee — the DAG adjudication (#99/#101). - const set = [...b.enteringSet(a)]; - assertEq(set.length, 1); - assertEq(set[0] === b, true); - // ...and a call to oneself still enters nothing. - assertEq([...a.enteringSet(a)].length, 0); -}); +// `enter_from`, `leave_to` and `ComponentInstance.parent` outright; +// polyengine#173 removed every call to them and then the definitions +// themselves, along with the per-instantiation root the plan used to stand +// in for the instance tree. Nothing gates entry any more except +// polyengine's own per-instance poison marker (pinned further below). +// +// What follows pins the MERGED semantics: the shapes that the deleted model +// used to forbid, and that must now proceed. -Deno.test("root: tick resumes a ready sibling thread during a live host entry", () => { +Deno.test("cm705: tick resumes a ready sibling thread during a live host entry", () => { // INVERTED by polyengine#173 (CM#705). This shape used to be the #155 // regression: `tick` filtered its candidates on host-enterability, and - // under the shared synthetic root a host entry into A made every sibling - // non-enterable, so B could not run until A's call returned. + // under that model's shared per-instantiation root a host entry into A made + // every sibling non-enterable, so B could not run until A's call returned. // // The merged reference (definitions.py @ 2f13265 `Store.tick`) resumes any // ready thread with no gate and no bracket, so B runs immediately. @@ -322,8 +260,8 @@ Deno.test("root: tick resumes a ready sibling thread during a live host entry", // --- issue #156: settled activation tails, post-CM#705 --------------------- // // History: `Store.settled` tails are dispatched through `Thread.resumeWith`, -// which used to bracket the resumption with `enterFrom(null)`. Under the -// shared synthetic root a host entry into ANY instance locked every sibling, +// which used to bracket the resumption with a host entry. Under the shared +// per-instantiation root a host entry into ANY instance locked every sibling, // so dispatching a sibling's tail in that window tripped `resumeWith`'s // enterability assert (and, mutating before asserting, stranded the thread // and lost the settle); #156 deferred such tails IN PLACE. @@ -341,7 +279,7 @@ async function queueSettledTail(settle: () => void): Promise { await Promise.resolve(); } -Deno.test("root: serviceSettled dispatches a sibling tail immediately", async () => { +Deno.test("cm705: serviceSettled dispatches a sibling tail immediately", async () => { const store = new Store(); const a = new ComponentInstanceState(0, store); const b = new ComponentInstanceState(1, store); @@ -377,10 +315,10 @@ Deno.test("root: serviceSettled dispatches a sibling tail immediately", async () assertEq(store.awaiting.has(bThread), false); }); -Deno.test("root: the phantom-state gate holds for a serviceable tail", async () => { - // The tick gate relaxes ONLY for deferred tails: a serviceable unserviced - // tail still refuses tick, preserving the reference's atomic-resume - // discipline. +Deno.test("cm705: the phantom-state gate holds for a serviceable tail", async () => { + // An unserviced tail refuses tick, preserving the reference's atomic-resume + // discipline. (Pre-#173 the gate relaxed for reentrance-deferred tails; + // there are none now, so the gate is simply "the queue is non-empty".) const store = new Store(); const a = new ComponentInstanceState(0, store); const b = new ComponentInstanceState(1, store); @@ -417,7 +355,7 @@ Deno.test("root: the phantom-state gate holds for a serviceable tail", async () await queueSettledTail(settle); assertEq(store.settled.length, 1); - assertEq(dispatchableTail(bThread), true, "the tail is serviceable"); + assertEq(store.hasServiceableSettled(), true, "the tail is serviceable"); assertEq(store.tick(), false, "a serviceable tail gates tick"); assertEq(order.length, 0); @@ -426,7 +364,7 @@ Deno.test("root: the phantom-state gate holds for a serviceable tail", async () assertEq(order.join(","), "a ran"); }); -Deno.test("root: a poisoned instance's tail retires without running", async () => { +Deno.test("cm705: a poisoned instance's tail retires without running", async () => { // `resumeWith`'s poison early-return retires the tail: the queue drains and // the body does NOT run. (#66 / #156; unchanged by CM#705 — a corpse's // parked segments must never resume.) @@ -459,7 +397,7 @@ Deno.test("root: a poisoned instance's tail retires without running", async () = assertEq(order.length, 0, "retired quietly: the body never ran"); }); -Deno.test("root: stale settled entries are removed", async () => { +Deno.test("cm705: stale settled entries are removed", async () => { // "Stale" = the thread was resumed elsewhere (driveAsync's race-winner // path), i.e. it is gone from `store.awaiting`. Such entries are dropped // whenever encountered, and dropping one is not progress. @@ -490,7 +428,7 @@ Deno.test("root: stale settled entries are removed", async () => { assertEq(store.settled.length, 0, "but it is removed"); }); -Deno.test("root: trap poisoning stays per-instance (named divergence)", () => { +Deno.test("cm705: trap poisoning stays per-instance (named divergence)", () => { const store = new Store(); const a = new ComponentInstanceState(0, store); const b = new ComponentInstanceState(1, store); @@ -816,7 +754,7 @@ Deno.test("tick: a trap under tick records the poison marker", async () => { // A trap escaping `thread.resume()` under `Store.tick` poisons the // instance. Post-CM#705 there is no bracket to break, so recording the // MARKER is the entire act — and it is what `Thread.resumeWith`'s - // quiet-retire, `dispatchableTail` and `entryRefusal` all read + // quiet-retire and `entryRefusal` both read // (polyengine#145, #156, #251). const store = new Store(); const b = new ComponentInstanceState(0, store); @@ -867,7 +805,7 @@ Deno.test("tick: a trap under tick records the poison marker", async () => { // #156 interaction: the settled tail of the poisoned instance drains // quietly instead of hitting `resumeWith`'s backstop assert (or deferring - // forever, which is what `dispatchableTail` would do without the marker). + // forever if the tail were resumed as if the instance were healthy). await queueSettledTail(settle); assertEq(store.settled.length, 1, "the tail is queued"); assertEq(store.serviceSettled(), true, "poisoned tails dispatch"); @@ -930,8 +868,8 @@ Deno.test("request_cancellation: a capability signal does not poison", () => { // White-box pins on the property the re-key bought and CM#705 then made // unavoidable: every entry-refusal DECISION reads the poison MARKER, which is // now the only refusal mechanism there is. Nothing locks an instance any -// more, so these tests need no `mayEnter` manipulation — an unmarked instance -// is always enterable, by construction. +// more, so these tests need no reentrance-state manipulation — an unmarked +// instance is always enterable, by construction. Deno.test("re-key: entryRefusal refuses a marked instance", () => { const store = new Store();