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
25 changes: 19 additions & 6 deletions contracts/descriptor-ir.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,22 @@ fixtures.

## Resource-type identity

The shim emits `resource` indices into the plan's `resourceTables`; the runtime
builds identity tokens (`ResourceTypeInfo`) at plan-load time. **Tokens must be
fresh per instantiation** (the executor re-runs plan loading per instantiate),
so resource-type identity never leaks across instances. Within one
instantiation, concrete tables naming the same resource share a token; table
indices are aliases, not distinct types.
The shim emits `resource` indices into the plan's `resourceTables`. Preserve two
identities when loading these descriptors:

- **Underlying resource identity**, keyed by concrete `ResourceIndex`, owns
implementation/destructor metadata in `ResourceTypeInfo`. Legitimate
cross-component aliases and host resource wrappers compare this identity.
- **Local handle type identity**, keyed by `TypeResourceTableIndex`, governs
guest handle access through `ResourceTableInfo`, which references its
underlying resource. Distinct abstract imports can share an underlying
resource and a component instance while retaining different local identities.

Both are scoped to a runtime instantiation of the plan, not just its reusable
JSON object. Guest-defined identities are fresh on every instantiation.
Transferring a handle validates the source local type and tags the destination
handle with its destination local type; it does not change the resource origin.
Stream/future endpoints likewise retain their local element descriptors for
guest access checks, while rendezvous compatibility uses underlying identities.
Sharing origin metadata must not erase the child's abstract type distinctions
(pinned Explainer, type imports and substitution).
6 changes: 6 additions & 0 deletions contracts/intrinsics.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,12 @@ directly. `modules[].intrinsics` records import names and resolved categories.
- **Resource borrows:** transfer registers a lender on the current call scope,
including re-lending an already borrowed handle and same-instance rep fast
paths. A lend prevents own transfer or drop until its scope ends.
- **Resource handle types:** validate against the source resource-table
identity, then create the destination handle with its destination table
identity. Tables may share resource origin and destructor metadata without
being interchangeable inside the guest. Stream/future operations check their
endpoint's local element type; boundary transfers compare underlying origins
and retag the endpoint for the receiving component.
- **Unwind:** a failed call releases the lenders it registered, including
non-poisoning capability failures. The host boundary restores `may_leave`
according to entry identity: it excludes the host entry's own instance and
Expand Down
7 changes: 5 additions & 2 deletions contracts/plan-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,11 @@ Notes on specific entries:
resource type can be reachable through several distinct table indices — e.g. a
type export pointing at table 1 while the functions' handles use table 0, both
resolving to the same `ResourceIndex` via `resourceTables[n].resource`.
Consumers keying per-resource state must key by the resolved `ResourceIndex`,
treating table indices as aliases.
Implementation/destructor state is keyed by the resolved `ResourceIndex`.
Guest handle checks must retain the table index: two abstract types inside one
nested instance may resolve to the same origin without being interchangeable
there. `TypeResourceTableIndex` is not reconstructible from the pair
`(ResourceIndex, instance)`; preserve the emitted table entries.
- **Module exports**: the executor surfaces the export as the platform's
compiled-module value — `WebAssembly.Module` in the JS runtime — reusing the
compilation the instantiation path already performs. Module exports are
Expand Down
9 changes: 9 additions & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,15 @@ failure is reported through the host-failure channel, not swallowed.
Backstop-versus-teardown policy remains tracked in
[#10](https://github.com/polymorph-components/polyengine/issues/10).

Resource origin and guest-local handle type are separate identities. The plan's
`ResourceIndex` identifies shared implementation/destructor metadata;
`TypeResourceTableIndex` preserves the importing component's abstract type.
Distinct tables can have the same origin and component instance. Guest handle
access compares the local type; transfers validate the source and tag the
destination. Host wrappers and cross-component payload compatibility retain
underlying origin identity. Both identities are scoped to an instantiation of
the plan; see [descriptor IR](../contracts/descriptor-ir.md#resource-type-identity).

**Destructors.** `canon_resource_drop` lifts a core `[rep] -> []`
destructor with synchronous canonical options. The destructor may not
Component-Model-block, though the spec permits spawning an explicit
Expand Down
9 changes: 5 additions & 4 deletions runtime/src/cabi/async_values.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { copyCensus, ERROR_CONTEXT, hasBrand } from "@polyengine/protocol";
import { assert_, trapIf } from "./trap.ts";
import type { LiftLowerContext } from "./context.ts";
import type { ValType } from "./types.ts";
import { contains, fmtValType } from "./types.ts";
import { contains, fmtValType, valTypeEqual } from "./types.ts";
import {
CopyState,
ErrorContext,
Expand Down Expand Up @@ -74,11 +74,12 @@ function liftAsyncValue(
trapIf(!(e instanceof EndT), `${what} lift: handle is not a ${what} end`);
const end = e as {
shared: SharedBase;
elem: ValType | null;
state: CopyState;
inWaitableSet(): boolean;
};
trapIf(
!sameElemType(end.shared.t, elem),
!valTypeEqual(end.elem, elem),
`${what} lift: element type mismatch`,
);
trapIf(
Expand Down Expand Up @@ -163,7 +164,7 @@ export function lowerStream(
(v as { boundStore?: unknown }).boundStore ??=
(inst as unknown as { store?: unknown }).store;
(v as { onLowered?: ((i: unknown) => void) | null }).onLowered?.(inst);
return inst!.handles.add(new ReadableStreamEnd(v));
return inst!.handles.add(new ReadableStreamEnd(v, declared));
}

/** definitions.py `lower_future`. */
Expand All @@ -190,7 +191,7 @@ export function lowerFuture(
(v as { boundStore?: unknown }).boundStore ??=
(inst as unknown as { store?: unknown }).store;
(v as { onLowered?: ((i: unknown) => void) | null }).onLowered?.(inst);
return inst!.handles.add(new ReadableFutureEnd(v));
return inst!.handles.add(new ReadableFutureEnd(v, declared));
}

/** definitions.py `lift_error_context`. Does not remove the handle. */
Expand Down
19 changes: 12 additions & 7 deletions runtime/src/cabi/handles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ import type {
SubtaskBorrowScope,
TaskBorrowScope,
} from "./context.ts";
import type { BorrowType, OwnType, ResourceTypeInfo } from "./types.ts";
import type {
BorrowType,
OwnType,
ResourceTableInfo,
ResourceTypeInfo,
} from "./types.ts";

export class Table<T> {
static readonly MAX_LENGTH = 2 ** 28 - 1;
Expand Down Expand Up @@ -65,7 +70,7 @@ export class ResourceHandle {
numLends = 0;

constructor(
public rt: ResourceTypeInfo,
public rt: ResourceTableInfo,
public rep: number,
public own: boolean,
public borrowScope: TaskBorrowScope | null = null,
Expand Down Expand Up @@ -133,7 +138,7 @@ export function lowerBorrow(
scope !== null && typeof scope.numBorrows === "number",
"lowering a borrow requires a task borrow scope",
);
if (cx.inst !== null && cx.inst === (t.rt.impl as unknown)) {
if (cx.inst !== null && cx.inst === (t.rt.resource.impl as unknown)) {
return rep;
}
const h = new ResourceHandle(t.rt, rep, false, scope);
Expand All @@ -148,7 +153,7 @@ export function lowerBorrow(

export function canonResourceNew(
inst: ComponentInstanceLike,
rt: ResourceTypeInfo,
rt: ResourceTableInfo,
rep: number,
): number {
trapIf(!inst.mayLeave, "may_leave violation");
Expand Down Expand Up @@ -221,7 +226,7 @@ export function callDtorGated(

export function canonResourceDrop(
inst: ComponentInstanceLike,
rt: ResourceTypeInfo,
rt: ResourceTableInfo,
i: number,
): void {
trapIf(!inst.mayLeave, "may_leave violation");
Expand All @@ -233,7 +238,7 @@ export function canonResourceDrop(
if (rh.own) {
assert_(rh.borrowScope === null);
// Enter a fresh synchronous dtor task, not the dropping task's ambient.
callDtorGated(rt, rh.rep, inst);
callDtorGated(rt.resource, rh.rep, inst);
} else {
assert_(rh.borrowScope !== null);
rh.borrowScope!.numBorrows -= 1;
Expand All @@ -243,7 +248,7 @@ export function canonResourceDrop(

export function canonResourceRep(
inst: ComponentInstanceLike,
rt: ResourceTypeInfo,
rt: ResourceTableInfo,
i: number,
): number {
const h = inst.handles.get(i);
Expand Down
49 changes: 32 additions & 17 deletions runtime/src/cabi/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export interface InstanceLike {

/**
* definitions.py `ResourceType`: identity + implementing instance + optional
* destructor. Compared by object identity everywhere.
* destructor. Shared origin identity across component-local resource tables.
*
* `dtorHost` is the host-initiated drop entry, wired by exec/executor.ts or
* lazily by `hostDtorCall` in exec/boundary.ts. It lifts the dtor with a fresh
Expand All @@ -58,6 +58,11 @@ export class ResourceTypeInfo {
) {}
}

/** Component-local handle identity; distinct wire tables never share a wrapper. */
export class ResourceTableInfo {
constructor(readonly resource: ResourceTypeInfo) {}
}

// ---------------------------------------------------------------------------
// Value types (definitions.py ValType hierarchy)
// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -133,11 +138,11 @@ export interface FlagsType {
}
export interface OwnType {
kind: "own";
rt: ResourceTypeInfo;
rt: ResourceTableInfo;
}
export interface BorrowType {
kind: "borrow";
rt: ResourceTypeInfo;
rt: ResourceTableInfo;
}
export interface StreamType {
kind: "stream";
Expand Down Expand Up @@ -418,34 +423,41 @@ export function contains(
// ---------------------------------------------------------------------------

/**
* Structural equality except for resource types, which compare by token
* identity. Do not serialize or recurse into ResourceTypeInfo: its instance
* Structural equality with local resource-table identity by default. Shared
* async payload compatibility explicitly selects underlying origin identity.
* Do not serialize or recurse into ResourceTypeInfo: its instance
* points back to live handle tables and can form cycles.
*/
export function valTypesEqual(a: ValType[], b: ValType[]): boolean {
return a.length === b.length && a.every((t, i) => valTypeEqual(t, b[i]));
}

export function valTypeEqual(a: ValType, b: ValType): boolean {
export function valTypeEqual(
a: ValType | null,
b: ValType | null,
identity: "local" | "underlying" = "local",
): boolean {
if (a === b) return true;
if (a === null || b === null) return false;
if (a.kind !== b.kind) return false;
switch (a.kind) {
case "list": {
const bb = b as typeof a;
return a.length === bb.length && valTypeEqual(a.element, bb.element);
return a.length === bb.length &&
valTypeEqual(a.element, bb.element, identity);
}
case "record": {
const bb = b as typeof a;
return a.fields.length === bb.fields.length &&
a.fields.every((f, i) =>
f.label === bb.fields[i].label &&
valTypeEqual(f.type, bb.fields[i].type)
valTypeEqual(f.type, bb.fields[i].type, identity)
);
}
case "tuple": {
const bb = b as typeof a;
return a.elements.length === bb.elements.length &&
a.elements.every((e, i) => valTypeEqual(e, bb.elements[i]));
a.elements.every((e, i) => valTypeEqual(e, bb.elements[i], identity));
}
case "variant": {
const bb = b as typeof a;
Expand All @@ -456,7 +468,7 @@ export function valTypeEqual(a: ValType, b: ValType): boolean {
if (c.type === null || other.type === null) {
return c.type === other.type;
}
return valTypeEqual(c.type, other.type);
return valTypeEqual(c.type, other.type, identity);
});
}
case "enum":
Expand All @@ -467,30 +479,33 @@ export function valTypeEqual(a: ValType, b: ValType): boolean {
}
case "option": {
const bb = b as typeof a;
return valTypeEqual(a.type, bb.type);
return valTypeEqual(a.type, bb.type, identity);
}
case "result": {
const bb = b as typeof a;
if ((a.ok === null) !== (bb.ok === null)) return false;
if ((a.error === null) !== (bb.error === null)) return false;
return (a.ok === null || valTypeEqual(a.ok, bb.ok!)) &&
(a.error === null || valTypeEqual(a.error, bb.error!));
return (a.ok === null || valTypeEqual(a.ok, bb.ok!, identity)) &&
(a.error === null || valTypeEqual(a.error, bb.error!, identity));
}
case "map": {
const bb = b as typeof a;
return valTypeEqual(a.key, bb.key) && valTypeEqual(a.value, bb.value);
return valTypeEqual(a.key, bb.key, identity) &&
valTypeEqual(a.value, bb.value, identity);
}
case "own":
case "borrow": {
const bb = b as typeof a;
// Object-identity type (documented invariant): reference equality only.
return a.rt === bb.rt;
return identity === "local"
? a.rt === bb.rt
: a.rt.resource === bb.rt.resource;
}
case "stream":
case "future": {
const bb = b as typeof a;
if ((a.element === null) !== (bb.element === null)) return false;
return a.element === null || valTypeEqual(a.element, bb.element!);
return a.element === null ||
valTypeEqual(a.element, bb.element!, identity);
}
case "error-context":
return true;
Expand Down
Loading
Loading