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
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
+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,