Add composable interface operation forwarding
Support bind member.operation to interface Target<Args>.member.operation. Check exact signatures and scopes, same-receiver conformances, cycles and watch pairing. Resolve chains for native queries, editing and dispatch while retaining the terminal conformance owner and injected package dependencies. Track forwarded providers in dependency and semantic-major review machinery. Cover native get/set/watch chains, generic targets, invalid bindings, package forwarding, runtime owner authority and watch cancellation. Document the language feature and keep Studio search adoption explicitly pending. No push or deployment.
This commit is contained in:
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1787,6 +1787,36 @@ const lowerConformance = (
|
||||
return [];
|
||||
}
|
||||
const provider = bindingContext.operationProvider();
|
||||
if (provider.INTERFACE()) {
|
||||
const targetName = identifier(provider.identifier(0));
|
||||
let target = requireSymbol(state, state.interfaces, targetName, provider, "interface");
|
||||
if (!target) return [];
|
||||
if (provider.typeArguments() || target.definition?.template) {
|
||||
const id = new TypeSubstitution({ ...state.types.environment(), self: atomId }).application(
|
||||
state.types.interfaceByName(targetName, provider.typeArguments()),
|
||||
);
|
||||
target = interfaceSymbolFor(state.types.definitions.get(id)!);
|
||||
}
|
||||
const ref = provider.memberOperationRef()!;
|
||||
const member = target.members.get(identifier(ref.identifier()));
|
||||
const targetOperation = member?.operations.get(text(ref.operationName()));
|
||||
if (!targetOperation) {
|
||||
loweringIssue(
|
||||
state,
|
||||
provider,
|
||||
"unknown-operation",
|
||||
"Forwarding target must name an interface member operation",
|
||||
);
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
{
|
||||
operationId,
|
||||
binding: { kind: "interface", interfaceRevisionId: target.revisionId, operationId: targetOperation },
|
||||
...(bindingContext.stringLiteral() ? { queryReason: stringValue(bindingContext.stringLiteral()!) } : {}),
|
||||
},
|
||||
];
|
||||
}
|
||||
if (provider.STATE()) {
|
||||
const attachmentName = identifier(provider.identifier(0));
|
||||
const attachment = requireSymbol(state, state.attachments, attachmentName, provider, "attachment");
|
||||
|
||||
@@ -167,8 +167,22 @@ export const runtimeContracts = (workspace: WorkspaceRevision): RuntimeContract[
|
||||
parent.dependencies.add(conformanceKey(conformance.atomId, conformance.interfaceRevisionId));
|
||||
reviews?.add(conformanceIdentity(conformance));
|
||||
for (const operation of conformance.operationBindings) {
|
||||
if (operation.binding.kind !== "package") continue;
|
||||
const revisionId = operation.binding.packageRevisionId;
|
||||
let provider: Binding | undefined = operation.binding;
|
||||
const visited = new Set<string>();
|
||||
while (provider?.kind === "interface") {
|
||||
const forward: Extract<Binding, { kind: "interface" }> = provider;
|
||||
const key = `${provider.interfaceRevisionId}\0${provider.operationId}`;
|
||||
if (visited.has(key)) throw new Error("Forwarding cycle in evolution contract");
|
||||
visited.add(key);
|
||||
const target = workspace.conformances.find(
|
||||
(c) => c.atomId === conformance.atomId && c.interfaceRevisionId === forward.interfaceRevisionId,
|
||||
);
|
||||
if (!target) throw new Error("Missing forwarding conformance in evolution contract");
|
||||
reviews?.add(conformanceIdentity(target));
|
||||
provider = target.operationBindings.find((entry) => entry.operationId === forward.operationId)?.binding;
|
||||
}
|
||||
if (provider?.kind !== "package") continue;
|
||||
const revisionId = provider.packageRevisionId;
|
||||
const pkg = workspace.packageImports.find((entry) => entry.revisionId === revisionId);
|
||||
if (pkg) reviews?.add(pkg.packageId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import type {
|
||||
AtomId,
|
||||
Binding,
|
||||
Conformance,
|
||||
InterfaceRevisionId,
|
||||
OperationBinding,
|
||||
OperationId,
|
||||
WorkspaceRevision,
|
||||
} from "./types.js";
|
||||
|
||||
export type ConcreteBinding = Exclude<Binding, { kind: "interface" }>;
|
||||
export type ResolvedBinding = { conformance: Conformance; entry: OperationBinding; binding: ConcreteBinding };
|
||||
|
||||
/** Follow public operations on the same receiver, never private attachment names. */
|
||||
export function resolveForwardedBinding(
|
||||
workspace: Pick<WorkspaceRevision, "conformances">,
|
||||
atomId: AtomId,
|
||||
interfaceRevisionId: InterfaceRevisionId,
|
||||
operationId: OperationId,
|
||||
): ResolvedBinding {
|
||||
const visited = new Set<string>();
|
||||
for (;;) {
|
||||
const key = `${interfaceRevisionId}\0${operationId}`;
|
||||
if (visited.has(key)) throw new Error(`Forwarding cycle at ${interfaceRevisionId}/${operationId}`);
|
||||
visited.add(key);
|
||||
const conformance = workspace.conformances.find(
|
||||
(c) => c.atomId === atomId && c.interfaceRevisionId === interfaceRevisionId,
|
||||
);
|
||||
const entry = conformance?.operationBindings.find((b) => b.operationId === operationId);
|
||||
if (!conformance || !entry)
|
||||
throw new Error(`Missing forwarding target ${atomId} as ${interfaceRevisionId}/${operationId}`);
|
||||
if (entry.binding.kind !== "interface") return { conformance, entry, binding: entry.binding };
|
||||
interfaceRevisionId = entry.binding.interfaceRevisionId;
|
||||
operationId = entry.binding.operationId;
|
||||
}
|
||||
}
|
||||
@@ -337,6 +337,11 @@ export interface RelationshipMaterialization {
|
||||
}
|
||||
|
||||
export type Binding =
|
||||
| {
|
||||
kind: "interface";
|
||||
interfaceRevisionId: InterfaceRevisionId;
|
||||
operationId: OperationId;
|
||||
}
|
||||
| {
|
||||
kind: "state";
|
||||
slotId: SlotId;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveForwardedBinding, type ConcreteBinding } from "./forwarding.js";
|
||||
import type {
|
||||
AtomConstructorBinding,
|
||||
AtomId,
|
||||
@@ -66,6 +67,7 @@ export type CapabilityValidationIssueCode =
|
||||
| "invalid-state-binding"
|
||||
| "invalid-edge-binding"
|
||||
| "invalid-package-binding"
|
||||
| "invalid-forwarding-binding"
|
||||
| "invalid-dependency-binding"
|
||||
| "private-attachment-access"
|
||||
| "incompatible-package-receiver"
|
||||
@@ -1521,7 +1523,32 @@ const validateConformances = (
|
||||
continue;
|
||||
}
|
||||
const operation = operationEntry.operation;
|
||||
const binding = entry.binding;
|
||||
let binding: ConcreteBinding;
|
||||
if (entry.binding.kind === "interface") {
|
||||
const target = indexes.interfaces
|
||||
.get(entry.binding.interfaceRevisionId)
|
||||
?.operations.get(entry.binding.operationId)?.operation;
|
||||
if (!target || !signaturesMatch(operation, target) || operation.scope !== target.scope) {
|
||||
issue(
|
||||
issues,
|
||||
"invalid-forwarding-binding",
|
||||
bindingPath,
|
||||
"Forwarding requires matching signatures, modes and receiver scopes",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
binding = resolveForwardedBinding(
|
||||
workspace,
|
||||
conformance.atomId,
|
||||
conformance.interfaceRevisionId,
|
||||
entry.operationId,
|
||||
).binding;
|
||||
} catch (error) {
|
||||
issue(issues, "invalid-forwarding-binding", bindingPath, String(error));
|
||||
continue;
|
||||
}
|
||||
} else binding = entry.binding;
|
||||
const member = operationEntry.member;
|
||||
if (
|
||||
member.kind !== "operation" &&
|
||||
@@ -1556,6 +1583,9 @@ const validateConformances = (
|
||||
);
|
||||
continue;
|
||||
}
|
||||
// The target conformance validates the concrete provider and its private
|
||||
// authority. Forwarding exposes that operation, not its attachment ports.
|
||||
if (entry.binding.kind === "interface") continue;
|
||||
if (operation.scope === "class" && binding.kind !== "package") {
|
||||
issue(
|
||||
issues,
|
||||
@@ -1760,6 +1790,49 @@ const validateConformances = (
|
||||
});
|
||||
}
|
||||
|
||||
for (const member of interfaceEntry.revision.members) {
|
||||
const start = member.operations.find((op) => op.mode === "watch-start");
|
||||
const stop = member.operations.find((op) => op.mode === "watch-stop");
|
||||
if (!start || !stop || ![start, stop].some((op) => bindings.get(op.id)?.kind === "interface")) continue;
|
||||
try {
|
||||
const a = resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, start.id);
|
||||
const b = resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, stop.id);
|
||||
const target = indexes.interfaces
|
||||
.get(a.conformance.interfaceRevisionId)
|
||||
?.revision.members.find((m) => m.operations.some((op) => op.id === a.entry.operationId));
|
||||
if (a.conformance !== b.conformance || !target?.operations.some((op) => op.id === b.entry.operationId))
|
||||
issue(
|
||||
issues,
|
||||
"invalid-forwarding-binding",
|
||||
path,
|
||||
"Forwarded watch start/stop must resolve to the same target member",
|
||||
);
|
||||
const get = member.operations.find((op) => op.displayName === (member.kind === "value" ? "get" : "resolve"));
|
||||
if (get && (a.binding.kind === "state" || a.binding.kind === "edge")) {
|
||||
const read = resolveForwardedBinding(
|
||||
workspace,
|
||||
conformance.atomId,
|
||||
conformance.interfaceRevisionId,
|
||||
get.id,
|
||||
).binding;
|
||||
const matches =
|
||||
a.binding.kind === "state"
|
||||
? read.kind === "state" && read.slotId === a.binding.slotId
|
||||
: read.kind === "edge" &&
|
||||
read.edgeTypeId === a.binding.edgeTypeId &&
|
||||
read.projectionId === a.binding.projectionId;
|
||||
if (!matches)
|
||||
issue(
|
||||
issues,
|
||||
"invalid-forwarding-binding",
|
||||
path,
|
||||
"Native forwarded watch must observe its getter's storage",
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
issue(issues, "invalid-forwarding-binding", path, String(error));
|
||||
}
|
||||
}
|
||||
const materializedMembers = new Set<string>();
|
||||
for (const [materializationIndex, materialization] of (conformance.relationshipMaterializations ?? []).entries()) {
|
||||
const materializationPath = `${path}.relationshipMaterializations[${materializationIndex}]`;
|
||||
@@ -2063,7 +2136,10 @@ export const resolveOperationBinding = (
|
||||
atomId: AtomId,
|
||||
interfaceRevisionId: InterfaceRevisionId,
|
||||
operationId: OperationId,
|
||||
): Binding | undefined => resolveConformance(plan, atomId, interfaceRevisionId)?.operationBindings.get(operationId);
|
||||
): ConcreteBinding | undefined => {
|
||||
if (!resolveConformance(plan, atomId, interfaceRevisionId)?.operationBindings.has(operationId)) return undefined;
|
||||
return resolveForwardedBinding(plan.source, atomId, interfaceRevisionId, operationId).binding;
|
||||
};
|
||||
|
||||
export type ResolvedOperationPlan =
|
||||
| {
|
||||
@@ -2259,6 +2335,8 @@ export const computeCapabilityClosure = (
|
||||
attachments.add(binding.slotId);
|
||||
} else if (binding.kind === "edge") {
|
||||
attachments.add(binding.edgeTypeId);
|
||||
} else if (binding.kind === "interface") {
|
||||
queued.push({ atomId: root.atomId, interfaceRevisionId: binding.interfaceRevisionId });
|
||||
} else {
|
||||
packages.add(binding.packageRevisionId);
|
||||
includeDependencies(root.atomId, binding.dependencies);
|
||||
|
||||
+11
-2
@@ -10,6 +10,7 @@ import type {
|
||||
import { QueryCompileError, type CheckedQuery } from "./types.js";
|
||||
import { queryRuntimePlan } from "./proto.js";
|
||||
import { canonicalJson } from "../capability-model/evolution.js";
|
||||
import { resolveForwardedBinding } from "../capability-model/forwarding.js";
|
||||
|
||||
export interface LinkedQueryField {
|
||||
atomId: AtomId;
|
||||
@@ -74,7 +75,12 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
|
||||
const provider = conformance.operationBindings.find((entry) => entry.operationId === getter.id);
|
||||
if (!provider)
|
||||
throw new QueryCompileError("QUERY_CONTRACT", `Missing binding ${conformance.atomId}.${getter.id}`);
|
||||
const binding = provider.binding;
|
||||
const binding = resolveForwardedBinding(
|
||||
workspace,
|
||||
conformance.atomId,
|
||||
conformance.interfaceRevisionId,
|
||||
getter.id,
|
||||
).binding;
|
||||
if (binding.kind === "package") {
|
||||
if (effect.execution !== "rpc-permitted")
|
||||
throw new QueryCompileError(
|
||||
@@ -95,7 +101,10 @@ export function linkQueries(workspace: WorkspaceRevision): LinkedQuery[] {
|
||||
);
|
||||
const start = member.operations.find((op) => op.displayName === "watch-start");
|
||||
const stop = member.operations.find((op) => op.displayName === "watch-stop");
|
||||
const watch = start && stop && conformance.operationBindings.find((entry) => entry.operationId === start.id);
|
||||
const watch =
|
||||
start && stop && conformance.operationBindings.some((entry) => entry.operationId === start.id)
|
||||
? resolveForwardedBinding(workspace, conformance.atomId, conformance.interfaceRevisionId, start.id)
|
||||
: undefined;
|
||||
if (
|
||||
binding.kind === "state" &&
|
||||
watch &&
|
||||
|
||||
Reference in New Issue
Block a user