Implement typed module contracts and conformance bindings

This commit is contained in:
Timothy J. Aveni
2026-09-22 13:51:32 -07:00
parent 60b44a6637
commit 7173751f37
33 changed files with 2963 additions and 2261 deletions
+63 -18
View File
@@ -642,7 +642,19 @@ const lowerInterface = (
source: SourceRevision,
): InterfaceRevision => {
const alias = identifier(context.identifier());
const members = context.interfaceMember().flatMap((entry) => {
const members = context.interfaceMember().flatMap<InterfaceMember>((entry) => {
const module = entry.moduleMember();
if (module)
return [
{
kind: "module" as const,
id: capabilityId.member(stringValue(module.stringLiteral(0))),
displayName: identifier(module.identifier()),
contract: stringValue(module.stringLiteral(1)),
...(module.targetConstraint() ? { subject: state.types.target(module.targetConstraint()!) } : {}),
operations: [],
},
];
const value = entry.valueMember();
if (value) {
return [lowerValueMember(state, value)];
@@ -745,6 +757,16 @@ const lowerInterfaceTemplate = (
const members: InterfaceMember<ValueTypeExpression, ObjectTypeExpression>[] = context
.interfaceMember()
.map((entry) => {
const module = entry.moduleMember();
if (module)
return {
kind: "module" as const,
id: capabilityId.member(stringValue(module.stringLiteral(0))),
displayName: identifier(module.identifier()),
contract: stringValue(module.stringLiteral(1)),
...(module.targetConstraint() ? { subject: types.targetExpression(module.targetConstraint()!) } : {}),
operations: [],
};
const value = entry.valueMember();
if (value) {
const type = types.expression(value.valueType());
@@ -1333,23 +1355,26 @@ const lowerPackage = (
}),
);
const genericExports: GenericPackageExport[] = [];
const components: NonNullable<PackageRevision["components"]> = [];
const modules: NonNullable<PackageRevision["modules"]> = [];
const queries: QueryDeclaration[] = [];
const queryTemplates: QueryTemplate[] = [];
const exports = context.packageExport().flatMap((exportContext): PackageExport[] => {
const component = exportContext.componentDecl();
if (component) {
const runtime = identifier(component.identifier(1));
if (runtime !== "react")
loweringIssue(state, component, "unsupported-component-runtime", `Unsupported component runtime ${runtime}`);
components.push({
id: stringValue(component.stringLiteral()),
displayName: identifier(component.identifier(0)),
subject: new TypeSubstitution(state.types.environment()).application(
state.types.interface(component.interfaceType()),
),
runtime: "react",
});
const module = exportContext.moduleExport();
if (module) {
const interfaceRevisionId = new TypeSubstitution(state.types.environment()).application(
state.types.interface(module.interfaceType()),
);
const contract = state.types.definitions.get(interfaceRevisionId);
const member = contract?.members.find((m) => m.displayName === identifier(module.identifier(1)));
if (member?.kind !== "module")
loweringIssue(state, module, "invalid-module-export", "Module export must implement an imported module member");
else
modules.push({
id: stringValue(module.stringLiteral()),
displayName: identifier(module.identifier(0)),
interfaceRevisionId,
memberId: member.id,
});
return [];
}
if (exportContext.packageQuerySpecialization()) return [];
@@ -1611,7 +1636,7 @@ const lowerPackage = (
displayName: alias,
source,
exports,
...(components.length ? { components } : {}),
...(modules.length ? { modules } : {}),
...(queries.length ? { queries } : {}),
...(queryTemplates.length ? { queryTemplates } : {}),
...(queries.some((query) => query.argumentRequirements?.length)
@@ -1931,6 +1956,25 @@ const lowerConformance = (
state.types.self = previousSelf;
}
}
const moduleBindings: NonNullable<WorkspaceRevision["conformances"][number]["moduleBindings"]> = [];
for (const item of context.conformanceItem()) {
const binding = item.moduleBindingDecl();
if (!binding) continue;
const memberName = identifier(binding.identifier(0));
const member = interfaceSymbol.definition?.members.find((m) => m.displayName === memberName);
const pkg = requireSymbol(state, state.packages, identifier(binding.identifier(1)), binding, "package");
const module = pkg?.definition?.modules?.find((m) => m.displayName === identifier(binding.identifier(2)));
if (member?.kind !== "module" || !module || !pkg) {
loweringIssue(
state,
binding,
"invalid-module-binding",
"Expected a module member bound to an imported module export",
);
continue;
}
moduleBindings.push({ memberId: member.id, packageRevisionId: pkg.revisionId, moduleId: module.id });
}
const operationBindings = context
.conformanceItem()
.flatMap<WorkspaceRevision["conformances"][number]["operationBindings"][number]>((item) => {
@@ -2182,6 +2226,7 @@ const lowerConformance = (
...(context.INTEGER() ? { semanticMajor: Number(context.INTEGER()!.getText()) } : {}),
privateAttachments,
operationBindings,
...(moduleBindings.length ? { moduleBindings } : {}),
relationshipMaterializations,
};
};
@@ -2420,7 +2465,7 @@ const lowerWorkspace = (
const packageName = identifier(presentation.identifier(1));
const componentName = identifier(presentation.identifier(2));
const pkg = requireSymbol(state, state.packages, packageName, presentation, "package");
const component = pkg?.definition?.components?.find((entry) => entry.displayName === componentName);
const component = pkg?.definition?.modules?.find((entry) => entry.displayName === componentName);
if (!component) {
loweringIssue(state, presentation, "unknown-component", `Unknown component ${packageName}.${componentName}`);
return [];
@@ -2692,7 +2737,7 @@ const compileCapabilityResourceSourceInternal = (
externalInterfaces,
revision: lowerPackage(state, context, options.source),
};
if (resource.revision.components?.length) {
if (resource.revision.modules?.length) {
resource.revision.clientInterfaces = Object.fromEntries(
imports.flatMap((entry) => {
if (entry.kind !== "interface") return [];