import { capabilityId, type InterfaceRevision, type InterfaceRevisionId } from "../capability-model/types.js"; import { TypeSubstitution, bindTypeParameters, type ClosedTypeArgument, type GenericTypeEnvironment, } from "../capability-model/generics.js"; import { GenericSourceTypes } from "../capability-language/generic-types.js"; import { compileQuery } from "./compile.js"; import { QueryCompileError, type QueryDeclaration, type QueryTemplate } from "./types.js"; export function specializeQueryTemplate( template: QueryTemplate, arguments_: ClosedTypeArgument[], environment: GenericTypeEnvironment, interfaces: () => readonly InterfaceRevision[], identity: { id: string; displayName: string }, ): QueryDeclaration { const obligations: NonNullable = []; const checked = { ...environment, implementsInterface: ( target: Parameters[0], required: InterfaceRevisionId, ) => { obligations.push({ target, required }); return environment.implementsInterface(target, required); }, }; const substitution = new TypeSubstitution({ ...checked, arguments: bindTypeParameters(template.parameters, arguments_, checked, template.declaration.displayName), }); return { ...structuredClone(template.declaration), ...identity, root: { kind: template.root.kind, interface: substitution.application(template.root.interface) }, views: template.views .map((view) => { const target = substitution.object(view.target); const interfaceRevisionId = substitution.application(view.interface); if (target.kind === "interface") { if (target.interfaceRevisionId !== interfaceRevisionId) throw new QueryCompileError( "QUERY_VIEW_REQUIRED", "An interface argument must use its exact selected query view", ); return undefined; } return { atomId: target.atomId, interfaceRevisionId }; }) .filter((view): view is NonNullable => !!view), allowances: template.allowances.map((allowance) => { const interfaceRevisionId = substitution.application(allowance.interface); const member = interfaces() .find((entry) => entry.revisionId === interfaceRevisionId) ?.members.find((entry) => entry.displayName === allowance.memberName); if (!member) throw new QueryCompileError("QUERY_ALLOWANCE", `Unknown template field ${allowance.memberName}`); return { interfaceRevisionId, memberId: member.id, uses: allowance.uses, reason: allowance.reason }; }), application: { templateId: template.declaration.id, arguments: structuredClone(arguments_) }, argumentRequirements: obligations, }; } /** Check a document with only its declared bounds available, even if no concrete * specialization is installed. No concrete atom fields can leak into this check. */ export async function checkQueryTemplate( template: QueryTemplate, interfaces: readonly InterfaceRevision[], read: (name: string) => Promise, ) { const types = new GenericSourceTypes(new Map()); for (const contract of interfaces) types.register(contract.displayName, contract); const arguments_: ClosedTypeArgument[] = template.parameters.map((parameter) => { if (parameter.kind !== "object") throw new QueryCompileError( "QUERY_TEMPLATE_PARAMETER", "Query templates currently require object parameters with explicit interface views; scalar/value polymorphism is not queryable", ); return { kind: "object", target: { kind: "atom", atomId: capabilityId.atom(`query-bound:${template.declaration.id}:${parameter.id}`) }, }; }); const bindings = new Map(template.parameters.map((parameter, index) => [parameter.id, arguments_[index]!])); const substitution = new TypeSubstitution({ ...types.environment(), arguments: bindings }); const evidence = new Map( arguments_.map((argument, index) => { const parameter = template.parameters[index]!; const target = argument.kind === "object" && argument.target.kind === "atom" ? argument.target.atomId : ""; return [ target, new Set( parameter.kind === "object" ? parameter.implements.map((bound) => substitution.application(bound)) : [], ), ]; }), ); const implies = (offered: string, required: string, seen = new Set()): boolean => { if (offered === required) return true; if (seen.has(offered)) return false; seen.add(offered); const contract = types.definitions.get(offered); return (contract?.requiredInterfaces ?? []).some((parent) => implies(parent, required, seen)); }; const environment = { ...types.environment(), implementsInterface: ( target: Parameters[0], required: InterfaceRevisionId, ) => target.kind === "atom" && [...(evidence.get(target.atomId) ?? [])].some((offered) => implies(offered, required)), }; const declaration = specializeQueryTemplate( template, arguments_, environment, () => [...types.definitions.values()], template.declaration, ); for (const view of declaration.views) if (!environment.implementsInterface({ kind: "atom", atomId: view.atomId }, view.interfaceRevisionId)) throw new QueryCompileError( "QUERY_TEMPLATE_BOUND", `The declared bounds do not guarantee the selected view ${view.interfaceRevisionId}`, ); for (const application of types.applications.values()) for (const obligation of application.argumentRequirements ?? []) if (!environment.implementsInterface(obligation.target, obligation.required)) throw new QueryCompileError( "QUERY_TEMPLATE_BOUND", `Template arguments do not guarantee ${obligation.required}`, ); return compileQuery(declaration, [...types.definitions.values()], read); }