Implement reusable interface conformance rules

This commit is contained in:
Timothy J. Aveni
2026-09-23 00:17:13 -07:00
parent 20e9f4b056
commit 8455599c3c
14 changed files with 2387 additions and 1638 deletions
+215 -10
View File
@@ -46,6 +46,8 @@ import {
type GenericDependencyPort,
specializePackageExport,
} from "../capability-model/index.js";
import { expandConformanceRules, type ConformanceRule } from "../capability-model/conformance-rules.js";
import type { ClosedTypeArgument, TypeParameter } from "../capability-model/generics.js";
import { GenericSourceTypes } from "./generic-types.js";
import {
defaultQueryBudgets,
@@ -1992,14 +1994,140 @@ const lowerRelationshipMaterialization = (
: undefined;
};
const lowerConformance = (
const withConformanceTypes = <T>(
state: LoweringState,
atom: AtomId | undefined,
parameters: TypeParameter[],
bindings: Map<string, ClosedTypeArgument>,
run: () => T,
): T => {
const previous = { self: state.types.self, parameters: state.types.parameters, bindings: state.types.bindings };
state.types.self = atom;
state.types.parameters = new Map(parameters.map((p) => [p.name, p]));
state.types.bindings = bindings;
try {
return run();
} finally {
Object.assign(state.types, previous);
}
};
const lowerConformanceRule = (state: LoweringState, context: ConformanceDeclContext): ConformanceRule | undefined => {
if (!context.stringLiteral()) {
loweringIssue(state, context, "missing-rule-id", "Interface conformance rules require a stable ID");
return undefined;
}
return withConformanceTypes(state, undefined, [], new Map(), () => {
const id = stringValue(context.stringLiteral()!);
const parameters = state.types.declareParameters(
context.typeParameters(),
JSON.stringify(["conformance-rule", id]),
);
const source = state.types.interfaceByName(
identifier(context.identifier(0)),
context.conformanceSourceArguments()?.typeArguments() ?? null,
);
const target = state.types.interfaceByName(identifier(context.identifier(1)), context.typeArguments());
const targetDefinition = state.types.definitions.get(target.definitionId)!;
const applications = [source, target];
const members = targetDefinition.template?.members ?? targetDefinition.members;
for (const item of context.conformanceItem()) {
if (
item.attachmentDecl() ||
item.initializationDecl() ||
item.relationshipMaterializationDecl() ||
item.stateFieldBindingDecl()
) {
loweringIssue(
state,
item,
"unsupported-rule-storage",
"Rules support operation and module bindings, not private storage, initialization or materialization",
);
continue;
}
const module = item.moduleBindingDecl();
if (module) {
const pkg = requireSymbol(state, state.packages, identifier(module.identifier(1)), module, "package");
const member = members.find((m) => m.displayName === identifier(module.identifier(0)));
if (
member?.kind !== "module" ||
!pkg?.definition?.modules?.some((m) => m.displayName === identifier(module.identifier(2)))
)
loweringIssue(state, module, "invalid-module-binding", "Expected a module member and imported module export");
}
const binding = item.operationBindingDecl();
if (!binding) continue;
const ref = binding.memberOperationRef();
const member = members.find((m) => m.displayName === identifier(ref.identifier()));
if (!member?.operations.some((op) => op.displayName === text(ref.operationName())))
loweringIssue(state, ref, "unknown-operation", "Unknown target interface member operation");
const provider = binding.operationProvider();
if (provider.INTERFACE()) {
const application = state.types.interfaceByName(identifier(provider.identifier(0)), provider.typeArguments());
applications.push(application);
const definition = state.types.definitions.get(application.definitionId)!;
const targetRef = provider.memberOperationRef()!;
if (
!(definition.template?.members ?? definition.members)
.find((m) => m.displayName === identifier(targetRef.identifier()))
?.operations.some((op) => op.displayName === text(targetRef.operationName()))
)
loweringIssue(state, provider, "unknown-operation", "Unknown forwarding member operation");
} else if (provider.PACKAGE()) {
const pkg = requireSymbol(state, state.packages, identifier(provider.identifier(0)), provider, "package");
const name = identifier(provider.identifier(1));
if (pkg && !pkg.exports.has(name))
loweringIssue(state, provider, "unknown-export", `Unknown package export ${name}`);
const arguments_ = state.types.arguments(provider.typeArguments());
const exportDefinition = pkg?.definition?.genericExports?.find((entry) => entry.displayName === name);
const expected = exportDefinition?.parameters ?? [];
if (expected.length !== arguments_.length || expected.some((p, i) => p.kind !== arguments_[i]?.kind))
loweringIssue(state, provider, "parameter-kind", "Package export argument kinds or arity do not match");
for (const block of [
provider.dependencyBindingBlock(),
provider.acceptanceBinding()?.dependencyBindingBlock(),
]) {
for (const dependency of block?.dependencyBinding() ?? []) {
if (dependency.INTERFACE())
applications.push(
state.types.interfaceByName(identifier(dependency.identifier(1)), dependency.typeArguments()),
);
}
}
} else {
loweringIssue(
state,
provider,
"unsupported-rule-provider",
"Rule operations forward an interface or bind a package export",
);
}
}
state.types.validateTemplate({
...targetDefinition,
template: {
parameters,
members: [],
requires: applications,
usesSelf: false,
aliases: [...state.types.aliases.values()],
},
});
return { id, parameters, source, target };
});
};
const lowerConformanceBody = (
parent: LoweringState,
context: ConformanceDeclContext,
privateAttachments: PersistentAttachment[],
implementingAtom?: AtomId,
): WorkspaceRevision["conformances"][number] | undefined => {
const state = { ...parent, interfaces: new Map(parent.interfaces) };
const atomName = identifier(context.identifier(0));
const interfaceName = identifier(context.identifier(1));
const atomId = requireSymbol(state, state.atoms, atomName, context, "atom");
const atomId = implementingAtom ?? requireSymbol(state, state.atoms, atomName, context, "atom");
let interfaceSymbol = requireSymbol(state, state.interfaces, interfaceName, context, "interface");
if (!atomId || !interfaceSymbol) {
return undefined;
@@ -2461,7 +2589,7 @@ const lowerWorkspace = (
}
}
const conformance = item.conformanceDecl();
if (conformance) {
if (conformance && !state.interfaces.has(identifier(conformance.identifier(0)))) {
state.types.self = state.atoms.get(identifier(conformance.identifier(0)));
const attachments = conformance.conformanceItem().flatMap((entry) => {
const declaration = entry.attachmentDecl();
@@ -2480,16 +2608,93 @@ const lowerWorkspace = (
}
}
const conformances = items.flatMap((item) => {
const context = item.conformanceDecl();
if (!context) {
return [];
const contexts = items.flatMap((item) => item.conformanceDecl() ?? []);
const rules = new Map<ConformanceRule, ConformanceDeclContext>();
const direct: ConformanceDeclContext[] = [];
for (const declaration of contexts) {
const name = identifier(declaration.identifier(0));
if (state.interfaces.has(name) && state.atoms.has(name)) {
loweringIssue(state, declaration, "ambiguous-conformance-source", name + " names both an atom and an interface");
continue;
}
const conformance = atGenericSource(context, () =>
lowerConformance(state, context, privateAttachments.get(context) ?? []),
if (state.interfaces.has(name)) {
const rule = atGenericSource(declaration, () => lowerConformanceRule(state, declaration));
if (rule) rules.set(rule, declaration);
} else {
if (declaration.typeParameters() || declaration.conformanceSourceArguments())
loweringIssue(state, declaration, "invalid-conformance-source", "Only interface rules take source parameters");
direct.push(declaration);
}
}
const origins = new Map<WorkspaceRevision["conformances"][number], ConformanceDeclContext>();
const conformances = direct.flatMap((declaration) => {
const atom = state.atoms.get(identifier(declaration.identifier(0)));
const result = atGenericSource(declaration, () =>
withConformanceTypes(state, atom, [], new Map(), () =>
lowerConformanceBody(state, declaration, privateAttachments.get(declaration) ?? []),
),
);
return conformance ? [conformance] : [];
if (result) origins.set(result, declaration);
return result ? [result] : [];
});
if (!state.diagnostics.length) {
const applications = expandConformanceRules(
[...rules.keys()],
conformances,
state.types.definitions,
(atom, bindings) => ({
...state.types.environment(),
self: atom,
arguments: bindings,
applyInterface: (id, args) =>
withConformanceTypes(state, atom, [], bindings, () => state.types.apply(id, args).revisionId),
}),
);
for (const application of applications) {
const declaration = rules.get(application.rule)!;
const result = atGenericSource(declaration, () =>
withConformanceTypes(state, application.atomId, application.rule.parameters, application.bindings, () =>
lowerConformanceBody(state, declaration, [], application.atomId),
),
);
if (result) {
const generated = {
...result,
id: application.id,
ruleOrigin: {
ruleId: application.rule.id,
sourceInterfaceRevisionId: application.sourceId,
line: declaration.start?.line ?? 0,
column: declaration.start?.column ?? 0,
},
};
conformances.push(generated);
origins.set(generated, declaration);
}
}
}
const candidates = new Map<string, WorkspaceRevision["conformances"][number]>();
for (const conformance of conformances) {
const key = JSON.stringify([conformance.atomId, conformance.interfaceRevisionId]);
const previous = candidates.get(key);
if (previous) {
const describe = (c: typeof conformance) =>
c.ruleOrigin
? "rule " + c.ruleOrigin.ruleId + " from " + c.ruleOrigin.sourceInterfaceRevisionId
: "conformance " + (c.id ?? "(unnamed)");
const message =
"Duplicate conformance for " +
conformance.atomId +
" as " +
conformance.interfaceRevisionId +
": " +
describe(previous) +
" conflicts with " +
describe(conformance);
for (const candidate of [previous, conformance])
loweringIssue(state, origins.get(candidate)!, "duplicate-conformance", message);
} else candidates.set(key, conformance);
}
const constructors = items.flatMap((item) => {
const constructor = item.constructorBindingDecl();