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
@@ -14,7 +14,7 @@ export async function scaffoldComponent(
throw Error("Component scaffold requires a PascalCase name, ID and QX declaration");
const source = `package Draft id "draft" revision "draft@1" { ${spec.declaration} }`;
const syntax = parseQx(source);
const declarations = [...walkSyntax(syntax.root)].filter((node) => node.kind === "componentDecl");
const declarations = [...walkSyntax(syntax.root)].filter((node) => node.kind === "moduleExport");
const pkg = syntax.root.children.find((node) => node.kind === "packageResourceDecl");
if (
syntax.diagnostics.length ||
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
EDIT=1
COMPONENT=2
PRESENTATION=3
SUBJECT=4
RUNTIME=5
MODULE=2
CONTRACT=3
PRESENTATION=4
SUBJECT=5
FOR=6
CREATION=7
INITIALIZE=8
@@ -152,10 +152,10 @@ LINE_COMMENT=151
BLOCK_COMMENT=152
WS=153
'edit'=1
'component'=2
'presentation'=3
'subject'=4
'runtime'=5
'module'=2
'contract'=3
'presentation'=4
'subject'=5
'for'=6
'creation'=7
'initialize'=8
File diff suppressed because one or more lines are too long
@@ -1,8 +1,8 @@
EDIT=1
COMPONENT=2
PRESENTATION=3
SUBJECT=4
RUNTIME=5
MODULE=2
CONTRACT=3
PRESENTATION=4
SUBJECT=5
FOR=6
CREATION=7
INITIALIZE=8
@@ -152,10 +152,10 @@ LINE_COMMENT=151
BLOCK_COMMENT=152
WS=153
'edit'=1
'component'=2
'presentation'=3
'subject'=4
'runtime'=5
'module'=2
'contract'=3
'presentation'=4
'subject'=5
'for'=6
'creation'=7
'initialize'=8
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -20,6 +20,7 @@ import { TypeArgumentsContext } from "./QuixosCapabilityParser.js";
import { TypeArgumentContext } from "./QuixosCapabilityParser.js";
import { TypeAliasDeclContext } from "./QuixosCapabilityParser.js";
import { InterfaceMemberContext } from "./QuixosCapabilityParser.js";
import { ModuleMemberContext } from "./QuixosCapabilityParser.js";
import { OperationMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberContext } from "./QuixosCapabilityParser.js";
import { ValueMemberOperationContext } from "./QuixosCapabilityParser.js";
@@ -29,7 +30,7 @@ import { RelationshipOperationContext } from "./QuixosCapabilityParser.js";
import { TargetConstraintContext } from "./QuixosCapabilityParser.js";
import { PackageResourceDeclContext } from "./QuixosCapabilityParser.js";
import { PackageExportContext } from "./QuixosCapabilityParser.js";
import { ComponentDeclContext } from "./QuixosCapabilityParser.js";
import { ModuleExportContext } from "./QuixosCapabilityParser.js";
import { PresentationDeclContext } from "./QuixosCapabilityParser.js";
import { PackageQueryContext } from "./QuixosCapabilityParser.js";
import { PackageQuerySpecializationContext } from "./QuixosCapabilityParser.js";
@@ -56,6 +57,7 @@ import { EdgeDeclContext } from "./QuixosCapabilityParser.js";
import { EdgeEndpointContext } from "./QuixosCapabilityParser.js";
import { ConformanceDeclContext } from "./QuixosCapabilityParser.js";
import { ConformanceItemContext } from "./QuixosCapabilityParser.js";
import { ModuleBindingDeclContext } from "./QuixosCapabilityParser.js";
import { InitializationDeclContext } from "./QuixosCapabilityParser.js";
import { InitializationFieldContext } from "./QuixosCapabilityParser.js";
import { CreationOwnerMappingContext } from "./QuixosCapabilityParser.js";
@@ -199,6 +201,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitInterfaceMember?: (ctx: InterfaceMemberContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleMember`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleMember?: (ctx: ModuleMemberContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.operationMember`.
* @param ctx the parse tree
@@ -254,11 +262,11 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
*/
visitPackageExport?: (ctx: PackageExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.componentDecl`.
* Visit a parse tree produced by `QuixosCapabilityParser.moduleExport`.
* @param ctx the parse tree
* @return the visitor result
*/
visitComponentDecl?: (ctx: ComponentDeclContext) => Result;
visitModuleExport?: (ctx: ModuleExportContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.presentationDecl`.
* @param ctx the parse tree
@@ -415,6 +423,12 @@ export class QuixosCapabilityVisitor<Result> extends AbstractParseTreeVisitor<Re
* @return the visitor result
*/
visitConformanceItem?: (ctx: ConformanceItemContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.moduleBindingDecl`.
* @param ctx the parse tree
* @return the visitor result
*/
visitModuleBindingDecl?: (ctx: ModuleBindingDeclContext) => Result;
/**
* Visit a parse tree produced by `QuixosCapabilityParser.initializationDecl`.
* @param ctx the parse tree
+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 [];