137 lines
5.8 KiB
TypeScript
137 lines
5.8 KiB
TypeScript
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<QueryDeclaration["argumentRequirements"]> = [];
|
||
|
|
const checked = {
|
||
|
|
...environment,
|
||
|
|
implementsInterface: (
|
||
|
|
target: Parameters<GenericTypeEnvironment["implementsInterface"]>[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: substitution.application(template.root),
|
||
|
|
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<typeof view> => !!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<string>,
|
||
|
|
) {
|
||
|
|
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<string>()): 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<GenericTypeEnvironment["implementsInterface"]>[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);
|
||
|
|
}
|