import { createHash } from "node:crypto"; import { capabilityId, type AtomId, type InterfaceMember, type InterfaceOperation, type InterfaceRevision, type InterfaceRevisionId, type ObjectExpectation, type SourceRevision, type ValueType, } from "./types.js"; /** Authoring-only expressions. Installed ValueType deliberately has no variable case. */ export type ValueTypeExpression = | Extract | { kind: "parameter"; parameterId: string } | { kind: "record"; fields: Record } | { kind: "list" | "optional"; value: ValueTypeExpression } | { kind: "object-ref"; expectation: ObjectTypeExpression } | { kind: "alias"; definitionId: string; arguments: TypeArgumentExpression[] }; export type ObjectTypeExpression = | { kind: "atom"; atomId: AtomId } | { kind: "interface"; interfaceRevisionId: InterfaceRevisionId } | { kind: "parameter"; parameterId: string } | { kind: "self" } | { kind: "application"; application: InterfaceApplicationExpression }; export type TypeArgumentExpression = | { kind: "value"; type: ValueTypeExpression } | { kind: "object"; target: ObjectTypeExpression }; export type ClosedTypeArgument = { kind: "value"; type: ValueType } | { kind: "object"; target: ObjectExpectation }; export interface InterfaceApplicationExpression { definitionId: InterfaceRevisionId; arguments: TypeArgumentExpression[]; } export type TypeParameter = | { id: string; name: string; kind: "value"; storable?: boolean } | { id: string; name: string; kind: "object"; implements: InterfaceApplicationExpression[] }; export interface ValueAliasDefinition { id: string; parameters: TypeParameter[]; body: ValueTypeExpression; } export interface GenericInterfaceTemplate { parameters: TypeParameter[]; members: InterfaceMember[]; requires: InterfaceApplicationExpression[]; usesSelf: boolean; aliases?: ValueAliasDefinition[]; } /** Instantiation produces the existing closed runtime IR, with explicit provenance. */ export const instantiateInterface = ( definition: InterfaceRevision, arguments_: readonly ClosedTypeArgument[], environment: GenericTypeEnvironment, ): InterfaceRevision => { const template = definition.template; if (!template) { if (arguments_.length) fail("type-arity", definition.displayName, "Non-generic interface takes no type arguments"); return definition; } const lexicalEnvironment = { ...environment, aliases: new Map((template.aliases ?? []).map((alias) => [alias.id, alias])), }; const argumentsMap = bindTypeParameters(template.parameters, arguments_, lexicalEnvironment, definition.displayName); if (template.usesSelf && !environment.self) fail("unbound-self", definition.displayName, "Self requires an implementing atom"); const substitution = new TypeSubstitution({ ...lexicalEnvironment, arguments: argumentsMap }); const operation = (entry: InterfaceOperation): InterfaceOperation => ({ ...entry, inputType: substitution.value(entry.inputType, `${definition.displayName}.${entry.displayName}.input`), outputType: substitution.value(entry.outputType, `${definition.displayName}.${entry.displayName}.output`), eventType: entry.eventType ? substitution.value(entry.eventType, `${definition.displayName}.${entry.displayName}.event`) : undefined, }); const members: InterfaceMember[] = template.members.map((member) => { const common = { id: member.id, displayName: member.displayName, operations: member.operations.map(operation) }; switch (member.kind) { case "value": return { ...common, kind: "value", ...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}), valueType: substitution.value(member.valueType, member.displayName), }; case "relationship": return { ...common, kind: "relationship", ...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}), target: substitution.object(member.target, member.displayName), cardinality: member.cardinality, ordered: member.ordered, ...(member.keyType ? { keyType: member.keyType } : {}), }; case "operation": return { ...common, kind: "operation", inputType: substitution.value(member.inputType, member.displayName), outputType: substitution.value(member.outputType, member.displayName), }; } }); const application: AppliedInterfaceIdentity = { definitionId: definition.revisionId, source: definition.source, arguments: structuredClone([...arguments_]), ...(template.usesSelf ? { self: environment.self } : {}), }; return { interfaceId: definition.interfaceId, revisionId: appliedInterfaceId(application), displayName: definition.displayName, source: definition.source, members, application, requiredInterfaces: template.requires.map((required) => substitution.application(required)), }; }; export interface GenericTypeEnvironment { arguments: ReadonlyMap; self?: AtomId; aliases?: ReadonlyMap; /** Resolves only checked declarations, never a caller-supplied runtime type string. */ applyInterface: (definitionId: InterfaceRevisionId, arguments_: readonly ClosedTypeArgument[]) => InterfaceRevisionId; /** Proof in the candidate, not an authorization grant. */ implementsInterface: (target: ObjectExpectation, required: InterfaceRevisionId) => boolean; /** External codecs must explicitly declare reference-free persistence support. */ storableMessage?: (descriptorId: string) => boolean; } export class GenericTypeError extends Error { constructor( readonly code: string, readonly path: string, message: string, ) { super(`${path}: ${message}`); this.name = "GenericTypeError"; } } const fail = (code: string, path: string, message: string): never => { throw new GenericTypeError(code, path, message); }; /** One budget across nested aliases/substitutions, including concrete arguments. */ class Budget { private remaining = 10000; enter(path: string, depth: number) { if (depth > 128 || --this.remaining < 0) fail("type-complexity-limit", path, "Type exceeds the depth or expansion budget"); } } export const isStorableType = ( type: ValueType, storableMessage: (descriptorId: string) => boolean = () => false, ): boolean => { const budget = new Budget(); const visit = (value: ValueType, depth: number): boolean => { budget.enter("storable", depth); switch (value.kind) { case "scalar": return true; case "builtin": return value.name === "unit"; case "message": return storableMessage(value.descriptorId); case "object-ref": return false; case "list": case "optional": return visit(value.value, depth + 1); // Records currently have RPC codecs, not ordinary-state persistence codecs. // A generic bound must not promise storage that the installed model rejects. case "record": return false; } }; return visit(type, 0); }; /** Canonical closed-type encoding, independent of record insertion order. */ export const canonicalTypeArgument = (argument: ClosedTypeArgument): string => { const budget = new Budget(); const target = (value: ObjectExpectation): unknown => { switch (value.kind) { case "atom": return ["atom", value.atomId]; case "interface": return ["interface", value.interfaceRevisionId]; default: return fail("unresolved-type", "argument", "Expected a closed object target"); } }; const type = (value: ValueType, depth: number): unknown => { budget.enter("argument", depth); switch (value.kind) { case "builtin": case "scalar": return [value.kind, value.name]; case "message": return ["message", value.descriptorId]; case "object-ref": return ["object-ref", target(value.expectation)]; case "list": case "optional": return [value.kind, type(value.value, depth + 1)]; case "record": return [ "record", Object.keys(value.fields) .sort() .map((key) => [key, type(value.fields[key], depth + 1)]), ]; default: return fail("unresolved-type", "argument", "Expected a closed value type"); } }; switch (argument.kind) { case "value": return JSON.stringify(["value", type(argument.type, 0)]); case "object": return JSON.stringify(["object", target(argument.target)]); default: return fail("invalid-kind", "argument", "Expected a value or object type argument"); } }; export interface AppliedInterfaceIdentity { definitionId: InterfaceRevisionId; source: SourceRevision; arguments: ClosedTypeArgument[]; /** Only include Self when it is actually part of the closed contract. */ self?: AtomId; } export const appliedInterfaceId = (application: AppliedInterfaceIdentity): InterfaceRevisionId => { const encoding = JSON.stringify([ "quixos-applied-interface-v1", application.definitionId, application.source.repository, application.source.commit.toLowerCase(), application.arguments.map(canonicalTypeArgument), application.self ?? null, ]); return capabilityId.interfaceRevision( `interface-application:sha256:${createHash("sha256").update(encoding).digest("hex")}`, ); }; export class TypeSubstitution { private readonly budget = new Budget(); private readonly aliases: string[] = []; constructor(private environment: GenericTypeEnvironment) {} argument(expression: TypeArgumentExpression, path = "argument", depth = 0): ClosedTypeArgument { this.budget.enter(path, depth); switch (expression.kind) { case "value": return { kind: "value", type: this.value(expression.type, path, depth + 1) }; case "object": return { kind: "object", target: this.object(expression.target, path, depth + 1) }; default: return fail("invalid-kind", path, "Expected a value or object type argument"); } } application(expression: InterfaceApplicationExpression, path = "interface", depth = 0): InterfaceRevisionId { this.budget.enter(path, depth); return this.environment.applyInterface( expression.definitionId, expression.arguments.map((argument, index) => this.argument(argument, `${path}.arguments[${index}]`, depth + 1)), ); } object(expression: ObjectTypeExpression, path = "target", depth = 0): ObjectExpectation { this.budget.enter(path, depth); switch (expression.kind) { case "atom": return { kind: "atom", atomId: expression.atomId }; case "interface": return { kind: "interface", interfaceRevisionId: expression.interfaceRevisionId }; case "self": return this.environment.self ? { kind: "atom", atomId: this.environment.self } : fail("unbound-self", path, "Self requires an implementing atom"); case "parameter": { const argument = this.environment.arguments.get(expression.parameterId); if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`); if (argument.kind !== "object") return fail("parameter-kind", path, `Parameter ${expression.parameterId} is a value, not an object target`); canonicalTypeArgument(argument); return structuredClone(argument.target); } case "application": return { kind: "interface", interfaceRevisionId: this.application(expression.application, path, depth + 1) }; default: return fail("invalid-type", path, "Unknown object type expression"); } } value(expression: ValueTypeExpression, path = "type", depth = 0): ValueType { this.budget.enter(path, depth); switch (expression.kind) { case "builtin": return { kind: "builtin", name: expression.name }; case "scalar": return { kind: "scalar", name: expression.name }; case "message": return { kind: "message", descriptorId: expression.descriptorId }; case "list": case "optional": return { kind: expression.kind, value: this.value(expression.value, `${path}.${expression.kind}`, depth + 1) }; case "record": return { kind: "record", fields: Object.fromEntries( Object.entries(expression.fields).map(([name, field]) => [ name, this.value(field, `${path}.${name}`, depth + 1), ]), ), }; case "object-ref": return { kind: "object-ref", expectation: this.object(expression.expectation, `${path}.ref`, depth + 1) }; case "parameter": { const argument = this.environment.arguments.get(expression.parameterId); if (!argument) return fail("unbound-parameter", path, `Unbound parameter ${expression.parameterId}`); if (argument.kind !== "value") return fail("parameter-kind", path, `Parameter ${expression.parameterId} is an object target; use ref`); canonicalTypeArgument(argument); return this.value(argument.type, path, depth + 1); } case "alias": { const alias = this.environment.aliases?.get(expression.definitionId); if (!alias) return fail("unknown-alias", path, `Unknown type alias ${expression.definitionId}`); if (this.aliases.includes(alias.id)) return fail("recursive-alias", path, `Recursive value alias: ${[...this.aliases, alias.id].join(" -> ")}`); const arguments_ = expression.arguments.map((argument, index) => this.argument(argument, `${path}.arguments[${index}]`, depth + 1), ); const bindings = bindTypeParameters(alias.parameters, arguments_, this.environment, path); this.aliases.push(alias.id); try { // Reuse this expansion budget/stack; lexical parameter maps are restored. const previous = this.environment; this.environment = { ...previous, arguments: bindings }; try { return this.value(alias.body, `${path}.${alias.id}`, depth + 1); } finally { this.environment = previous; } } finally { this.aliases.pop(); } } default: return fail("invalid-type", path, "Unknown value type expression"); } } } export const bindTypeParameters = ( parameters: readonly TypeParameter[], arguments_: readonly ClosedTypeArgument[], environment: GenericTypeEnvironment, path = "parameters", ): ReadonlyMap => { if (parameters.length !== arguments_.length) fail("type-arity", path, `Expected ${parameters.length} type arguments, received ${arguments_.length}`); const bindings = new Map(); const names = new Set(); for (const [index, parameter] of parameters.entries()) { if ( !parameter.id || !parameter.name || parameter.name === "Self" || bindings.has(parameter.id) || names.has(parameter.name) ) fail("duplicate-parameter", path, `Invalid or duplicate parameter ${parameter.name}`); names.add(parameter.name); const argument = arguments_[index]; if (parameter.kind !== argument.kind) fail("parameter-kind", `${path}.${parameter.name}`, `Expected ${parameter.kind}, received ${argument.kind}`); canonicalTypeArgument(argument); bindings.set(parameter.id, structuredClone(argument)); } const substitution = new TypeSubstitution({ ...environment, arguments: bindings }); for (const parameter of parameters) { const argument = bindings.get(parameter.id)!; if ( parameter.kind === "value" && argument.kind === "value" && parameter.storable && !isStorableType(argument.type, environment.storableMessage) ) fail( "non-storable-argument", `${path}.${parameter.name}`, "State values cannot contain managed references or unsupported transport values", ); if (parameter.kind === "object" && argument.kind === "object") { for (const bound of parameter.implements) { const required = substitution.application(bound, `${path}.${parameter.name}.implements`); if (!environment.implementsInterface(argument.target, required)) fail("unsatisfied-bound", `${path}.${parameter.name}`, `Object target does not implement ${required}`); } } } return bindings; };