Support explicitly reference-selected read-only interface dependencies
This commit is contained in:
@@ -226,7 +226,7 @@ dependencyPort
|
|||||||
: REFLECTION identifier ID stringLiteral LBRACK identifierList RBRACK SEMI
|
: REFLECTION identifier ID stringLiteral LBRACK identifierList RBRACK SEMI
|
||||||
| STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
|
| STATE identifier ID stringLiteral COLON valueType primitiveList SEMI
|
||||||
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
|
| EDGE identifier ID stringLiteral COLON cardinality targetConstraint primitiveList SEMI
|
||||||
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? SEMI
|
| INTERFACE identifier ID stringLiteral COLON identifier typeArguments? selectedOperations? (FROM INPUT)? SEMI
|
||||||
| CREATION identifier ID stringLiteral COLON identifier INPUT valueType SEMI
|
| CREATION identifier ID stringLiteral COLON identifier INPUT valueType SEMI
|
||||||
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
| CONSTRUCTOR identifier ID stringLiteral COLON identifier (INPUT valueType)? SEMI
|
||||||
| QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI
|
| QUERY identifier ID stringLiteral COLON identifier DOT identifier SEMI
|
||||||
@@ -367,7 +367,7 @@ dependencyBinding
|
|||||||
: identifier TO WORKSPACE REFLECTION LBRACK identifierList RBRACK SEMI
|
: identifier TO WORKSPACE REFLECTION LBRACK identifierList RBRACK SEMI
|
||||||
| identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI
|
| identifier TO STATE identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||||
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
| identifier TO EDGE identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||||
| identifier TO INTERFACE identifier typeArguments? (VIA EDGE identifier DOT identifier)? SEMI
|
| identifier TO INTERFACE identifier typeArguments? ((VIA EDGE identifier DOT identifier) | (FROM INPUT))? SEMI
|
||||||
| identifier TO CREATION identifier WITH LBRACE creationOwnerMapping* RBRACE SEMI
|
| identifier TO CREATION identifier WITH LBRACE creationOwnerMapping* RBRACE SEMI
|
||||||
| identifier TO CONSTRUCTOR identifier SEMI
|
| identifier TO CONSTRUCTOR identifier SEMI
|
||||||
| identifier TO WORKSPACE? QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
| identifier TO WORKSPACE? QUERY identifier DOT identifier (VIA EDGE identifier DOT identifier)? SEMI
|
||||||
|
|||||||
@@ -183,10 +183,23 @@ function methods(
|
|||||||
`Selected operation is not an object call available in portable target ${target}; select a portable subset or move this handler to remote execution`,
|
`Selected operation is not an object call available in portable target ${target}; select a portable subset or move this handler to remote execution`,
|
||||||
);
|
);
|
||||||
if (execution.kind === "transaction" && !remote) writable();
|
if (execution.kind === "transaction" && !remote) writable();
|
||||||
|
if (req.receiver === "input" && (remote || execution.kind !== "snapshot"))
|
||||||
|
throw new PortableBindingError(path, "Reference-selected dependencies require portable snapshot calls");
|
||||||
return method(
|
return method(
|
||||||
`${member.displayName}.${op.displayName}`,
|
`${member.displayName}.${op.displayName}`,
|
||||||
{ kind: "interface", operationId: op.id },
|
{ kind: "interface", operationId: op.id },
|
||||||
op.inputType,
|
req.receiver === "input"
|
||||||
|
? {
|
||||||
|
kind: "record",
|
||||||
|
fields: {
|
||||||
|
receiver: {
|
||||||
|
kind: "object-ref",
|
||||||
|
expectation: { kind: "interface", interfaceRevisionId: req.interfaceRevisionId },
|
||||||
|
},
|
||||||
|
input: op.inputType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: op.inputType,
|
||||||
op.outputType,
|
op.outputType,
|
||||||
execution.kind === "transaction",
|
execution.kind === "transaction",
|
||||||
);
|
);
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -1020,6 +1020,7 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
|
|||||||
kind: "interface",
|
kind: "interface",
|
||||||
interfaceRevisionId: target.revisionId,
|
interfaceRevisionId: target.revisionId,
|
||||||
...lowerSelectedOperations(state, context, target),
|
...lowerSelectedOperations(state, context, target),
|
||||||
|
...(context.FROM() ? { receiver: "input" as const } : {}),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
: undefined;
|
: undefined;
|
||||||
@@ -1241,6 +1242,7 @@ const lowerGenericPackageExport = (
|
|||||||
kind: "interface",
|
kind: "interface",
|
||||||
application: state.types.interfaceByName(identifier(port.identifier(1)), port.typeArguments()),
|
application: state.types.interfaceByName(identifier(port.identifier(1)), port.typeArguments()),
|
||||||
...lowerSelectedOperations(state, port, state.interfaces.get(identifier(port.identifier(1)))),
|
...lowerSelectedOperations(state, port, state.interfaces.get(identifier(port.identifier(1)))),
|
||||||
|
...(port.FROM() ? { receiver: "input" as const } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const name = identifier(port.identifier(1)),
|
const name = identifier(port.identifier(1)),
|
||||||
@@ -1817,6 +1819,7 @@ const lowerBoundDependencies = (
|
|||||||
binding: {
|
binding: {
|
||||||
kind: "interface",
|
kind: "interface",
|
||||||
interfaceRevisionId: interfaceSymbol.revisionId,
|
interfaceRevisionId: interfaceSymbol.revisionId,
|
||||||
|
...(entry.FROM() ? { receiver: "input" as const } : {}),
|
||||||
...(via ? { via } : {}),
|
...(via ? { via } : {}),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,6 +66,23 @@ export function validateExecutionContracts(workspace: WorkspaceRevision): Capabi
|
|||||||
: requirement.application.definitionId,
|
: requirement.application.definitionId,
|
||||||
);
|
);
|
||||||
const selected = requirement.operations;
|
const selected = requirement.operations;
|
||||||
|
if (requirement.receiver === "input") {
|
||||||
|
if (
|
||||||
|
entry.execution?.kind !== "snapshot" ||
|
||||||
|
entry.execution.targets.length !== 1 ||
|
||||||
|
entry.execution.targets[0] !== "server" ||
|
||||||
|
!selected?.length
|
||||||
|
)
|
||||||
|
fail(
|
||||||
|
portPath,
|
||||||
|
"Reference-selected reads currently require a server-only snapshot and an explicit selection; local acquisition of dynamic inputs is not implemented",
|
||||||
|
);
|
||||||
|
for (const id of selected ?? []) {
|
||||||
|
const operation = contract && operations(id, contract);
|
||||||
|
if (operation?.execution?.kind !== "snapshot" || operation.scope === "class")
|
||||||
|
fail(portPath, "Reference-selected dependencies currently support object snapshot operations only");
|
||||||
|
}
|
||||||
|
}
|
||||||
if (selected && (!selected.length || new Set(selected).size !== selected.length))
|
if (selected && (!selected.length || new Set(selected).size !== selected.length))
|
||||||
fail(portPath, "Selected interface operations must be nonempty and distinct");
|
fail(portPath, "Selected interface operations must be nonempty and distinct");
|
||||||
for (const id of selected ?? [])
|
for (const id of selected ?? [])
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export type GenericDependencyPort = Omit<DependencyPort, "requirement"> & {
|
|||||||
kind: "interface";
|
kind: "interface";
|
||||||
application: InterfaceApplicationExpression;
|
application: InterfaceApplicationExpression;
|
||||||
operations?: import("./types.js").OperationId[];
|
operations?: import("./types.js").OperationId[];
|
||||||
|
receiver?: "input";
|
||||||
}
|
}
|
||||||
| { kind: "creation"; target: ObjectTypeExpression; inputType: ValueTypeExpression }
|
| { kind: "creation"; target: ObjectTypeExpression; inputType: ValueTypeExpression }
|
||||||
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
|
| { kind: "constructor"; target: ObjectTypeExpression; inputType: ValueTypeExpression };
|
||||||
@@ -100,6 +101,7 @@ export const specializePackageExport = (
|
|||||||
kind: "interface",
|
kind: "interface",
|
||||||
interfaceRevisionId: substitution.application(requirement.application),
|
interfaceRevisionId: substitution.application(requirement.application),
|
||||||
...(requirement.operations ? { operations: requirement.operations } : {}),
|
...(requirement.operations ? { operations: requirement.operations } : {}),
|
||||||
|
...(requirement.receiver ? { receiver: requirement.receiver } : {}),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
case "creation":
|
case "creation":
|
||||||
|
|||||||
@@ -246,6 +246,7 @@ export type DependencyPortRequirement =
|
|||||||
interfaceRevisionId: InterfaceRevisionId;
|
interfaceRevisionId: InterfaceRevisionId;
|
||||||
/** Exact public operation identities, not implementation export names. */
|
/** Exact public operation identities, not implementation export names. */
|
||||||
operations?: OperationId[];
|
operations?: OperationId[];
|
||||||
|
receiver?: "input";
|
||||||
}
|
}
|
||||||
| { kind: "creation"; atomId: AtomId; inputType: ValueType }
|
| { kind: "creation"; atomId: AtomId; inputType: ValueType }
|
||||||
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
|
| { kind: "constructor"; atomId: AtomId; inputType?: ValueType };
|
||||||
@@ -323,6 +324,7 @@ export type DependencyBinding =
|
|||||||
kind: "interface";
|
kind: "interface";
|
||||||
interfaceRevisionId: InterfaceRevisionId;
|
interfaceRevisionId: InterfaceRevisionId;
|
||||||
via?: EdgeTraversal;
|
via?: EdgeTraversal;
|
||||||
|
receiver?: "input";
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
kind: "creation";
|
kind: "creation";
|
||||||
|
|||||||
@@ -1418,6 +1418,15 @@ const validateBoundDependencies = (
|
|||||||
}
|
}
|
||||||
case "interface": {
|
case "interface": {
|
||||||
const interfaceBinding = binding as Extract<DependencyBinding, { kind: "interface" }>;
|
const interfaceBinding = binding as Extract<DependencyBinding, { kind: "interface" }>;
|
||||||
|
if (interfaceBinding.receiver !== requirement.receiver || (interfaceBinding.receiver && interfaceBinding.via)) {
|
||||||
|
issue(
|
||||||
|
issues,
|
||||||
|
"invalid-dependency-binding",
|
||||||
|
path,
|
||||||
|
"Reference-selected interface ports require an explicit matching from input binding",
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (interfaceBinding.interfaceRevisionId !== requirement.interfaceRevisionId) {
|
if (interfaceBinding.interfaceRevisionId !== requirement.interfaceRevisionId) {
|
||||||
issue(
|
issue(
|
||||||
issues,
|
issues,
|
||||||
@@ -1427,6 +1436,9 @@ const validateBoundDependencies = (
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// The reference supplies its own checked witness at invocation time.
|
||||||
|
// This is not a requirement that the current receiver implements the interface.
|
||||||
|
if (interfaceBinding.receiver === "input") break;
|
||||||
const traversal = interfaceBinding.via
|
const traversal = interfaceBinding.via
|
||||||
? validateTraversal(
|
? validateTraversal(
|
||||||
issues,
|
issues,
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ export function compileKernelProgram(plan: ExecutionPlan): unknown {
|
|||||||
...common,
|
...common,
|
||||||
kind: "interface",
|
kind: "interface",
|
||||||
interface: bound.interfaceRevisionId,
|
interface: bound.interfaceRevisionId,
|
||||||
|
...(bound.receiver ? { receiver: bound.receiver } : {}),
|
||||||
operations: requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)),
|
operations: requirement.operations ?? iface.members.flatMap((m) => m.operations.map((o) => o.id)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,6 +162,15 @@ export function compileExecutionPlan(input: WorkspaceRevision): ExecutionPlan {
|
|||||||
const queryOperations = (query: LinkedQuery) =>
|
const queryOperations = (query: LinkedQuery) =>
|
||||||
query.fields.map((f) => publicOperationKey(f.atomId, f.interfaceRevisionId, f.getter));
|
query.fields.map((f) => publicOperationKey(f.atomId, f.interfaceRevisionId, f.getter));
|
||||||
const targetAtoms = (binding: DependencyBinding, atom: AtomId): AtomId[] => {
|
const targetAtoms = (binding: DependencyBinding, atom: AtomId): AtomId[] => {
|
||||||
|
if (binding.kind === "interface" && binding.receiver === "input")
|
||||||
|
return workspace.atoms
|
||||||
|
.filter((candidate) =>
|
||||||
|
workspace.conformances.some(
|
||||||
|
(conformance) =>
|
||||||
|
conformance.atomId === candidate.id && conformance.interfaceRevisionId === binding.interfaceRevisionId,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.map((candidate) => candidate.id);
|
||||||
if (!("via" in binding) || !binding.via) return [atom];
|
if (!("via" in binding) || !binding.via) return [atom];
|
||||||
const edge = attachments.find((a) => a.id === binding.via!.edgeTypeId);
|
const edge = attachments.find((a) => a.id === binding.via!.edgeTypeId);
|
||||||
if (!edge || edge.kind !== "edge") throw new ExecutionPlanError("MISSING_EDGE", atom, "Missing traversal edge");
|
if (!edge || edge.kind !== "edge") throw new ExecutionPlanError("MISSING_EDGE", atom, "Missing traversal edge");
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { test } from "node:test";
|
||||||
|
import { compileCapabilityResourceSource, compileCapabilitySource } from "../src/capability-language/parser.js";
|
||||||
|
import { compileExecutionPlan, compileKernelProgram } from "../src/execution-world/index.js";
|
||||||
|
import { generatePortableTypeScriptBindings } from "../src/bindings/portable.js";
|
||||||
|
import { formatQx } from "../src/capability-language/source.js";
|
||||||
|
const source = { repository: "https://example.test/reference.git", commit: "a".repeat(40) };
|
||||||
|
const iface = `interface Data id "data" revision "data@1" {
|
||||||
|
value content id "content" : string { get id "content:get" execution snapshot targets [server]; }
|
||||||
|
operation peek id "peek" : interface-ref<Data> -> string { call id "peek:call" execution snapshot targets [server]; }
|
||||||
|
}`;
|
||||||
|
const pkg = `external atom Item id "item";
|
||||||
|
import interface Data;
|
||||||
|
package Runtime id "runtime" revision "runtime@1" {
|
||||||
|
operation peek id "runtime:peek" : interface-ref<Data> -> string mode call execution snapshot targets [server]
|
||||||
|
receiver atom Item requires { interface peers id "peers" : Data using [content.get] from input; };
|
||||||
|
}`;
|
||||||
|
const workspace = `workspace Test id "workspace:test" revision "workspace:test@1" commit "${"a".repeat(40)}" {
|
||||||
|
import interface Data; import package Runtime; atom Item id "item";
|
||||||
|
conform Item as Data id "item:data" {
|
||||||
|
private state Content id "content" on Item : string policy register default "hello";
|
||||||
|
bind content.get to state Content.read;
|
||||||
|
bind peek.call to package Runtime.peek with { peers to interface Data from input; };
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
function compile(packageSource = pkg, workspaceSource = workspace) {
|
||||||
|
const contract = compileCapabilityResourceSource(iface, { source });
|
||||||
|
assert.ok(contract.ok && contract.resource.kind === "interface", JSON.stringify(contract.diagnostics));
|
||||||
|
const interfaces = new Map([["Data", contract.resource.revision]]);
|
||||||
|
const runtime = compileCapabilityResourceSource(packageSource, { source, environment: { interfaces } });
|
||||||
|
if (!runtime.ok) return runtime;
|
||||||
|
assert.equal(runtime.resource.kind, "package");
|
||||||
|
if (runtime.resource.kind !== "package") throw Error("Expected package");
|
||||||
|
return compileCapabilitySource(workspaceSource, "workspace.qx", {
|
||||||
|
interfaces,
|
||||||
|
packages: new Map([["Runtime", runtime.resource.revision]]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
test("reference-selected dependencies round trip and do not require a recursive receiver conformance", () => {
|
||||||
|
const result = compile(formatQx(pkg), formatQx(workspace));
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const plan = compileExecutionPlan(result.workspace);
|
||||||
|
const kernel = compileKernelProgram(plan) as {
|
||||||
|
operations: { operation: string; binding: { ports: { receiver?: string }[] } }[];
|
||||||
|
};
|
||||||
|
assert.equal(kernel.operations.find((op) => op.operation === "peek:call")!.binding.ports[0]!.receiver, "input");
|
||||||
|
const peek = plan.definition.operations.find((op) => op.operationId === "peek:call")!;
|
||||||
|
assert.ok(
|
||||||
|
peek.dependencies.some(
|
||||||
|
(dependency) => dependency.kind === "operation" && dependency.operation.includes("content:get"),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const generated = generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", {
|
||||||
|
target: "server",
|
||||||
|
});
|
||||||
|
assert.ok(generated.includes('ManagedReference<"interface", "data@1">'));
|
||||||
|
assert.ok(generated.includes('"receiver"'));
|
||||||
|
});
|
||||||
|
test("both sides explicitly grant reference selection and only object snapshots are permitted", () => {
|
||||||
|
assert.equal(compile(pkg, workspace.replace("Data from input", "Data")).ok, false);
|
||||||
|
assert.equal(compile(pkg.replace(" from input", ""), workspace).ok, false);
|
||||||
|
assert.equal(compile(pkg.replace("execution snapshot targets [server]", "execution remote"), workspace).ok, false);
|
||||||
|
assert.equal(compile(pkg.replace("using [content.get]", "")).ok, false);
|
||||||
|
});
|
||||||
|
test("closed generic specialization preserves reference-selected interface ports", () => {
|
||||||
|
const result = compile(
|
||||||
|
pkg.replace("operation peek id", "operation peek<value T> id"),
|
||||||
|
workspace.replace("Runtime.peek with", "Runtime.peek<string> with"),
|
||||||
|
);
|
||||||
|
assert.ok(result.ok, JSON.stringify(result.diagnostics));
|
||||||
|
const plan = compileExecutionPlan(result.workspace);
|
||||||
|
const entry = plan.definition.bindingSchema.packages[0]!.exports[0]!;
|
||||||
|
assert.ok(
|
||||||
|
entry.dependencyPorts.some(
|
||||||
|
(port) => port.requirement.kind === "interface" && port.requirement.receiver === "input",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
assert.ok(
|
||||||
|
generatePortableTypeScriptBindings(plan.definition.bindingSchema, "runtime@1", { target: "server" }).includes(
|
||||||
|
'"receiver"',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("dynamic reference reads do not promise an unimplemented client input closure", () => {
|
||||||
|
const result = compile(pkg.replace("targets [server]", "targets [browser, server]"));
|
||||||
|
assert.equal(result.ok, false);
|
||||||
|
assert.ok(result.diagnostics.some((diagnostic) => diagnostic.message.includes("local acquisition")));
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user