Support checked keyed relationships across portable operations and queries

This commit is contained in:
Timothy J. Aveni
2026-09-25 12:02:05 -07:00
parent e0e8563224
commit 1bc3c69de3
23 changed files with 1203 additions and 949 deletions
+60 -8
View File
@@ -445,6 +445,7 @@ const relationshipOperation = (
id: InterfaceOperation["id"],
target: EdgeEndpointConstraint,
cardinality: EdgeCardinality,
keyType?: "string" | "boolean" | "int64",
): InterfaceOperation => {
const targetType =
target.kind === "atom" ? valueType.atomRef(target.atomId) : valueType.interfaceRef(target.interfaceRevisionId);
@@ -463,7 +464,16 @@ const relationshipOperation = (
return {
id,
displayName,
inputType: targetType,
inputType:
keyType && displayName === "connect"
? {
kind: "record",
fields: {
mapKey: { kind: "scalar", name: keyType === "boolean" ? "bool" : keyType },
target: targetType,
},
}
: targetType,
outputType: valueType.unit,
mode: "call",
};
@@ -545,6 +555,9 @@ const lowerRelationshipMember = (
return undefined;
}
const cardinality = lowerCardinality(context.cardinality());
const keyType = context.KEYED()
? (stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64")
: undefined;
const operations: InterfaceOperation[] = [];
for (const operation of context.relationshipOperation()) {
if (operation.RESOLVE()) {
@@ -554,6 +567,7 @@ const lowerRelationshipMember = (
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
keyType,
),
);
} else if (operation.CONNECT()) {
@@ -563,6 +577,7 @@ const lowerRelationshipMember = (
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
keyType,
),
);
} else if (operation.DISCONNECT()) {
@@ -572,6 +587,7 @@ const lowerRelationshipMember = (
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
keyType,
),
);
} else {
@@ -581,12 +597,14 @@ const lowerRelationshipMember = (
capabilityId.operation(stringValue(operation.stringLiteral(0))),
target,
cardinality,
keyType,
),
relationshipOperation(
"watch-stop",
capabilityId.operation(stringValue(operation.stringLiteral(1))),
target,
cardinality,
keyType,
),
);
}
@@ -847,6 +865,15 @@ const lowerInterfaceTemplate = (
const target = types.targetExpression(relationship.targetConstraint());
const targetType: ValueTypeExpression = { kind: "object-ref", expectation: target };
const cardinality = lowerCardinality(relationship.cardinality());
const keyType = relationship.KEYED()
? (stringValue(relationship.stringLiteral(1)) as "string" | "boolean" | "int64")
: undefined;
const keyedTarget: ValueTypeExpression = keyType
? {
kind: "record",
fields: { mapKey: { kind: "scalar", name: keyType === "boolean" ? "bool" : keyType }, target: targetType },
}
: targetType;
const resolved: ValueTypeExpression =
cardinality === "exactly-one"
? targetType
@@ -872,6 +899,7 @@ const lowerInterfaceTemplate = (
target,
cardinality,
ordered: Boolean(relationship.ORDERED()),
...(keyType ? { keyType } : {}),
operations: relationship.relationshipOperation().flatMap((operation) => {
const id = capabilityId.operation(stringValue(operation.stringLiteral(0)));
if (operation.RESOLVE())
@@ -881,7 +909,12 @@ const lowerInterfaceTemplate = (
if (operation.CONNECT() || operation.DISCONNECT())
return [
{
...signature(operation.CONNECT() ? "connect" : "disconnect", id, targetType, valueType.unit),
...signature(
operation.CONNECT() ? "connect" : "disconnect",
id,
operation.CONNECT() ? keyedTarget : targetType,
valueType.unit,
),
...lowerExecution(operation.executionClause()),
},
];
@@ -957,7 +990,7 @@ const lowerQueryReference = (
const lowerDependencyPort = (state: LoweringState, context: DependencyPortContext): DependencyPort | undefined => {
const name = identifier(context.identifier(0));
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral()));
const id = capabilityId.dependencyPort(stringValue(context.stringLiteral(0)));
if (context.REFLECTION())
return {
id,
@@ -1016,6 +1049,9 @@ const lowerDependencyPort = (state: LoweringState, context: DependencyPortContex
kind: "edge",
target,
cardinality: lowerCardinality(context.cardinality()),
...(context.KEYED()
? { keyType: stringValue(context.stringLiteral(1)) as "string" | "boolean" | "int64" }
: {}),
primitives: primitives.filter((entry) => !invalid.includes(entry)) as EdgePrimitive[],
},
};
@@ -1218,7 +1254,7 @@ const lowerGenericPackageExport = (
const parameters = state.types.declareParameters(context.typeParameters(), JSON.stringify(["export", id]));
const dependencyPorts = (context.dependencyBlock()?.dependencyPort() ?? []).map((port): GenericDependencyPort => {
const base = {
id: capabilityId.dependencyPort(stringValue(port.stringLiteral())),
id: capabilityId.dependencyPort(stringValue(port.stringLiteral(0))),
displayName: identifier(port.identifier(0)),
};
const primitives =
@@ -1257,6 +1293,7 @@ const lowerGenericPackageExport = (
kind: "edge",
target: state.types.targetExpression(port.targetConstraint()!),
cardinality: lowerCardinality(port.cardinality()!),
...(port.KEYED() ? { keyType: stringValue(port.stringLiteral(1)) as "string" | "boolean" | "int64" } : {}),
primitives: primitives as EdgePrimitive[],
},
};
@@ -2182,10 +2219,25 @@ const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclCont
} else if (provider.STATE() || provider.EDGE()) {
const name = identifier(provider.identifier(0));
const expected = provider.STATE() ? "state" : "edge";
// A rule may traverse another owner's public inverse projection, just as
// a concrete conformance may. Resolve external edges after specialization;
// ordinary ownership validation still permits only public resolve access.
if (provider.EDGE() && !privateStorage.has(name)) continue;
// Concrete attachments are registered before rules, so unused rules can
// also check public traversal without waiting for an application.
if (provider.EDGE() && !privateStorage.has(name)) {
const external = requireSymbol(state, state.attachments, name, provider, "attachment");
const projection =
external?.attachment.kind === "edge"
? external.attachment.endpoints.find(
(endpoint) => endpoint.displayName === identifier(provider.identifier(1)),
)
: undefined;
if (external && (!projection?.publicTraversal || text(provider.edgePrimitive()) !== "resolve"))
loweringIssue(
state,
provider,
"invalid-rule-storage-binding",
"External rule storage access requires a public edge resolve projection",
);
continue;
}
if (privateStorage.get(name) !== expected)
loweringIssue(
state,